From Liquibase to Ptah with a Verified Starting Point
Move a PostgreSQL subscription service from a Liquibase SQL changelog to Ptah, preserving its data and checking the imported rollback before the next change.
Ptah helps you plan, review, and apply database migrations. Try in your browser
A subscription service already has a Liquibase changelog, a deployed database, and customer rows. Moving it to Ptah requires a verified starting point: the imported history must describe the schema that is actually running. Its rollback SQL also deserves a test before anyone needs it.
We’ll import a formatted-SQL changelog, verify it in a shadow database, and record that history without replaying it against the existing tables. Then Ptah will generate the service’s next change: canceling a subscription at the end of its billing period.
This run used Liquibase Community 5.0.4, Ptah 0.8.1, and PostgreSQL 18.6. The supporting files contain the complete setup, original changelog, imported migrations, and captured results.
Import the SQL and its rollback
Section titled “Import the SQL and its rollback”Our original changelog creates public.subscriptions and an index on
account_id. Each changeset has explicit rollback SQL. Liquibase executed
both changesets, and the application inserted two subscriptions.
Liquibase’s history lives in the liquibase schema; application objects live
in public. This was the layout from the first deployment. It lets our
comparison cover application objects while retaining the original history.
Stop applying changes through Liquibase during the handoff. Preview the import, then create the native Ptah directory:
ptah migrations import --from liquibase \ --source-dir source --migrations-dir migrations --dry-runptah migrations import --from liquibase \ --source-dir source --migrations-dir migrationsPtah created two numbered up/down pairs and ptah.sum. Changesets simon:1
and simon:2 became versions 1 and 2. The second down file contains the
rollback supplied in the changelog:
DROP INDEX public.subscriptions_account_idx;The importer preserves the supplied SQL. It does not establish that running it is safe for the current application or data. The first changeset’s rollback drops the subscription table, which would also discard its rows.
Require the imported history to match
Section titled “Require the imported history to match”DB_URL points to the existing database. SHADOW_URL points to a separate,
disposable PostgreSQL database that Ptah can reset.
ptah migrations baseline --db-url "$DB_URL" \ --migrations-dir migrations --version 2 --schemas public \ --shadow-db "$SHADOW_URL"Ptah replayed the imported SQL in the shadow database and compared it with the
target. Only after that check passed did it record both versions as applied
in its own schema_migrations table.
The existing database kept its two subscription rows and both original Liquibase history entries. The baseline establishes Ptah’s starting point; it does not translate Liquibase’s execution timestamps or checksums into Ptah’s records.
Generate the first Ptah change
Section titled “Generate the first Ptah change”The desired schema in schema.sql adds
cancel_at_period_end boolean NOT NULL DEFAULT false. Existing subscriptions
should continue renewing until the application changes that value.
ptah migrations generate --db-url "$DB_URL" \ --schema-file schema.sql --schemas public \ --migrations-dir migrations --name cancel_at_period_end \ --shadow-db "$SHADOW_URL"The generated up statement is:
ALTER TABLE "public"."subscriptions" ADD COLUMN "cancel_at_period_end" boolean NOT NULL DEFAULT false;Generation also checked the new migration in the shadow database: apply, roll back, and apply again. After reviewing both files, update the directory checksum and lint the new version:
ptah migrations hash --dir migrationsptah migrations lint --dir migrations --dialect postgres --latest 1 --fail-on errorptah migrations up --db-url "$DB_URL" \ --migrations-dir migrations --verify-sumptah schema compare --db-url "$DB_URL" \ --schema-file schema.sql --schemas public --exit-codeLint reported no findings. The existing database applied one pending
migration and matched the desired schema. Updating subscription 1 set its
new flag to true; subscription 2 kept false. Both original rows and both
Liquibase history entries remained.
Exercise the imported rollback on a disposable database
Section titled “Exercise the imported rollback on a disposable database”Replay the complete directory on an empty database, then compare:
ptah migrations up --db-url "$FRESH_URL" \ --migrations-dir migrations --verify-sumptah schema compare --db-url "$FRESH_URL" \ --schema-file schema.sql --schemas public --exit-codeAll three versions applied. We inserted two test subscriptions there, then rolled back to version 1:
ptah migrations down --db-url "$FRESH_URL" \ --migrations-dir migrations --target 1 --verify-sum --confirmThat removed the new flag and executed the imported index rollback. The table still held two rows; the index and flag were absent. Reapplying the directory restored both objects, and another schema comparison passed.
This tests the rollback we imported for the index. Dropping the new flag still loses any values written to that column; a schema round trip does not prove a data-preserving rollback.
Check the boundary before converting a larger changelog
Section titled “Check the boundary before converting a larger changelog”For a larger history, inspect the import result before adopting anything. Contexts, preconditions, custom changes, and other execution behavior need their own assessment; this run establishes the formatted-SQL path shown here.
Commit the imported history, new migration, desired schema, and checksum together. Retire Liquibase execution for this database. The existing service and a fresh installation now reach the same schema through Ptah, and the tested index rollback is part of that history.
Put Ptah to work
Example files
- expected/index-rollback.sql
- expected/new-change.sql
- measured/application.txt
- measured/baseline.txt
- measured/commands.json
- measured/compare-fresh.txt
- measured/compare-live.txt
- measured/down-fresh.txt
- measured/driver-sha256.txt
- measured/generate.txt
- measured/hash.txt
- measured/import-command.json
- measured/import-dry-run.txt
- measured/import.txt
- measured/install-driver.txt
- measured/lint.txt
- measured/liquibase-image.json
- measured/liquibase-update-relative.txt
- measured/liquibase-version.txt
- measured/reapply-fresh.txt
- measured/recompare-fresh.txt
- measured/rollback-check.txt
- measured/seed-fresh.txt
- measured/seed.txt
- measured/source-history.txt
- measured/status-fresh.txt
- measured/status-live.txt
- measured/up-fresh.txt
- measured/up-live.txt
- migrations/0000000001_simon_1.down.sql
- migrations/0000000001_simon_1.up.sql
- migrations/0000000002_simon_2.down.sql
- migrations/0000000002_simon_2.up.sql
- migrations/1790349171_cancel_at_period_end.down.sql
- migrations/1790349171_cancel_at_period_end.up.sql
- migrations/ptah.sum
- README.md
- schema.sql
- seed.sql
- source/changelog.sql
- verified.json