This guide installs pglifecycle, pulls an existing database into YAML, changes a table, previews the generated DDL, and applies it.
PATH. pglifecycle runs pg_dump and pg_dumpall to read a cluster, and psql to apply a script.pg_dumpall --roles-only.-h/PGHOST, -U/PGUSER, and -d/PGDATABASE.pglifecycle is distributed as a single static binary. Select your platform for installation options.
Confirm that the binary is available before continuing.
$ brew tap gmr/postgres $ brew install pglifecycle $ pglifecycle --version pglifecycle 2.0.0-alpha.1
If Homebrew fails with build.rb … exited with 1, run brew trust --formula gmr/postgres/pglifecycle first.
# includes the PostgreSQL 17 client tools $ docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/project" \ ghcr.io/gmr/pglifecycle:latest --version
Pre-built Linux binaries for x86_64 and aarch64 are attached to each release.
# requires a Rust toolchain $ cargo install pglifecycle $ pglifecycle --version pglifecycle 2.0.0-alpha.1
Pre-built macOS binaries are also available on the releases page.
pull reads the database and writes one YAML file for each object. The project directory contains project.yaml and directories for tables, views, functions, roles, and other object types. Objects that belong to a schema are grouped by schema.
Commit the project before changing it. This gives you a baseline for the changes that follow.
-N/--exclude-schema, -T/--exclude-table, and --exclude-extension. Each is repeatable.pg_dumpall and are included by default. Leave them out with --no-roles, or leave privileges out entirely with -x.--include-password-hashes. They are written to the project as plain text, so only enable it for a repository you trust.pg_dump -Fc file instead of a live database, pass it with -D. Roles are skipped because there is no cluster to read them from.$ pglifecycle pull -h localhost -U postgres -d app_production schema/ pglifecycle v2.0.0-alpha.1 Creating app_production@localhost → schema/ Created schema/ with 412 objects: 4 schemas 38 sequences 11 views 2 extensions 37 tables 1 materialized view 3 types 26 functions 14 users 9 roles
$ git init -q # if this is a new repository $ git add schema/ $ git commit -qm "Pull app_production into schema/"
Edit the YAML file for the object you want to change. In this example, the users table gets a last_login_at column and an index. The file path identifies the schema and object name, so those fields are optional in the file.
Every file is validated against the JSON Schema for its object type when the project loads, so a typo in a key or an unknown field is reported before pglifecycle talks to a database.
The result is a small Git diff that shows the table definition being changed.
name: users schema: public owner: app columns: - name: id data_type: BIGINT nullable: false - {name: email, data_type: citext, nullable: false} - name: created_at data_type: TIMESTAMP WITH TIME ZONE nullable: false default: CURRENT_TIMESTAMP - name: last_login_at data_type: TIMESTAMP WITH TIME ZONE primary_key: id indexes: - name: users_email_key columns: - name: email unique: true - name: users_last_login_at_idx columns: - name: last_login_at
$ git diff --stat schema/ schema/tables/public/users.yaml | 5 +++++ 1 file changed, 5 insertions(+)
deploy compares the project with a database and generates the DDL needed to make them match. Without --apply, it does not change the database. The DDL is written to standard output or to the file specified by -o.
Add -v to show a summary of the plan. Log messages go to standard error, leaving standard output available for the generated SQL.
CREATE OR REPLACE for functions and views, ALTER TYPE … ADD VALUE for enums.--allow-drop. Each one excluded is reported as a warning.-D, or leave grants out of the comparison with -x.If you want to review the SQL in the pull request, write it to a file and commit it with the YAML change.
$ pglifecycle deploy -v -d app_production schema/ INFO pglifecycle v2.0.0-alpha.1 running deploy INFO Comparing app_production against localhost:5432/app_production INFO Plan: 2 statement(s) included, 0 excluded -- pglifecycle deploy -- project: app_production -- source: localhost:5432/app_production -- destructive statements: none -- TABLE public.users ALTER TABLE public.users ADD COLUMN last_login_at timestamp with time zone; -- INDEX public.users_last_login_at_idx CREATE INDEX users_last_login_at_idx ON public.users USING btree (last_login_at);
$ pglifecycle deploy -d app_production -o deploy.sql schema/ # or compare against a dump rather than a live database $ pglifecycle deploy -D app_production.dump -o deploy.sql schema/
With --apply, pglifecycle runs the generated script through psql using --single-transaction and ON_ERROR_STOP=1. If a statement fails, the transaction is rolled back.
--apply refuses to run while destructive statements are pending. Review them, then re-run with --allow-drop to include them.--apply cannot be combined with -D: there is nothing to apply to in a dump file.To verify the result, pull the database again with --update. It rewrites only files whose contents changed. If git diff has no output, the database and project match.
$ pglifecycle deploy -v -d app_production --apply schema/ INFO Comparing app_production against localhost:5432/app_production INFO Plan: 2 statement(s) included, 0 excluded INFO Applying 2 statement(s) ✓ Deploy applied successfully
$ pglifecycle pull -d app_production --update schema/ pglifecycle v2.0.0-alpha.1 Updating app_production@localhost → schema/ $ git diff --stat schema/ (no output: the project matches the database)
Test the deployment against a copy of production before applying it to the production database.
A pglifecycle project is a directory of text files, so it works with the same Git workflow you use for code:
deploy without --apply against a copy of the target database. Review the generated DDL with the YAML change.--apply.pull --update against the live database periodically. A non-empty git diff shows a change made outside the project.deploy compares the project with the database's current state. It does not replay a sequence of migrations.
A GitHub Action for installing pglifecycle is in progress. Until it is available, install a release binary or use the container image in your job.
name: schema on: [pull_request] jobs: preview: runs-on: ubuntu-latest container: ghcr.io/gmr/pglifecycle:latest steps: - uses: actions/checkout@v4 - name: Preview the DDL env: PGHOST: ${{ secrets.PGHOST }} PGUSER: ${{ secrets.PGUSER }} PGDATABASE: app_staging PGPASSWORD: ${{ secrets.PGPASSWORD }} run: pglifecycle deploy -v -o deploy.sql schema/ - uses: actions/upload-artifact@v4 with: name: deploy-sql path: deploy.sql
$ git checkout -b users-last-login $ $EDITOR schema/tables/public/users.yaml $ pglifecycle deploy -d app_staging schema/ # read before committing $ git commit -am "Track last_login_at on users" $ git push -u origin users-last-login
Store the deploy credentials as repository secrets; pglifecycle reads the standard PG* variables, so no flags carry them.
You can also build a restorable archive or create a new project from scratch.
# compile the project into a pg_restore-compatible archive $ pglifecycle build schema/ app.dump $ createdb app_review && pg_restore -d app_review app.dump # or start a project from nothing and write the objects by hand $ pglifecycle create new-project/