From Flyway to Ptah Without Replaying Production
Import a Flyway history, verify it against an existing PostgreSQL database, and let Ptah apply the next change while preserving the application's data.
An existing catalog database already has its tables, indexes, and product rows. Moving its migrations from Flyway to Ptah should preserve that state. Running the imported history against it would try to create objects that already exist.
Ptah’s baseline command can record the imported migrations as applied. Before doing that, we’ll have it replay the files in a disposable database and compare the result with the existing schema. Then we’ll use Ptah to generate and apply the next catalog change.
This run used Flyway OSS 13.7.0, Ptah 0.8.0, and PostgreSQL 18.6. The example files include the original migrations, imported files, image identity, and captured results.
Start with the history the database actually ran
Section titled “Start with the history the database actually ran”Flyway applied these files to our test database:
| File | Result |
|---|---|
V1__products.sql |
Product table with SKU, stock, and archive status |
V2__available_stock.sql |
Partial index for available stock |
R__available_products.sql |
View selecting products with positive stock that are not archived |
We then inserted four application rows. KIT-42 and CABLE-7 appeared in the
availability view; another product had no stock, and one was archived.
Flyway’s history table lives in the flyway schema in this example. Application
objects live in public. That separation lets us compare the application
schema while keeping the original history for reference. The full setup and
Flyway commands are in the supporting files.
Freeze changes through Flyway for the handoff. Import the exact files that produced the database state you intend to adopt:
ptah migrations import --from flyway \ --source-dir source --migrations-dir migrations --dry-runptah migrations import --from flyway \ --source-dir source --migrations-dir migrationsThe import created up/down pairs and ptah.sum. Its up files were:
0000000001_products.up.sql0000000002_available_stock.up.sql0000000003_repeatable_available_products.up.sqlVerify the baseline before recording it
Section titled “Verify the baseline before recording it”DB_URL names the existing application database. SHADOW_URL must name a
different, disposable database: Ptah resets it while checking the history.
Preview the metadata rows, then perform the verified baseline:
ptah migrations baseline --db-url "$DB_URL" \ --migrations-dir migrations --version 3 --schemas public \ --shadow-db "$SHADOW_URL" --dry-runptah migrations baseline --db-url "$DB_URL" \ --migrations-dir migrations --version 3 --schemas public \ --shadow-db "$SHADOW_URL"The dry run shows the proposed rows. The second command replays versions 1–3 in the shadow database and checks their schema against the target before recording them. Its result was:
Baselined 3 migration(s) through version 3 in "schema_migrations"The migration messages during this check describe work in the shadow database. The application database kept all four product rows and its original Flyway history. Ptah created its own revision records; it did not convert Flyway’s installation timestamps or checksums into Ptah history.
Check that nothing is pending:
ptah migrations status --db-url "$DB_URL" \ --migrations-dir migrations --verify-sumThe current version was 3, with three applied migrations and zero pending.
Check that a mismatched history is refused
Section titled “Check that a mismatched history is refused”We also ran a separate Flyway database to test the failure case. Its repeatable
view initially selected stock greater than zero. Changing the same R__ file
to require at least five units caused Flyway to run that file again. The view
then returned only KIT-42.
Trying to baseline that database against the earlier imported snapshot failed
with baseline shadow check failed: view mismatch available_products.
Ptah returned exit code 2, and its revision table was still absent afterward.
This is why a baseline needs a schema check. The source can be readable and the import successful while the imported history describes a different state.
Let Ptah generate the next change
Section titled “Let Ptah generate the next change”The catalog now needs a configurable reorder threshold. The new desired schema
adds reorder_level, defaulting to five, and makes the availability view use
that value.
We first normalized the desired SQL through PostgreSQL:
ptah schema inspect --schema-file schema-v2.sql \ --dev-url "$SHADOW_URL" --schemas public --format hcl > schema-v2.hclptah migrations generate --db-url "$DB_URL" \ --schema-file schema-v2.hcl --schemas public \ --migrations-dir migrations --name reorder_threshold \ --shadow-db "$SHADOW_URL"This normalization mattered in the tested release: passing the original SQL
directly made the shadow check report a view-body mismatch after PostgreSQL
reformatted it. schema inspect gives Ptah the database’s stored form. Both
the failed direct-SQL check and the successful normalized run are recorded.
The generated up migration contains:
ALTER TABLE "public"."products" ADD COLUMN "reorder_level" integer NOT NULL DEFAULT 5;CREATE OR REPLACE VIEW "public"."available_products" ASSELECT id, sku, stock FROM products WHERE stock >= reorder_level AND NOT archived;The shadow check replayed the imported history and new migration, rolled back the new migration, and reapplied it. That validates this new migration’s round trip; it does not supply the missing rollback SQL for the older files.
Apply the reviewed files and compare the result
Section titled “Apply the reviewed files and compare the result”After reviewing the generated pair, update the directory checksum and lint the new version:
ptah migrations hash --dir migrationsptah migrations lint --dir migrations --dialect postgres --latest 1ptah migrations up --db-url "$DB_URL" \ --migrations-dir migrations --verify-sumptah schema compare --db-url "$DB_URL" \ --schema-file schema-v2.hcl --schemas public --exit-codeThe lint run reported no findings. Ptah applied one pending migration and
reported no schema differences. All four product rows remained, each with
reorder_level = 5; only KIT-42 appeared in the updated availability view.
Finally, we replayed the complete directory on an empty database:
ptah migrations up --db-url "$FRESH_URL" \ --migrations-dir migrations --verify-sumptah schema compare --db-url "$FRESH_URL" \ --schema-file schema-v2.hcl --schemas public --exit-codeThat database applied all four migrations and reached the same schema. The existing database applied only the new one. Both had no pending migrations.
Keep the verified imported history and its checksum in Git, retire Flyway execution for this database, and make subsequent changes through new Ptah migrations. The handoff is complete when existing and fresh databases agree on the schema, with the existing application’s data preserved.
Example files
- application-query.sql
- expected/baseline.txt
- expected/imported.txt
- expected/new-change.sql
- measured/application-query.txt
- measured/apply-new.txt
- measured/baseline-dry-run.txt
- measured/baseline-mismatch-metadata.txt
- measured/baseline-mismatch.txt
- measured/baseline.txt
- measured/commands.json
- measured/compare-fresh.txt
- measured/compare-live.txt
- measured/data-after-baseline.txt
- measured/flyway-image.json
- measured/flyway-info.txt
- measured/flyway-migrate.txt
- measured/flyway-version.txt
- measured/generate-normalized.txt
- measured/generate.txt
- measured/hash-directory.txt
- measured/import-dry-run.txt
- measured/import.txt
- measured/lint-new-after-hash.txt
- measured/lint-new.txt
- measured/normalize-desired.txt
- measured/repeatable-after.txt
- measured/repeatable-before.txt
- measured/repeatable-changed.txt
- measured/repeatable-initial.txt
- measured/repeatable-seed.txt
- measured/replay-fresh.txt
- measured/seed-psql.txt
- measured/status-after-baseline.txt
- measured/status-fresh.txt
- measured/status-live.txt
- migrations/0000000001_products.down.sql
- migrations/0000000001_products.up.sql
- migrations/0000000002_available_stock.down.sql
- migrations/0000000002_available_stock.up.sql
- migrations/0000000003_repeatable_available_products.down.sql
- migrations/0000000003_repeatable_available_products.up.sql
- migrations/1790263096_reorder_threshold.down.sql
- migrations/1790263096_reorder_threshold.up.sql
- migrations/ptah.sum
- README.md
- schema-v2.hcl
- schema-v2.sql
- seed.sql
- source-changed/R__available_products.sql
- source-changed/V1__products.sql
- source-changed/V2__available_stock.sql
- source/R__available_products.sql
- source/V1__products.sql
- source/V2__available_stock.sql
- verified.json
- verify-history.sql