Ptah or Liquibase Community for an Existing PostgreSQL Workflow?
Run the same PostgreSQL change with Liquibase Community and Ptah, then compare edited history, rollback, and checks for changes made outside the changelog.
Ptah helps you plan, review, and apply database migrations. Try in your browser
A team already deploys PostgreSQL changes from a Liquibase changelog. Before its next change, it has to decide whether to keep that workflow, move to Ptah, or add Ptah for one job. The answer depends on which Liquibase features the changelog uses, and on what the team needs to know about the live database.
| Your situation | Our recommendation | Reason |
|---|---|---|
The changelog uses contexts, labels, preconditions, or runAlways |
Keep Liquibase | Ptah has no equivalent, and ptah migrations import refuses those changesets. |
| Your database is not in Ptah’s support matrix | Keep Liquibase | Liquibase supports more databases. |
| You keep Liquibase and want CI to catch changes made outside the changelog | Add ptah schema drift |
It fails the job on differences that Liquibase Community does not report. |
| You start a new service, or want SQL generated from a schema file | Use Ptah | A reviewed file in Git replaces the second database that Liquibase generation needs. |
We ran one change with Liquibase Community 5.0.4, Ptah 0.23.0, and PostgreSQL 18.6. The inputs and full output are beside this post. Liquibase Secure is the commercial edition and adds some of the missing features. We did not run it, so we cite its documentation where it matters.
How each tool describes a change
Section titled “How each tool describes a change”A Liquibase changeset is an instruction. Its identity is its id, its author, and the path of its changelog. Liquibase records that identity with a checksum and runs each changeset once, in order.
Ptah can work the same way with handwritten SQL. It can also start from a desired schema file, compare that file with the database, and write the migration for you to review.
| Job | Liquibase Community 5.0.4 | Ptah 0.23.0 |
|---|---|---|
| Apply reviewed SQL in order | Supported: update |
Supported: migrations up |
| Preview the SQL | Supported: update-sql |
Supported: migrations up --dry-run |
| Generate SQL | Partial: diff-changelog needs a second database that already has the change |
Supported: migrations generate, from a schema file |
| Refuse an edited applied change | Supported: checksums | Supported: ptah.sum and recorded checksums |
| Roll back the latest change | Supported: rollback-count, with SQL you write |
Supported: migrations down, with a generated file you review |
| Roll back one change out of order | Unsupported: commercial rollback-one-changeset |
Unsupported |
| Stop a pending destructive statement | Unsupported: commercial policy checks | Supported: migrations up needs --allow-destructive |
| Fail CI on live schema drift | Partial: diff reports some differences and exits 0 |
Supported: schema drift |
| Select changes by environment | Supported: contexts and labels | Unsupported |
| Run a change conditionally or repeatedly | Supported: preconditions, runAlways |
Unsupported |
Make the same change in both tools
Section titled “Make the same change in both tools”Both databases started from the two changesets and two rows of our Liquibase handoff guide. The change adds cancel_at_period_end to public.subscriptions.
With Liquibase, the team writes the SQL and its rollback:
--changeset team:3ALTER TABLE public.subscriptions ADD COLUMN cancel_at_period_end boolean NOT NULL DEFAULT false;--rollback ALTER TABLE public.subscriptions DROP COLUMN cancel_at_period_end;update-sql previews what update will run. These two lines are the full preview’s only application statement. The rest of its output is Liquibase’s own history and lock bookkeeping:
-- Changeset changelog.sql::3::teamALTER TABLE public.subscriptions ADD COLUMN cancel_at_period_end boolean NOT NULL DEFAULT false;Community 5 ships without database drivers, so a new installation stops with Cannot find database driver until you run liquibase lpm add postgresql --global.
With Ptah, the team edits the desired schema:
CREATE TABLE public.subscriptions ( id bigint PRIMARY KEY, account_id bigint NOT NULL, plan text NOT NULL, cancel_at_period_end boolean NOT NULL DEFAULT false);CREATE INDEX subscriptions_account_idx ON public.subscriptions (account_id);Then Ptah writes the migration. generate replays it up, down, and up on a shadow database, which it resets, so never point --shadow-db at an application database:
ptah migrations generate --db-url "$PTAH_DB_URL" --schema-file schema.sql --schemas public --migrations-dir migrations --name cancel_at_period_end --shadow-db "$PTAH_SHADOW_URL"ptah migrations hash --dir migrationsptah migrations up --db-url "$PTAH_DB_URL" --migrations-dir migrations --verify-sum --dry-runptah migrations up --db-url "$PTAH_DB_URL" --migrations-dir migrations --verify-sumALTER TABLE "public"."subscriptions" ADD COLUMN "cancel_at_period_end" boolean NOT NULL DEFAULT false;Both tools ended with the same column and the same two rows.
Liquibase can generate SQL too, and diff-changelog is in Community. It compares two databases, so a second database must already hold the change. That is the difference to weigh: Liquibase generates from a database, and Ptah generates from a file you review in Git. From a reference database with the new column, Liquibase wrote a changeset with this statement and no rollback line:
ALTER TABLE "subscriptions" ADD "cancel_at_period_end" BOOLEAN DEFAULT FALSE NOT NULL;Edit or move a change that already ran
Section titled “Edit or move a change that already ran”We changed DEFAULT false to DEFAULT true in the applied change. Liquibase refused, and named the changeset:
ERROR: Exception Primary Reason: Validation Failed: 1 changesets check sum changelog.sql::3::team was: 9:c881c6cb28e75038b78f436dd1a88e96 but is now: 9:cdabc4aee37ad0a6902ed780e61c191aPtah refused because the directory no longer matched ptah.sum:
error: migration sum verification failed:migration directory does not match ptah.sum: changed: 1794067402_cancel_at_period_end.up.sqlRehashing the directory does not get around this. migrations status --verify-sum --exit-code then compares the file with the checksum the database recorded and exits 1.
Each tool also has an explicit override: Liquibase’s clear-checksums and Ptah’s migrations repair --force. After either one, the tool reported success while the file said true and the server still said false (Liquibase, Ptah). Use them only when you understand the difference. clear-checksums clears every changeset’s checksum, while repair takes one version.
Moving a file is where the identity models differ. We moved the changelog to db/changelog.sql, and Liquibase treated every changeset as new. It tried to run the first one again and failed with relation "subscriptions" already exists. Adding logicalFilePath:changelog.sql to each changeset in the moved file fixed that, so plan for it before you reorganize a changelog. Ptah identifies a migration by its version, and the moved directory passed with no change.
Roll back, and stop a destructive change
Section titled “Roll back, and stop a destructive change”Liquibase runs the rollback SQL you wrote, and rollback-count-sql previews it. A raw SQL changeset with no rollback line fails only when you roll it back, so run update-testing-rollback on a disposable database. It applies, rolls back, and reapplies each change, and it found the missing rollback on our fixture.
Ptah wrote this down statement for us, and the shadow replay ran it before the migration reached a real database:
ALTER TABLE "public"."subscriptions" DROP COLUMN "cancel_at_period_end";migrations down --target ran it and removed the column. An imported changeset with no rollback SQL gets a down file with no statements, and migrations down refuses it, as Liquibase does. If the change truly needs no rollback, mark the file with -- +ptah empty_rollback. In either tool, a rollback that drops a column also drops its values, so it does not replace a recovery plan.
The tools differ on a pending destructive statement. Liquibase Community applied a DROP COLUMN with no warning, because its policy checks need a license. Ptah stopped before running anything and exited 2:
error: error running migrations: pending migrations contain destructive statements; rerun with --allow-destructive after review:- guard/0000000002_drop_plan.up.sql:1 DS102 error: DROP COLUMN permanently deletes the column's data; deploy readers that no longer use the column first, then drop it in a later releaseThe migration ran once we passed --allow-destructive.
Catch a change made outside the changelog
Section titled “Catch a change made outside the changelog”We added a column by hand. Liquibase status still said the database was up to date, and validate found no errors. Both are correct: they compare the changelog with the history, and neither reads the table.
Community’s check for this is diff, which compares two databases. It found the column, with a clean copy built from the changelog as its reference:
Unexpected Column(s): public.subscriptions.manual_notePtah compares the database with the schema file, so no second database is needed:
ptah schema drift --schema-file schema.sql --db-url "$LB_DB_URL" --schemas public --exclude public.databasechangelog --exclude public.databasechangeloglockSchema drift detected (highest severity: destructive).Failure threshold: all. Failing: true.Database: postgres://postgres:***@127.0.0.1:55520/lb_lab?sslmode=disable
Findings:- columns_removed: 1 (destructive)Liquibase keeps its history in public by default, so we left it there. --exclude keeps those two tables out of the check. Without it, the same command reports them as destructive drift on a database that matches the file.
Those two results agree, so we repeated the test with one hand-made change per fresh copy of the database:
| Change made by hand | Liquibase diff |
Ptah schema drift |
|---|---|---|
| Column | Reported | Reported |
| Column default | Reported | Reported |
| Table comment | Reported | Reported |
| Check constraint | Not reported | Reported |
| Index predicate, with the same name and columns | Not reported | Reported |
| Function | Not reported | Reported |
| Trigger | Not reported | Reported |
| Row-level security | Not reported | Reported |
| Grant to a role the schema file declares | Not reported | Reported |
The per-change results are beside this post. Asking Community for those object types fails with Unknown snapshot type(s); Liquibase documents them for the commercial edition. Ptah compares grants only for roles the schema file declares, so it did not report a grant to a role the file leaves out.
The exit code decides whether a pipeline can use the result:
| Command | What it compares | Exit code with a hand-made column |
|---|---|---|
liquibase status |
Changelog and history | 0 |
liquibase validate |
Changelog and history | 0 |
liquibase diff |
Two databases | 0 |
ptah schema drift |
Schema file and database | 1, and 0 once they match |
Community also refuses --format=JSON without a license, so a job would have to parse the text report. The Ptah check needs a schema file that matches the deployed release. Comparing a database with the schema of a release you have not applied reports the intended change as drift.
What Ptah does not replace
Section titled “What Ptah does not replace”Contexts are the clearest case. Liquibase filtered a demo-data changeset with --contexts=prod and ran it with --contexts=dev. A Ptah migration directory has no equivalent. Its importer refuses such a changeset, and leaves the decision with you:
error: parse liquibase source: liquibase changeset team:5 in "changelog.sql" is conditional on context, labels; a migration directory has no equivalent, so importing it would turn a conditional history into an unconditional one -- split the changelog or import it by handIt refuses runAlways and preconditions the same way. If a changelog depends on these features, moving it is a redesign, and Liquibase is the better fit.
Change types are a smaller case. A Liquibase <createTable> can target several databases. Ptah imports an XML changelog only for the one dialect you pass with --dialect, and writes SQL for it, so the result no longer targets the others.
Choose
Section titled “Choose”Keep Liquibase when a changelog relies on those features, when your database is outside our matrix, or when the team needs a tool with a longer production record. Ptah is pre-GA, so plan for command changes when you upgrade.
Add Ptah beside Liquibase when the changelog works and the gap is the live database. Liquibase stays in charge of history.
Move to Ptah when you want a schema file as the reviewed source, generated migrations, a stop on destructive statements, and a drift check in one tool. Our handoff guide covers importing a formatted SQL changelog and its limits.
Licensing can also affect the decision. Liquibase Community 5 moved to the Functional Source License, FSL-1.1-ALv2, and Ptah is MIT. If you are comparing other tools too, see our Flyway comparison.
Put Ptah to work
Example files
- drift/check.sql
- drift/column.sql
- drift/comment.sql
- drift/default.sql
- drift/function.sql
- drift/grant.sql
- drift/partial-index.sql
- drift/row-level-security.sql
- drift/trigger.sql
- liquibase/changelog-context.sql
- liquibase/changelog-drop-column.sql
- liquibase/changelog-logical-path.sql
- liquibase/changelog-no-rollback.sql
- liquibase/changelog-precondition.sql
- liquibase/changelog-run-always.sql
- liquibase/changelog.change.excerpt.sql
- liquibase/changelog.sql
- liquibase/changelog.xml
- measured/commands.json
- measured/drift-coverage.json
- measured/import-boundary.json
- measured/import-context.excerpt.txt
- measured/liquibase-add-driver.txt
- measured/liquibase-after-clear.txt
- measured/liquibase-checks.txt
- measured/liquibase-checksum-refusal.excerpt.txt
- measured/liquibase-checksum-refusal.txt
- measured/liquibase-clear-checksums.txt
- measured/liquibase-context-dev.txt
- measured/liquibase-context-prod-rows.txt
- measured/liquibase-context-prod.txt
- measured/liquibase-diff-changelog.txt
- measured/liquibase-diff-full.txt
- measured/liquibase-diff-json.txt
- measured/liquibase-diff-types.txt
- measured/liquibase-diff.excerpt.txt
- measured/liquibase-drop-column.txt
- measured/liquibase-generated.excerpt.sql
- measured/liquibase-generated.postgresql.sql
- measured/liquibase-history.txt
- measured/liquibase-image.txt
- measured/liquibase-moved-file.txt
- measured/liquibase-moved-logical-path.txt
- measured/liquibase-no-driver.txt
- measured/liquibase-no-rollback-test.txt
- measured/liquibase-rollback-sql.txt
- measured/liquibase-rows.txt
- measured/liquibase-server-default.txt
- measured/liquibase-status-after-drift.txt
- measured/liquibase-status-pending.txt
- measured/liquibase-targeted-rollback.txt
- measured/liquibase-testing-rollback.txt
- measured/liquibase-update-sql.excerpt.txt
- measured/liquibase-update-sql.txt
- measured/liquibase-update.txt
- measured/liquibase-v1.txt
- measured/liquibase-validate-after-drift.txt
- measured/liquibase-version.txt
- measured/postgres-image.txt
- measured/postgres-version.txt
- measured/ptah-after-down.txt
- measured/ptah-archive-sha256.txt
- measured/ptah-binary-sha256.txt
- measured/ptah-down-dry-run.txt
- measured/ptah-down-statement.sql
- measured/ptah-down.txt
- measured/ptah-drift-after-column.txt
- measured/ptah-drift-clean.txt
- measured/ptah-drift-declared-grant.txt
- measured/ptah-drift-default-layout-excluded.txt
- measured/ptah-drift-default-layout.txt
- measured/ptah-drop-column-allowed.txt
- measured/ptah-drop-column.excerpt.txt
- measured/ptah-drop-column.txt
- measured/ptah-dry-run.txt
- measured/ptah-edited-after-repair.txt
- measured/ptah-edited-history.txt
- measured/ptah-edited-repair.txt
- measured/ptah-edited-sum.excerpt.txt
- measured/ptah-edited-sum.txt
- measured/ptah-generate.txt
- measured/ptah-import-no-rollback-down.sql
- measured/ptah-import-xml-up.sql
- measured/ptah-import-xml.txt
- measured/ptah-import.txt
- measured/ptah-moved-dir.txt
- measured/ptah-norb-down.txt
- measured/ptah-norb-index.txt
- measured/ptah-norb-status.txt
- measured/ptah-norb-up-again.txt
- measured/ptah-rows.txt
- measured/ptah-server-default.txt
- measured/ptah-status.txt
- measured/ptah-up-statement.sql
- measured/ptah-up.txt
- measured/ptah-v1.txt
- measured/ptah-version.txt
- measured/published-checksum.txt
- ptah/migrations/0000000001_team_1.down.sql
- ptah/migrations/0000000001_team_1.up.sql
- ptah/migrations/0000000002_team_2.down.sql
- ptah/migrations/0000000002_team_2.up.sql
- ptah/migrations/1794067402_cancel_at_period_end.down.sql
- ptah/migrations/1794067402_cancel_at_period_end.up.sql
- ptah/migrations/ptah.sum
- ptah/source/changelog.sql
- README.md
- schema-with-grant.sql
- schema.sql
- seed.sql
- verified.json