All demos/ Existing database

DEMO-01-DBJUL 2026

Import and evolve an existing PostgreSQL database.

From a dump of a populated database, retrofit imports the schema, preserves its rows through column and table refactors, and pulls changes made directly in PostgreSQL back into the log. The starting point can be a database dump, a set of migration scripts, or an empty log; 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.

We start from the Chinook sample database: 11 related tables and their sample data. retrofit import reads a dump of it into the operation log, rows included, and snapshot rebuilds all 3,503 tracks from that log. Then we refactor live tables: rename track.name to title, convert track.milliseconds to an interval, rename invoice.total to amount, and split invoice into invoice_core and invoice_billing.

Each change is declared as a schema operation, with its risks reported, before retrofit renders any SQL. All 412 invoices rejoin row for row across the split, and all 2,240 invoice_line rows still point at the invoice core.

The warnings are part of that proof, not noise removed for the recording: the rename and cast risks while those changes are authored, and the foreign-key validation lock when the split is applied. The chapter asserts exactly those codes and nothing else.

Finally, we add track.download_count and an index directly in PostgreSQL. drift compares a dump with the operation log and records both, so the database and its history agree again.

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 the data corpus as one plain-text file per table, and rebuild 3,503 tracks and 412 invoices from reviewable inputs.
33 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 reads the DDL and writes it to the log as operations; the rows are set aside as data:

retrofit import chinook.sql --allow-capability-gaps --seal --label 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 rows ride alongside it in the data corpus, one file per table:

ls chinook.oplog data/seed/

3/3 rebuild the database from the log

snapshot emits the schema and the named dataset in one pipe:

retrofit snapshot --label seed -q | 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. import reads the DDL in the file and writes it to the log as operations: 138 lines of plain text. The rows from 24 INSERTs are written to the data corpus, one file per table, labelled seed.

  3. snapshot renders the schema at the head of the log as SQL, and the seed dataset with it. psql applies it: 3,503 tracks and 412 invoices, with no 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. Three risk advisories, two while authoring and one when the generated SQL pays the lock cost, and one notice from migrate for each of the 24 preserved INSERTs it leaves behind its window.
1 min 41 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. op add records it, and migrate renders its SQL both ways:

retrofit op add RenameColumn track.name title -q
retrofit migrate -qq | grep '^ALTER'
retrofit migrate --down -qq | grep '^ALTER'

the warning came with op add, while the change was one line in the log. Retrofit cannot see your application, so it names the risk instead of deciding for you. Apply it:

retrofit migrate -qq | 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 -qq | 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. everything since the last seal 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. op add appends one operation to the log; advisories arrive here, before any SQL exists. PostgreSQL renames in place, so all 3,503 values stay attached. The column’s readers are outside what retrofit can see, and that is what the warning names.

  2. We write the USING transform and retrofit cannot check it, so it warns that a row the cast rejects fails the migration. One migrate covers this and the invoice.total rename.

  3. DecomposeTable moves five billing columns into invoice_billing, keeps the rest in invoice_core, and re-points invoice_line. The lock the new key’s validation takes is reported at migrate. All 412 invoices rejoin row for row and 2,240 lines stay joined.

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

the database and the operation log now disagree.

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

both changes identified, by type and by name: a column and an index.

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 log reads as a list of seals, each with its reason:

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. A hotfix adds track.download_count and track_composer_idx straight to PostgreSQL, with psql. No operation records it.

  2. drift compares a schema-only dump with the log and appends what the log is missing; --dry-run lists it instead. Both changes, by type and by name, from an ordinary pg_dump.

  3. Without the flag, drift appends both operations and --seal seals them. snapshot now carries the column and the index, and the log reads as five seals, v1 to v5.

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.
3 min 02 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

bring up a disposable PostgreSQL 17 database:

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 --allow-capability-gaps --seal --label 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 rows land in the data corpus, one plain-text file per table:

ls data/seed/

each file frames its rows as records:

grep -m1 'retrofit:dml seq=' data/seed/chinook.track.*.data.sql

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 and the named dataset in one pipe:

retrofit snapshot --label seed -q | 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. everything since the last seal 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 operation log is text, and each seal carries its reason:

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.

Not shipped yet. No list, no noise, one message when it does.