All demos/ Existing database

DEMO-01-DBJUL 2026

Import and evolve an existing PostgreSQL database.

Retrofit imports a populated database, preserves its rows through column and table refactors, and pulls changes made directly in PostgreSQL back into the log. Retrofit starts from a database dump, a set of migration scripts, or zero; this demo starts from the dump.

Stack
PostgreSQL
Change
op add, drift
Reverse
DecomposeTable, Seal, op log

Summary

You have a populated database, with or without a migration history.

Retrofit starts from a database dump, a set of migration scripts, or nothing at all. This demo starts with the Chinook sample database, its 11 related tables, and populated sample data. It imports that database, rebuilds all 3,503 tracks, renames track.name to track.title, converts track.milliseconds from an integer to an interval, and renames invoice.total to invoice.amount.

The larger refactor moves five billing-address columns out of invoice and creates invoice_core and invoice_billing.

Both tables retain invoice_id, all 412 invoices rejoin row for row, and all 2,240 rows in invoice_line continue to point at the invoice core. Each change is safe by construction: declared as a schema operation, with its risks reported, before Retrofit generates any SQL.

The warnings are part of that proof, not noise removed for the recording. Retrofit reports the rename and cast risks while those changes are still being authored, then reports the foreign-key validation lock when the generated split is applied. The chapter verifies that exactly those three advisory codes appear and nothing else.

Finally, the demo adds track.download_count and the track_composer_idx index directly to PostgreSQL. Retrofit compares a catalog dump with the operation log and records both changes, bringing the database and its managed history back into agreement.

Chapter 01Import an existing database

Can Retrofit adopt a database I already have, with its data?

Import Chinook’s 11 related tables as 138 schema operations, preserve its rows in a plain-text sidecar, and rebuild 3,503 tracks and 412 invoices from reviewable inputs.

32 sec space to pause f for fullscreen
Read transcript

import-drift-demo: import existing database

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

1/3 start with a database you already have

Chinook: 11 related tables and real sample data. No migration history to replay:

grep -c 'CREATE TABLE' chinook.sql | sed 's/^/  tables: /'
grep -c 'INSERT INTO' chinook.sql | sed 's/^/  insert statements: /'

2/3 import it as schema operations

one import turns the schema into operators and sets the existing rows aside:

retrofit import chinook.sql --relaxed --seal --purpose=seed -q > import.log

138 schema operations; the rows from 24 INSERT statements are preserved:

grep -E 'imported [0-9]+ ops|dml-preserved' import.log

the operation log is plain text: one operation per line, reviewable in a diff:

head -5 chinook.oplog

and the data rides alongside it in a sidecar, not buried in the schema:

ls chinook.oplog 0001-chinook.dml.sql

3/3 rebuild the database from the log

materialize the framed seed records as executable SQL:

awk '/^-- retrofit:dml-end/{print ";"} {print}' 0001-chinook.dml.sql > chinook.seed.sql && mv 0001-chinook.dml.sql chinook.seed.records

snapshot emits the schema; the preserved rows follow in the same pipe:

{ retrofit snapshot -q; echo 'SET search_path TO chinook;'; cat chinook.seed.sql; } | psql "$DB" -v ON_ERROR_STOP=1 -q

3,503 tracks and 412 invoices, live. schema and data both rebuilt from the log:

psql "$DB" -tAc 'SELECT count(*) FROM chinook.track' | sed 's/^/  tracks:   /'
psql "$DB" -tAc 'SELECT count(*) FROM chinook.invoice' | sed 's/^/  invoices: /'

There is no migrations/ directory. The oplog is the source and psql is the applier.

  1. Start with the upstream Chinook SQL: 11 related tables, 24 data inserts, and no migration history to replay.

  2. Turn the schema into 138 schema operations. The operation log remains plain text, and the existing rows move into a reviewable seed sidecar.

  3. Pipe the generated snapshot and preserved seed data into PostgreSQL, rebuilding all 3,503 tracks and 412 invoices without a migrations directory.

Chapter 02Refactor a live table

What does Retrofit tell me before I change a populated table?

Rename a column in use, convert 3,503 stored durations, and split 412 populated invoices. Retrofit reports exactly three expected advisories: two while authoring and one when the generated SQL pays the lock cost.

1 min 27 sec space to pause f for fullscreen
Read transcript

import-drift-demo: refactor a live table

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

1/3 rename a column that has readers

rename track.name to track.title. PostgreSQL renames in place, so the 3,503 existing values stay attached, and Retrofit flags who else may care:

retrofit op add RenameColumn track.name title -q

the warning arrives now, while the change is still just a declaration. Retrofit cannot see your application, so it names the risk instead of deciding for you. Apply it:

retrofit migrate -q 2>/dev/null | psql "$DB" -v ON_ERROR_STOP=1 -q
psql "$DB" -tAc 'SELECT title FROM chinook.track ORDER BY track_id LIMIT 1' | sed 's/^/  first title: /'
retrofit op seal --reason 'rename track.name -> title' -q

2/3 change a column’s type with the data still in it

convert track.milliseconds from integer to interval. the USING transform is caller-asserted, so Retrofit warns that rows failing the cast fail the migration:

retrofit op add CastColumn track.milliseconds interval 'using=milliseconds * interval '\''1 ms'\''' -q
retrofit op add RenameColumn invoice.total amount -q

one migrate covers both changes:

retrofit migrate -q 2>/dev/null | psql "$DB" -v ON_ERROR_STOP=1 -q
psql "$DB" -tAc 'SELECT milliseconds FROM chinook.track ORDER BY track_id LIMIT 1' | sed 's/^/  a real interval now: /'
retrofit op seal --reason 'milliseconds -> interval; invoice.total -> amount' -q

3/3 split a populated table in one operation

move five billing-address columns out of invoice. one operator creates both tables, moves all 412 rows, and re-points invoice_line’s foreign key. Retrofit names the lock that re-pointing will take:

retrofit op add DecomposeTable chinook invoice --into invoice_billing=billing_address,billing_city,billing_state,billing_country,billing_postal_code --rest invoice_core -q

one intent, one op. the whole unsealed window is that single record:

retrofit op show -q | sed 's/^/  /'

the key that makes the split rejoinable rides on the op itself: rejoin-col-ids, rejoin-pk-ids, rejoin-fk-ids. it is not spilled into separate AddColumn / AddPrimaryKey / AddForeignKey ops you could edit away and silently break. applying it re-points the foreign key, and validating that key takes a brief lock. Retrofit reports it here, at apply time, where the cost is actually paid:

retrofit migrate -q | psql "$DB" -v ON_ERROR_STOP=1 -q

nothing was dropped in the move, and the two halves still rejoin row for row:

psql "$DB" -tAc 'SELECT count(*) FROM chinook.invoice_core' | sed 's/^/  invoice_core:       /'
psql "$DB" -tAc 'SELECT count(*) FROM chinook.invoice_billing' | sed 's/^/  invoice_billing:    /'
psql "$DB" -tAc 'SELECT count(*) FROM chinook.invoice_core c JOIN chinook.invoice_billing b USING (invoice_id)' | sed 's/^/  rejoined:           /'
psql "$DB" -tAc 'SELECT count(*) FROM chinook.invoice_line il JOIN chinook.invoice_core ic ON ic.invoice_id = il.invoice_id' | sed 's/^/  lines still joined: /'
retrofit op seal --reason 'split invoice -> invoice_core + invoice_billing' -q

Three refactorings on a populated table, 3,503 tracks and 412 invoices intact. Retrofit named each risk at the moment it mattered: two while writing, one while applying.

  1. Rename the existing column in place so all 3,503 titles stay attached. Retrofit flags the readers it cannot see while the change is still a declaration.

  2. Apply an explicit `USING` transform to every stored duration. Retrofit warns that a row which cannot be cast will fail the migration.

  3. Move five billing fields into `invoice_billing`, retain the rest in `invoice_core`, and redirect `invoice_line`. All 412 invoices rejoin row for row and all 2,240 lines stay connected.

Chapter 03Reconcile a direct database change

Someone patched production directly. Now what?

Add a column and index directly to PostgreSQL, detect both from an ordinary schema-only dump, and bring the database and operation log back into agreement.

36 sec space to pause f for fullscreen
Read transcript

import-drift-demo: reconcile drift

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

1/3 someone changed the database directly

not every change arrives through the tool. a hotfix adds a column and an index straight to PostgreSQL, with no operation recorded anywhere:

psql \"\$DB\" -v ON_ERROR_STOP=1 <<'SQL

2/3 find the difference

take an ordinary schema-only dump. no special access, no agent:

pg_dump --schema-only --schema=chinook "$DB" > db.dump.sql

compare that dump against the log, and report before recording anything:

retrofit drift db.dump.sql --dry-run -q

3/3 adopt the change as managed history

record them as real operations and seal the reconciled state:

retrofit drift db.dump.sql --seal='drifted in track.download_count + composer index' -q

they are in the log now, as if they had been typed there:

retrofit snapshot -q | grep -E 'download_count|track_composer_idx' | sed 's/^/  /'

and the managed history reads as a list of reviewed cuts:

retrofit op show --include-sealed -q | grep -oE 'reason="[^"]*"' | awk -F'"' '{print "  v"NR"  "$2}'

The out-of-band change is no longer drift. It is version five.

  1. Simulate a production hotfix by adding `track.download_count` and `track_composer_idx` directly in PostgreSQL, with no operation recorded.

  2. Compare an ordinary `pg_dump` with the operation log and identify both changes by type and name before recording anything.

  3. Record both changes as schema operations, seal the reconciled state, and verify that a second comparison finds no residual drift.

Chapter 04Prove the complete walkthrough

The uninterrupted run exercises all six acts end to end: import, rebuild, rename, cast, table decomposition, and drift reconciliation, with the final row and history proofs intact.

2 min 51 sec space to pause f for fullscreen
Read transcript

import-drift-demo: complete walkthrough

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

docker run -d --name '$DB_NAME' -e POSTGRES_PASSWORD=pw -e POSTGRES_DB=chinook -p '$DB_PORT:5432' postgres:17

what we’ll do

start with Chinook: 11 related tables plus populated sample data. import it, change real columns and tables, then capture a direct database patch.

retrofit --version
ls -la inputs/chinook.sql

import the schema

retrofit init -q

name the schema first; it becomes the default, so the rest just work:

retrofit schema create chinook -q

import the 11 tables as schema operations and preserve the existing data:

retrofit import inputs/chinook.sql --relaxed --seal --purpose=seed -q > import.log

the first operations:

head -6 import.log

138 schema operations; the rows from 24 INSERT statements are preserved:

grep -E 'imported [0-9]+ ops|dml-preserved' import.log

the seed records stay in a plain-text sidecar:

ls chinook.oplog 0001-chinook.dml.sql
grep -m1 'retrofit:dml seq=1' 0001-chinook.dml.sql

materialize those framed records as executable seed SQL for this demo:

awk '/^-- retrofit:dml-end/{print ";"} {print}' 0001-chinook.dml.sql > chinook.seed.sql && mv 0001-chinook.dml.sql chinook.seed.records

the operation log is plain text, one operation per line:

head -5 chinook.oplog
retrofit op show --include-sealed -q | tail -3

build the database

snapshot emits the schema; append the preserved seed SQL in the same pipe:

{ retrofit snapshot -q; echo 'SET search_path TO chinook;'; cat chinook.seed.sql; } | psql "$DB" -v ON_ERROR_STOP=1 -q

3,503 tracks, live:

psql "$DB" -tAc 'SELECT count(*) FROM chinook.track'

rename a column

rename track.name to track.title without dropping and recreating the column:

retrofit op add RenameColumn track.name title -q

‘migrate’ writes the SQL since the last seal; pipe it at psql:

retrofit migrate -q | psql "$DB" -v ON_ERROR_STOP=1 -q

all 3,503 titles survived:

psql "$DB" -tAc 'SELECT track_id, title FROM chinook.track ORDER BY track_id LIMIT 1'

seal it. –reason names the version:

retrofit op seal --reason 'rename track.name -> title' -q

batch a few changes

convert track.milliseconds from integer to interval and rename invoice.total to amount:

retrofit op add CastColumn track.milliseconds interval 'using=milliseconds * interval '\''1 ms'\''' -q
retrofit op add RenameColumn invoice.total amount -q

one migrate covers both. apply it the same way:

retrofit migrate -q | psql "$DB" -v ON_ERROR_STOP=1 -q

a real interval now, all 3,503 converted:

psql "$DB" -tAc 'SELECT track_id, milliseconds FROM chinook.track ORDER BY track_id LIMIT 1'
retrofit op seal --reason 'milliseconds -> interval; invoice.total -> amount' -q

split a table

move five billing-address columns out of invoice. one operation creates invoice_core and invoice_billing, moves the rows, and re-points the foreign key:

retrofit op add DecomposeTable chinook invoice --into invoice_billing=billing_address,billing_city,billing_state,billing_country,billing_postal_code --rest invoice_core -q

one intent, one op. the whole unsealed window is that single record:

retrofit op show -q | sed 's/^/  /'

the key that makes the split rejoinable rides on the op itself: rejoin-col-ids, rejoin-pk-ids, rejoin-fk-ids. it is not spilled into separate AddColumn / AddPrimaryKey / AddForeignKey ops that you could edit away and silently break the rejoin guarantee this operator rests on. apply the generated split to all 412 invoices. Retrofit still reports the brief lock PostgreSQL needs to validate the new foreign key:

retrofit migrate -q | psql "$DB" -v ON_ERROR_STOP=1 -q

both children carry all 412 rows. nothing dropped in the move:

psql "$DB" -tAc 'SELECT count(*) FROM chinook.invoice_core' | sed 's/^/  invoice_core:    /'
psql "$DB" -tAc 'SELECT count(*) FROM chinook.invoice_billing' | sed 's/^/  invoice_billing: /'

both tables retain invoice_id, so core and billing rejoin row-for-row:

psql "$DB" -tAc 'SELECT count(*) FROM chinook.invoice_core c JOIN chinook.invoice_billing b USING (invoice_id)' | sed 's/^/  rejoined: /'

invoice_line.invoice_id now references invoice_core.invoice_id; every line still joins:

psql "$DB" -tAc 'SELECT count(*) FROM chinook.invoice_line il JOIN chinook.invoice_core ic ON ic.invoice_id = il.invoice_id' | sed 's/^/  lines joined: /'
retrofit op seal --reason 'split invoice -> invoice_core + invoice_billing' -q

capture a direct database change

simulate a direct patch: add track.download_count and index track.composer:

psql \"\$DB\" -v ON_ERROR_STOP=1 <<'SQL

dump the current shape:

pg_dump --schema-only --schema=chinook "$DB" > db.dump.sql

compare that dump with the operation log before recording either change:

retrofit drift db.dump.sql --dry-run -q

Retrofit identifies the new column and index. record both as managed changes and seal the new state:

retrofit drift db.dump.sql --seal='drifted in track.download_count + composer index' -q

in the oplog now, like we’d typed them:

retrofit snapshot -q | grep -E 'download_count|track_composer_idx'

the managed history

the text operation log records each reviewed cut:

retrofit op show --include-sealed -q | grep -oE 'reason="[^"]*"' | awk -F'"' '{print "  v"NR"  "$2}'

the operation log and seed data diff, branch, and merge in Git alongside the code.

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