Course 5 · Module 2 — Migrating from Liquibase
Liquibase gave you structure. Changesets with IDs and authors, a master changelog that pulls it all together, contexts so dev seed data never hit prod. But it also gave you two tables you never asked for — DATABASECHANGELOG to remember every changeset, and DATABASECHANGELOGLOCK to stop two runs from colliding. And to know what your schema actually is, you still read XML from the top down.
Let’s move that database to SchemaSmith — and leave both ledgers behind.
Liquibase, concept for concept
Section titled “Liquibase, concept for concept”| Liquibase | SchemaSmith |
|---|---|
changeSet blocks in a changelog | Declarative table files — the end state, not the changesets that built it |
DATABASECHANGELOG (ran-changeset ledger) | No ledger at all — your table definitions are the state |
DATABASECHANGELOGLOCK (run lock) | Gone too — there’s no changelog to guard |
liquibase generate-changelog (reverse-engineer a live DB into XML) | SchemaTongs extract — cast the live database straight to files |
Contexts / labels (context="dev") | ShouldApplyExpression — gate an object on the environment, declared in the package |
<preConditions> | A ValidationScript on the product — checked before quench runs |
<rollback> blocks you hand-write | Declarative convergence — change the declared state, quench closes the gap. No rollback to author |
The first three rows are the whole story. Liquibase’s model is two bookkeeping tables: one that remembers what ran, one that locks the door while it runs. SchemaSmith has neither, because it never asks “what ran?” — it asks “what shape did you declare, and what shape is live?” and makes them match.
Extract, and leave both ledgers behind
Section titled “Extract, and leave both ledgers behind”Open the lab’s before/ folder and you’ll find a real Liquibase project — db.changelog-master.xml pulling in two changesets that build the shop. You don’t run it. The setup already applied its end state to shop_from_liquibase, exactly the way liquibase update would have — DATABASECHANGELOG, DATABASECHANGELOGLOCK, and all.
Point SchemaTongs at that database. The settings file names the four tables you’re keeping:
"ShouldCast": { "ObjectList": "dbo.Customer,dbo.Product,dbo.SalesOrder,dbo.OrderItem" }cd sqlserverschematongs --ConfigFile:SchemaTongs.settings.json=== Casting Summary === Tables: 4 extracted, 0 errorsFour tables — Customer, Product, SalesOrder, OrderItem. Both Liquibase tables? Not in the package. You named the four you wanted; the changelog and its lock weren’t on the list, so neither came along. One whitelist, two ledgers left behind. You don’t delete them — you just don’t invite them.
Now quench the package back to prove it captured the state:
schemaquench --ConfigFile:quench.settings.jsonThe first run adopts your tables — SchemaSmith stamps them as managed and stands up its own bookkeeping in a separate SchemaSmith schema, well clear of your dbo tables. Run it again and nothing happens. No adds, no alters, no drops. A clean no-op is the proof: the package is a faithful cast of the live database, and you’re declarative now.
DATABASECHANGELOG and DATABASECHANGELOGLOCK are still sitting there, untouched. SchemaSmith left them exactly where they were — two inert tables you can drop whenever you feel like it. You traded two ledgers you maintained for one you’ll never think about again.
What about contexts and rollbacks?
Section titled “What about contexts and rollbacks?”The two Liquibase features people ask about most:
Contexts and labels — context="dev" on a changeset so it only runs in some environments (that demo-customer seed in the lab’s before/ is one). SchemaSmith does the same with ShouldApplyExpression: a table, column, or index carries an expression, and the object is only deployed to targets where it evaluates true. It’s a real field in the table file:
{ "Name": "[AuditNote]", "DataType": "NVARCHAR(400)", "Nullable": true, "ShouldApplyExpression": "'{{DeployEnv}}' <> 'Production'"}{{DeployEnv}} is a script token you pass per environment (the same token mechanism the quench config uses). On a Production run the expression is false, so the column never lands; everywhere else it does. Same intent as a Liquibase context — environment-aware deployment — but declared on the object instead of passed as a --contexts flag at runtime.
And the preconditions row in that table? A Liquibase <preConditions> that gates a changeset becomes a ValidationScript on the product — a query that must return true before quench touches the database at all:
"ValidationScript": "SELECT CAST(CASE WHEN EXISTS(SELECT * FROM sys.databases WHERE name = '{{MainDb}}') THEN 1 ELSE 0 END AS BIT)"Rollback blocks — the <rollback> you hand-write so liquibase rollback can walk backward. There’s nothing to write in SchemaSmith. You don’t step backward through history; you change the declared state and quench converges to it. The “undo” is just the next forward run with a different shape declared.
Per-engine notes
Section titled “Per-engine notes”The mechanism is identical on all three engines — whitelist the four tables, extract, quench to a no-op. Only the dialect’s names differ.
| SQL Server | PostgreSQL | MySQL | |
|---|---|---|---|
ObjectList form | dbo.Customer,… | public.customer,… (folded lowercase) | Customer,… (schema = database) |
| Tables left behind | DATABASECHANGELOG, DATABASECHANGELOGLOCK | same two | same two |
| Extract result | 4 tables, no ledgers | 4 tables, no ledgers | 4 tables, no ledgers |
Check yourself: Liquibase leaves two bookkeeping tables, not one. How many do you have to name to exclude them?
None. You whitelist what you want, not what you want gone. ShouldCast:ObjectList names the four shop tables, so SchemaTongs extracts exactly those — both DATABASECHANGELOG and DATABASECHANGELOGLOCK are off the list and never enter the package. One whitelist handles any number of tables you’re leaving behind. The live tables stay put; quench never touches them.
Liquibase kept two ledgers by the anvil — one to log every changeset, one to lock the door while it worked. But the finished blade needs neither. SchemaTongs reads the shape you forged, casts it to files, and both ledgers go cold in the corner where you left them.
Got a changelog full of contexts or rollback blocks you’re not sure how to map? Email me at forgebarrett@schemasmith.com — I read every one.
Next up: Course 5 · Module 3 — Migrating from EF Core migrations, where the history table is __EFMigrationsHistory and the schema lives in C# instead of XML.
Until then, may no lock decide when you may strike, and every shape you forge ring true on the first blow.
— Forge