All demos/ Flyway adoption

DEMO-02-FLYAUG 2026

Retrofit generates the migration scripts. Flyway runs them.

Retrofit imports a schema dump into its operation log, writes the migrations that add and then validate a foreign key between existing tables, then backs the change out and reapplies it. Every script is generated, none is written by hand.

Stack
Flyway + PostgreSQL
Change
AddForeignKey, NOT VALID
Reverse
generated + classified

Summary

Your production migrations run through Flyway, and you want them generated and checked, not written by hand.

Marquez is our starting point: a real schema built by 74 Flyway migrations. We take a schema-only dump, import it into Retrofit, and seal it as the V74 baseline. That yields 469 schema operations: 30 tables, 199 columns, 181 constraints, and 46 indexes. Nothing is replayed and nothing is rewritten (Fig. 1A).

jobs.current_run_uuid already points at runs.uuid, and is indexed, but no foreign key connects the two. We add one with ON DELETE SET NULL.

We create the foreign key with PostgreSQL’s NOT VALID option: the constraint checks new writes immediately and defers the full-table scan of existing rows to a later validation step (Fig. 1B). We check those rows first, proving every one points at a real run, so the second migration’s VALIDATE CONSTRAINT is guaranteed to pass when Flyway applies it.

Each generated migration leaves transaction framing to Flyway. The attestation says only that; every other diagnostic is still captured, and the demo fails unless the advisory stream is otherwise empty.

Then we back the change out and put it back. A migration that fails mid-run is PostgreSQL rollback, not an undo; this is a deliberate reversal after V75 and V76 succeeded. Flyway Community is append-only, so the generated reverses deploy as V77 and V78. We reapply the same forward bodies as V79 and V80 and rerun the test: deleting a run leaves the job in place with a null current_run_uuid (Fig. 1C). Flyway’s history keeps every step, V74 to V80.

Import Existing Schema: a stack of 74 Flyway migrations becomes one card of 469 Retrofit schema operations.
Fig. 1AOne schema-only dump imports as 469 schema operations; the 74-migration Flyway history is not replayed.
Stage Changes in Retrofit: the jobs table and runs table converge into a new foreign key box marked ON DELETE SET NULL, NOT VALID.
Fig. 1BThe foreign key is staged as NOT VALID: new writes are protected immediately, and the scan waits for validation.
Confirm with Application: deleting a run leaves the job in place; current_run_uuid becomes NULL; migration history untouched; new writes protected by PostgreSQL.
Fig. 1CThe application confirms the result: deleting a run leaves the job in place with a null current_run_uuid.

Chapter 01Adopt and deploy

Can Retrofit fit into the Flyway workflow we already have?

Import Marquez’s current PostgreSQL schema after 74 existing Flyway migrations, add a foreign key from jobs.current_run_uuid to runs.uuid, and hand transaction framing and deployment back to Flyway.

48 sec space to pause f for fullscreen
Read transcript

flyway-adopt-retrofit-demo: adopt existing project

Generated from the demo script. Every command below is run verbatim by the recording.

1/4 start with an existing Flyway project

Marquez already has 74 Flyway versions in production. Keep every applied migration:

psql "$DB" -X -Atq -c "SELECT 'Latest Flyway version: V' || version FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 1"

capture the schema as it exists now, without replaying its migration history:

pg_dump "$DB" --schema-only --no-owner --no-privileges --exclude-table=public.flyway_schema_history > .runtime/marquez-v74-baseline.sql

2/4 add Retrofit

import that dump once; the Seal establishes where Retrofit starts managing new changes:

retrofit -C .runtime import marquez-v74-baseline.sql --schema public --force --relaxed --seal='Flyway V74 baseline' --summary-file import-summary.json --skip-manifest import-skips.jsonl -q > .runtime/import.log

a mature schema becomes 469 schema operations, including 30 tables, 199 columns, 181 constraints, and 46 indexes; nothing skipped:

cat .runtime/import-summary.json

3/4 define the schema change

469 operations describe the schema you already have. The change you want is one: a foreign key from jobs.current_run_uuid to runs.uuid with ON DELETE SET NULL. NOT VALID protects new writes without scanning existing jobs rows in this deployment:

retrofit op add AddForeignKey jobs references=runs on_delete=set_null name=jobs_current_run_uuid_fkey not_valid=true --column=jobs.current_run_uuid --ref_column=runs.uuid -q
retrofit op seal --reason 'Flyway V75: protect jobs.current_run_uuid' -q

4/4 generate migration SQL for Flyway

envelope=caller hands transaction framing to Flyway; the flag next to it attests that we know Flyway opens it. Retrofit stays loud about everything it cannot check:

retrofit migrate --from seal~1 --to seal --envelope caller --allow-external-atomicity -q > .runtime/flyway/V75__add_jobs_current_run_fk.sql
retrofit migrate --from seal~1 --to seal --down --strict --envelope caller --allow-external-atomicity -q > .runtime/reverse/U75__add_jobs_current_run_fk.sql

ordinary reviewable SQL in both directions:

sed -n '/^ALTER TABLE/p' .runtime/flyway/V75__add_jobs_current_run_fk.sql .runtime/reverse/U75__add_jobs_current_run_fk.sql

Flyway owns the transaction and deploys V75 through the same pipeline:

docker compose -p '$COMPOSE_PROJECT' -f compose.yaml run --rm flyway migrate
psql "$DB" -X -Atq -c "SELECT 'Flyway applied: V' || version FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 1"

the database now clears the pointer when a current run is deleted. the job survives:

./consumer/after/test-current-run.sh "$DB"

Retrofit writes the migration. Flyway remains the deployer.

  1. Export the current Marquez schema without replaying its 74-version Flyway history.

  2. Model 30 tables, 199 columns, 181 constraints, and 46 indexes as schema operations, then seal the point where Retrofit takes over.

  3. Connect jobs.current_run_uuid to runs.uuid. PostgreSQL protects new writes immediately and clears the job's current-run value if that run is deleted.

  4. Generate reviewable forward and strict reverse SQL under Flyway-owned transaction framing. The external-atomicity attestation suppresses only that named fact; Flyway deploys V75 and the application test verifies the result.

Chapter 02Validate the existing rows

Can Retrofit model a production rollout, not just emit valid DDL?

Confirm every stored current-run value points to a row in runs, validate the foreign key in a second migration, and show its reverse is a no-op: there is nothing to un-prove.

43 sec space to pause f for fullscreen
Read transcript

flyway-adopt-retrofit-demo: staged validation

Generated from the demo script. Every command below is run verbatim by the recording.

1/3 V75 enforces the FK for new writes

Flyway deployed the foreign key without forcing an old-row validation scan:

psql "$DB" -X -Atq -c "SELECT 'Flyway V' || max(version) || '  FK active  validation=' || CASE WHEN bool_and(convalidated) THEN 'complete' ELSE 'pending' END FROM flyway_schema_history, pg_constraint WHERE conname='jobs_current_run_uuid_fkey'"

check that every jobs.current_run_uuid value matches an existing runs.uuid:

psql "$DB" -X -Atq -c "SELECT 'existing orphan rows: ' || count(*) FROM jobs j LEFT JOIN runs r ON r.uuid=j.current_run_uuid WHERE j.current_run_uuid IS NOT NULL AND r.uuid IS NULL"

2/3 validate existing rows as Flyway V76

validate jobs_current_run_uuid_fkey in an explicit, independently deployable cut:

retrofit op add ValidateConstraint jobs_current_run_uuid_fkey -q
retrofit op seal --reason 'Flyway V76: validate jobs.current_run_uuid' -q
retrofit migrate --from seal~1 --to seal --envelope caller --allow-external-atomicity -q > .runtime/flyway/V76__validate_jobs_current_run_fk.sql
retrofit migrate --from seal~1 --to seal --down --strict --envelope caller --allow-external-atomicity -q > .runtime/reverse/U76__validate_jobs_current_run_fk.sql

the forward proves the rows. there is nothing to un-prove, so the strict reverse is an explicit no-op:

sed -n '/^ALTER TABLE/p;/^-- reverse-noop/p' .runtime/flyway/V76__validate_jobs_current_run_fk.sql .runtime/reverse/U76__validate_jobs_current_run_fk.sql

3/3 Flyway V76 completes validation

docker compose -p '$COMPOSE_PROJECT' -f compose.yaml run --rm flyway migrate
psql "$DB" -X -Atq -c "SELECT 'Flyway V' || max(version) || '  constraint validated=' || bool_and(convalidated) FROM flyway_schema_history, pg_constraint WHERE conname='jobs_current_run_uuid_fkey'"

Retrofit models the rollout boundary. the deployment’s undo lands through U75’s DROP.

  1. Verify that every non-null value in jobs.current_run_uuid matches a value in runs.uuid before validation.

  2. Validate jobs_current_run_uuid_fkey in a separate migration so the table scan is explicit.

  3. The strict reverse is an explicit no-op. The rows are proven, and the deployment's undo lands through U75's DROP.

Chapter 03Reverse and reapply

Can the generated reverse be used in a release process?

With V75 and V76 applied, deploy their generated reverses as append-only migrations V77 and V78, then reapply the same two steps as V79 and V80.

52 sec space to pause f for fullscreen
Read transcript

flyway-adopt-retrofit-demo: community recovery

Generated from the demo script. Every command below is run verbatim by the recording.

1/3 deploy generated reverse SQL as V77 and V78

V75 and V76 succeeded. This is not failed-migration recovery; PostgreSQL handles that transactionally. Community Flyway is append-only, so render Retrofit’s reverse as the next versioned releases:

retrofit migrate --from seal~1 --to seal --down --strict --envelope caller --allow-external-atomicity -q > .runtime/flyway/V77__revert_validation.sql
retrofit migrate --from seal~2 --to seal~1 --down --strict --envelope caller --allow-external-atomicity -q > .runtime/flyway/V78__revert_jobs_current_run_fk.sql

V77 preserves proven validation; V78 drops jobs_current_run_uuid_fkey:

sed -n '/^ALTER TABLE/p;/^-- reverse-noop/p' .runtime/flyway/V77__revert_validation.sql .runtime/flyway/V78__revert_jobs_current_run_fk.sql
docker compose -p '$COMPOSE_PROJECT' -f compose.yaml run --rm flyway migrate
psql "$DB" -X -Atq -c "SELECT 'after V' || max(version) || ': foreign key count=' || count(c.oid) FROM flyway_schema_history h LEFT JOIN pg_constraint c ON c.conname='jobs_current_run_uuid_fkey'"

2/3 reapply the original changes as V79 and V80

render the original two forward cuts again. no copied or hand-edited SQL:

retrofit migrate --from seal~2 --to seal~1 --envelope caller --allow-external-atomicity -q > .runtime/flyway/V79__reapply_jobs_current_run_fk.sql
retrofit migrate --from seal~1 --to seal --envelope caller --allow-external-atomicity -q > .runtime/flyway/V80__revalidate_jobs_current_run_fk.sql
docker compose -p '$COMPOSE_PROJECT' -f compose.yaml run --rm flyway migrate

jobs_current_run_uuid_fkey is validated again. deleting a run still keeps the jobs row while clearing jobs.current_run_uuid:

./consumer/after/test-current-run.sh "$DB"

3/3 Flyway records the rollback and reapply

psql "$DB" -X -Atq -c "SELECT version || '  ' || description || '  success=' || success FROM flyway_schema_history WHERE version BETWEEN '75' AND '80' ORDER BY installed_rank"

Forward → reverse → forward. Retrofit derives every body; Flyway records every deployment.

  1. Deploy generated SQL that reverses validation and drops jobs_current_run_uuid_fkey as new versioned migrations.

  2. Recreate jobs_current_run_uuid_fkey as NOT VALID, then validate it in the following migration.

  3. Byte comparisons, the catalog, Flyway history, and the consumer test all close the loop.

Chapter 04Prove the entire lifecycle

The uninterrupted run includes authentic provenance, deterministic rerenders and byte checks, database assertions, append-only reversal, reapply, and the final application proof.

4 min 44 sec space to pause f for fullscreen
Read transcript

flyway-adopt-retrofit-demo: complete walkthrough

Generated from the demo script. Every command below is run verbatim by the recording.

a current application, one weak invariant

this is an existing Flyway project at V74, frozen by source commit and checksum:

sed -n '1,10p' inputs/MANIFEST.md

Retrofit is source-identifiable too:

retrofit --version

we will add a foreign key from jobs.current_run_uuid to runs.uuid; deleting a referenced runs row will clear that column without deleting the jobs row. the policy is illustrative, not an upstream proposal. start a disposable PostgreSQL 17 database:

docker compose -p '$COMPOSE_PROJECT' -f compose.yaml up -d --wait db

add Retrofit to the existing project

the checked-in config matches the dump’s explicit empty search_path:

cp inputs/retrofit.toml .runtime/retrofit.toml
retrofit schema create public .runtime/public.oplog --is-default -q

import the frozen V74 schema; save the 469-line operation report off-screen:

retrofit import inputs/marquez-v74.schema.sql --schema public --force --relaxed --seal='Marquez V74 baseline' --summary-file .runtime/import-summary.json --skip-manifest .runtime/import-skips.jsonl -q > .runtime/import.log

the mature schema becomes 469 schema operations: 30 tables, 199 columns, 181 constraints, 46 indexes, and supporting database objects:

cat .runtime/import-summary.json

the last operation is a named cut, not a SQL filename:

retrofit op show --include-sealed -q | tail -2

snapshot is a build artifact. render it twice and prove byte identity:

retrofit snapshot -q > .runtime/marquez-v74.snapshot.sql
retrofit snapshot -q > .runtime/verify/marquez-v74.snapshot.sql
cmp .runtime/marquez-v74.snapshot.sql .runtime/verify/marquez-v74.snapshot.sql

byte-identical. now restore that generated snapshot, not a hand-maintained migration stack:

psql "$DB" -X -v ON_ERROR_STOP=1 -q -f .runtime/marquez-v74.snapshot.sql

the authentic shape is here: column, index, and no foreign key:

psql "$DB" -X -v ON_ERROR_STOP=1 -P pager=off -f assertions/baseline-proof.sql

keep representative Marquez job/run rows through the whole rollout:

psql "$DB" -X -v ON_ERROR_STOP=1 -q -f assertions/setup-fixture.sql

the consumer exposes today’s failure: cleanup leaves a dangling pointer:

./consumer/before/test-current-run.sh "$DB"

Flyway still owns deployed history; mark this non-empty database as V74:

docker compose -p '$COMPOSE_PROJECT' -f compose.yaml run --rm flyway -url=jdbc:postgresql://db:5432/marquez -user=postgres -password=pw -locations=filesystem:/flyway/sql -baselineVersion=74 '-baselineDescription=Marquez V74 baseline' baseline

cut one protect new writes

add jobs.current_run_uuid -> runs.uuid with ON DELETE SET NULL and NOT VALID:

retrofit op add AddForeignKey jobs references=runs on_delete=set_null name=jobs_current_run_uuid_fkey not_valid=true --column=jobs.current_run_uuid --ref_column=runs.uuid -q
retrofit op seal --reason 'protect jobs.current_run_uuid' -q

NOT VALID protects new writes immediately while old rows are checked separately. Flyway owns the transaction, so render this cut with envelope=caller and attest it. the attestation answers one named fact; it silences no other diagnostic:

retrofit migrate --from seal~1 --to seal --envelope caller --allow-external-atomicity -q > .runtime/flyway/V75__add_jobs_current_run_fk.sql
retrofit migrate --from seal~1 --to seal --down --strict --envelope caller --allow-external-atomicity -q > .runtime/reverse/U75__add_jobs_current_run_fk.sql

the generated forward and strict reverse are ordinary reviewable SQL:

sed -n 1,12p .runtime/flyway/V75__add_jobs_current_run_fk.sql
sed -n 1,12p .runtime/reverse/U75__add_jobs_current_run_fk.sql

render both again before deployment. the bytes, not our confidence, must match:

retrofit migrate --from seal~1 --to seal --envelope caller --allow-external-atomicity -q > .runtime/verify/V75.sql
cmp .runtime/flyway/V75__add_jobs_current_run_fk.sql .runtime/verify/V75.sql
retrofit migrate --from seal~1 --to seal --down --strict --envelope caller --allow-external-atomicity -q > .runtime/verify/U75.sql
cmp .runtime/reverse/U75__add_jobs_current_run_fk.sql .runtime/verify/U75.sql

both V75 and U75 are byte-identical. Flyway deploys the generated file unchanged:

docker compose -p '$COMPOSE_PROJECT' -f compose.yaml run --rm flyway -url=jdbc:postgresql://db:5432/marquez -user=postgres -password=pw -locations=filesystem:/flyway/sql migrate

the catalog proves jobs_current_run_uuid_fkey exists and awaits old-row validation:

psql "$DB" -X -v ON_ERROR_STOP=1 -P pager=off -f assertions/catalog-unvalidated.sql

now the revised consumer test proves the payoff through the same cleanup path:

./consumer/after/test-current-run.sh "$DB"

cut two validate existing data

pressure-test the old rows explicitly before taking the validation lock:

psql "$DB" -X -v ON_ERROR_STOP=1 -P pager=off -f assertions/orphan-check.sql

zero orphans. promote that proof, then make it a second deployment cut:

retrofit op add ValidateConstraint jobs_current_run_uuid_fkey -q
retrofit op seal --reason 'validate jobs.current_run_uuid' -q
retrofit migrate --from seal~1 --to seal --envelope caller --allow-external-atomicity -q > .runtime/flyway/V76__validate_jobs_current_run_fk.sql
retrofit migrate --from seal~1 --to seal --down --strict --envelope caller --allow-external-atomicity -q > .runtime/reverse/U76__validate_jobs_current_run_fk.sql

forward proves the rows. there is nothing to un-prove, so the strict reverse is an explicit no-op:

sed -n 1,12p .runtime/flyway/V76__validate_jobs_current_run_fk.sql
sed -n 1,14p .runtime/reverse/U76__validate_jobs_current_run_fk.sql

V76 and U76 are deterministic too:

retrofit migrate --from seal~1 --to seal --envelope caller --allow-external-atomicity -q > .runtime/verify/V76.sql
cmp .runtime/flyway/V76__validate_jobs_current_run_fk.sql .runtime/verify/V76.sql
retrofit migrate --from seal~1 --to seal --down --strict --envelope caller --allow-external-atomicity -q > .runtime/verify/U76.sql
cmp .runtime/reverse/U76__validate_jobs_current_run_fk.sql .runtime/verify/U76.sql
docker compose -p '$COMPOSE_PROJECT' -f compose.yaml run --rm flyway -url=jdbc:postgresql://db:5432/marquez -user=postgres -password=pw -locations=filesystem:/flyway/sql migrate

the same constraint is now validated:

psql "$DB" -X -v ON_ERROR_STOP=1 -P pager=off -f assertions/catalog-validated.sql

change, deployment, behavior

Retrofit records the schema operations and the two deployment cuts:

retrofit op show --include-sealed -q | grep -E 'AddForeignKey|ValidateConstraint|Seal' | tail -5

Flyway records what reached the database:

psql "$DB" -X -Atc "SELECT version || '  ' || description FROM flyway_schema_history WHERE version IN ('75','76') ORDER BY installed_rank"

reverse, then reapply

Community Flyway is append-only, so render the V76 reverse directly as V77:

retrofit migrate --from seal~1 --to seal --down --strict --envelope caller --allow-external-atomicity -q > .runtime/flyway/V77__revert_validate_jobs_current_run_fk.sql
cmp .runtime/reverse/U76__validate_jobs_current_run_fk.sql .runtime/flyway/V77__revert_validate_jobs_current_run_fk.sql
docker compose -p '$COMPOSE_PROJECT' -f compose.yaml run --rm flyway -url=jdbc:postgresql://db:5432/marquez -user=postgres -password=pw -locations=filesystem:/flyway/sql migrate

the proof-preserving no-op leaves the stronger validated state in place:

psql "$DB" -X -v ON_ERROR_STOP=1 -P pager=off -f assertions/catalog-validated.sql

then render the V75 reverse directly as V78; this drops jobs_current_run_uuid_fkey:

retrofit migrate --from seal~2 --to seal~1 --down --strict --envelope caller --allow-external-atomicity -q > .runtime/flyway/V78__revert_add_jobs_current_run_fk.sql
cmp .runtime/reverse/U75__add_jobs_current_run_fk.sql .runtime/flyway/V78__revert_add_jobs_current_run_fk.sql
docker compose -p '$COMPOSE_PROJECT' -f compose.yaml run --rm flyway -url=jdbc:postgresql://db:5432/marquez -user=postgres -password=pw -locations=filesystem:/flyway/sql migrate
psql "$DB" -X -v ON_ERROR_STOP=1 -P pager=off -f assertions/catalog-absent.sql

render the same forward cuts again as V79 and V80. no copied or edited SQL:

retrofit migrate --from seal~2 --to seal~1 --envelope caller --allow-external-atomicity -q > .runtime/flyway/V79__reapply_jobs_current_run_fk.sql
retrofit migrate --from seal~1 --to seal --envelope caller --allow-external-atomicity -q > .runtime/flyway/V80__revalidate_jobs_current_run_fk.sql
cmp .runtime/flyway/V75__add_jobs_current_run_fk.sql .runtime/flyway/V79__reapply_jobs_current_run_fk.sql
cmp .runtime/flyway/V76__validate_jobs_current_run_fk.sql .runtime/flyway/V80__revalidate_jobs_current_run_fk.sql
docker compose -p '$COMPOSE_PROJECT' -f compose.yaml run --rm flyway -url=jdbc:postgresql://db:5432/marquez -user=postgres -password=pw -locations=filesystem:/flyway/sql migrate
psql "$DB" -X -v ON_ERROR_STOP=1 -P pager=off -f assertions/catalog-validated.sql

and the application proof still passes after the full operational cycle:

./consumer/after/test-current-run.sh "$DB"

six append-only Flyway rows; two Retrofit cuts; one behavior engineers care about:

psql "$DB" -X -Atc "SELECT version || '  ' || description || '  success=' || success FROM flyway_schema_history WHERE version BETWEEN '75' AND '80' ORDER BY installed_rank"

Retrofit writes the migrations. Flyway runs them. The application proves the result.

Retrofit has not shipped yet. No list, no noise, one message when it does.