pglifecycle reads a PostgreSQL database into plain YAML, with one file for each object. It validates those files against JSON Schema and builds archives that work with pg_restore.
$ pglifecycle pull -h db.internal -U postgres -d app schema/ --exclude-schema pgq
pglifecycle v2.0.0-alpha.0 Creating postgres@db.internal → schema/
Created schema/ from postgres@db.internal with 3036 objects:
37 schemas 278 sequences 80 views
13 extensions 1734 tables 11 materialized views
3 domains 523 functions 191 users
62 types 104 roles
$ git diff --stat
schema/tables/public/users.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
Review a schema change as an ordinary Git diff.
When a table has changed across many numbered SQL files, understanding its current definition means following the full migration history or inspecting a database. pglifecycle adds a checked-in representation of the resulting schema.
The current shape of users may depend on several SQL files. A reviewer has to combine those changes to understand the complete table.
pglifecycle stores each object as structured data. The file for users contains the table definition that will be used when the project is built.
Create a project, pull a live database into YAML, build those files into a restorable archive, or deploy the differences to a database.
Connect to a database and write its objects to a schema directory. pglifecycle uses the PostgreSQL catalog information exposed through pg_dump.
--update, rewrites only files whose content has changed.$ pglifecycle pull -h db.internal -U postgres -d app schema/ --exclude-schema pgq
pglifecycle v2.0.0-alpha.0 Creating postgres@db.internal → schema/
Created schema/ from postgres@db.internal with 3036 objects:
37 schemas 278 sequences 80 views
13 extensions 1734 tables 11 materialized views
3 domains 523 functions 191 users
62 types 104 roles
Compile the schema directory into an archive accepted by pg_restore. libpgdump orders the objects using PostgreSQL's dependency information.
pg_restore.$ pglifecycle build # files → archive schema/ build.dump topological sort … 3036 objects wrote build.dump ✓ restore-order verified
Create an empty project directory. Use it as the destination for pull, or add schema objects by hand.
project.yaml config..gitkeep files so empty dirs survive your first commit.$ pglifecycle create # scaffold → disk my-project/ created my-project/ ✓ project.yaml · tables/ · views/ · roles/ ✓ functions/ · schemata/ · sequences/
Compare the project with a live database and generate the DDL needed to reconcile them. Output goes to stdout or -o FILE. With --apply, pglifecycle runs the script through psql in one transaction.
CREATE OR REPLACE for functions and views, ALTER TYPE … ADD VALUE for enums.--allow-drop, and --apply refuses while any are pending.pg_dump -Fc file instead of a live connection with -D.$ pglifecycle deploy # diff → DDL -d app_production -o deploy.sql schema/ Comparing app against postgres@localhost/app_production Plan: 7 statement(s) included, 1 excluded wrote deploy.sql $ psql --single-transaction -v ON_ERROR_STOP=1 -f deploy.sql
Start with a database dump, convert the database to YAML, build and restore it, then dump the restored database. Comparing the two archive listings checks that the same objects appear in the same restore order.
$ pg_dump -Fc app_production -f original.dump $ pglifecycle pull --dbname app_production schema/ $ pglifecycle build schema/ roundtrip.dump $ pg_restore -d app_roundtrip roundtrip.dump $ pg_dump -Fc app_roundtrip -f roundtrip2.dump $ pg_restore -l original.dump > a.txt; pg_restore -l roundtrip2.dump > b.txt $ diff a.txt b.txt && echo "round-trip OK" round-trip OK # run this comparison in CI to detect a round-trip difference
pglifecycle derives most dependencies from the schema objects and passes them to libpgdump for topological sorting. You do not have to maintain a separate, hand-ordered manifest.
There is no order.txt or set of numbered prefixes. Most relationships come from the objects themselves. A dependencies: key handles relationships that cannot be inferred.
Views, grants, and foreign keys already refer to other objects. pglifecycle uses those references to construct the dependency graph.
During a build, libpgdump uses the topological sorting behavior from pg_dump to place objects in restore order.
The project format is intended for version control, while the build process stays compatible with PostgreSQL's archive and restore tools.
Schema objects are stored as YAML, where they can be reviewed, queried, linted, or generated with existing tools.
Each object is validated against the published JSON Schema when the project loads, so invalid fields and shapes are reported before a build or deploy.
Builds use the pg_dump archive format and its topological ordering, and the result can be restored with pg_restore.
Each table, view, function, and role has its own file, keeping diffs and Git history scoped to the object that changed.
Roles, memberships, and per-object ACLs, including PUBLIC, are stored in the project alongside schema objects.
libpgfmt normalizes function, view, and trigger bodies so formatting differences do not dominate a diff.
pglifecycle is distributed as a static binary for Linux and macOS. It does not require an interpreter or a project-level runtime environment.
These examples show a table, the ACL for the PUBLIC role, and the directory layout produced and consumed by pglifecycle.
# schema/tables/public/users.yaml name: users schema: public owner: app comment: Application users columns: - name: id data_type: BIGINT nullable: false generated: sequence_behavior: ALWAYS - name: email data_type: citext nullable: false - {name: full_name, data_type: TEXT} - name: created_at data_type: TIMESTAMP WITH TIME ZONE nullable: false default: CURRENT_TIMESTAMP primary_key: id indexes: - name: users_email_key columns: - name: email unique: true dependencies: extensions: - citext
The columns, generated identity, primary key, indexes, and comment are kept together in the table's YAML file.
pull writes this representation from a database. build converts it into the statements stored in the output archive.
schema and name, so both are optional in the body.# schema/roles/PUBLIC.yaml # The PUBLIC pseudo-role, locked down by default. name: PUBLIC create: false # defined, never created revocations: schemata: public: - ALL databases: app: - ALL grants: databases: app: - CONNECT
The implicit PUBLIC role has its own versioned file, so changes to its grants and revocations appear in pull requests.
Grants and revocations are grouped by object type and used to reproduce the ACL during a build.
git blame like any other change.Objects are grouped first by kind and then by schema, as in tables/public/users.yaml. You can browse the project without connecting to a database.
The binary validates every file against its JSON Schema when the project loads.
The parser, archive handling, and SQL formatting are implemented in separate, published libraries.
An incremental parser for the PostgreSQL SQL dialect. It parses function, view, and check bodies into syntax trees for validation and formatting.
Reads and writes the pg_dump archive format and implements its topological object ordering for generated archives.
A formatter for PostgreSQL SQL that normalizes embedded bodies to a consistent style before they are written to YAML.
Version 2 rewrites the CLI in Rust and distributes it as a static binary. The project files are validated against a defined schema.
pglifecycle is available under the BSD 3-Clause license, the same license used by PostgreSQL.
Install the binary, pull the database into a schema directory, and review the generated files.
$ brew tap gmr/postgres $ brew install pglifecycle $ pglifecycle --version pglifecycle 2.0.0-alpha.0
Homebrew 6.0+ may require trusting the tap first: brew trust --formula gmr/postgres/pglifecycle
$ cargo install pglifecycle $ pglifecycle --version pglifecycle 2.0.0-alpha.0
$ docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/project" \ ghcr.io/gmr/pglifecycle:latest build my-project/ mydb.dump
The image bundles the PostgreSQL 17 client tools pglifecycle shells out to, and works in /project. It runs as uid 65532, so --user hands the container your own uid for a bind-mounted directory you own.
Pre-built binaries for Linux and macOS (x86_64 and aarch64) are attached to each release. No Rust toolchain required.
Browse releases →