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
Chapters
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.
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.


NOT VALID: new writes are protected immediately, and the scan waits for validation.
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.
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.
Export the current Marquez schema without replaying its 74-version Flyway history.
Model 30 tables, 199 columns, 181 constraints, and 46 indexes as schema operations, then seal the point where Retrofit takes over.
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.
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.
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.
Verify that every non-null value in jobs.current_run_uuid matches a value in runs.uuid before validation.
Validate jobs_current_run_uuid_fkey in a separate migration so the table scan is explicit.
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.
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.
Deploy generated SQL that reverses validation and drops jobs_current_run_uuid_fkey as new versioned migrations.
Recreate jobs_current_run_uuid_fkey as NOT VALID, then validate it in the following migration.
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.
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.