Skip to content

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.

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:

Terminal window
ptah migrations import --from liquibase \
--source-dir source --migrations-dir migrations --dry-run
ptah migrations import --from liquibase \
--source-dir source --migrations-dir migrations

Ptah 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:

examples/expected/index-rollback.sql
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.

DB_URL points to the existing database. SHADOW_URL points to a separate, disposable PostgreSQL database that Ptah can reset.

Terminal window
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.

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.

Terminal window
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:

examples/expected/new-change.sql
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:

Terminal window
ptah migrations hash --dir migrations
ptah migrations lint --dir migrations --dialect postgres --latest 1 --fail-on error
ptah migrations up --db-url "$DB_URL" \
--migrations-dir migrations --verify-sum
ptah schema compare --db-url "$DB_URL" \
--schema-file schema.sql --schemas public --exit-code

Lint 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:

Terminal window
ptah migrations up --db-url "$FRESH_URL" \
--migrations-dir migrations --verify-sum
ptah schema compare --db-url "$FRESH_URL" \
--schema-file schema.sql --schemas public --exit-code

All three versions applied. We inserted two test subscriptions there, then rolled back to version 1:

Terminal window
ptah migrations down --db-url "$FRESH_URL" \
--migrations-dir migrations --target 1 --verify-sum --confirm

That 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