Retiring the gates
That legacy variant you’ve been carrying since Module 2 — the one for the server nobody’s upgraded yet? The last laggard just upgraded. So why is that dead branch still in your package?
Hey folks. I’m Forge Barrett, master of the Content Forge here at SchemaSmith.
This is the last module, and it’s the one that makes the whole pattern honest. Everything we built across this course — the engine adapting, the gates you wrote, two shapes of one query, the wire format bending at the oldest tier — every bit of it was debt with a payoff date. A gate is scaffolding. Scaffolding you never take down isn’t a feature; it’s rot. Today we collect the debt. Let’s kindle the forge one more time.
A gate is transitional by design
Section titled “A gate is transitional by design”Here’s the reframe the whole module turns on. When you gate a script on {{ServerMajorVersion}} < 16, you’re not writing a permanent branch. You’re writing a note to your future self: this shape exists only until the last old server catches up. The gate has an expiration date baked in — you just don’t know the exact day yet.
The farm converges. It always does, eventually — the maintenance windows come, the budgets clear, the one contract-bound tenant finally moves. And when the last laggard upgrades, the legacy variant behind your gate stops running. Not with an error. Not with a log line. It just quietly never fires again, sitting in the package as dead code nobody has the nerve to delete because nobody’s sure it’s safe.
Retiring it is a deliberate act. And it’s a three-part move — the third part is the one everybody forgets.
The three-part retirement
Section titled “The three-part retirement”Take the PostgreSQL view split from Module 2 — the any_value() modern view gated {{ServerMajorVersion}} >= 16, paired with a min() legacy view gated < 16. The fleet’s converged; every server’s on PG16 now. Here’s how you retire it.
Part 1 — delete the legacy variant. The gate and its second shape come out together. You delete the Programmability/Legacy folder’s SQL file, drop its entry from Template.json, and strip the ShouldApplyExpression off the surviving Programmability/Modern folder — because a blank or absent gate always applies. One shape again, exactly like before the fleet ever split.
Part 2 — raise MinimumVersion. In Product.json, you declare the floor you now enforce:
{ "Name": "ReadingPlatform", "MinimumVersion": "16", "Platform": "PostgreSQL"}Part 3 — pre-flight refuses below it. This is the part people skip, and skipping it is where retirement bites back. With MinimumVersion set, any server below the floor gets refused at pre-flight — before a single object changes.
Deploy the retired package to a PG16 server and it lands clean: one view, no gate, floor cleared.
[localhost].[learn] Quenched .\after\package\Templates\Main\Programmability/Modern\public.v_reading_summary.sql[localhost].[learn] Successfully QuenchedPoint that same package at a PG12 server — a laggard that slipped back into the farm somehow — and watch the floor do its job:
One or more target servers are below the product's declared MinimumVersion; aborting before any deployment: localhost: detected version 12 is below the product's declared MinimumVersion 16
# non-zero exit, nothing deployed:$ echo $?3Read what that refusal actually is. It’s not a runtime failure buried three tables deep in a deploy. It’s a loud, early, enforced stop that fires before anything is touched. Without Part 2, retirement is only half done: the gate’s gone, the legacy SQL’s gone, and nothing stops a forgotten PG12 server from being handed the package and blowing up deep in the run where any_value() doesn’t resolve. Part 3 turns a silent runtime branch into a pre-flight wall.
The two numbers, finally paid off
Section titled “The two numbers, finally paid off”Remember Module 0? The very first thing this course insisted on was keeping two numbers apart — and here’s where that pays off.
MinimumVersion is your policy floor. It answers “may I deploy here?” The detected version is the engine’s own report — it answers “what DDL will I generate?” We drew that line in Module 0 and never let it blur. Raising MinimumVersion to retire a gate only makes sense because these were never the same number. You’re not changing what the engine detects; you’re changing what you’ve agreed to support.
And there’s a third number in play, which is why the honesty matters. SchemaSmith’s own capability floor reaches all the way back — PostgreSQL 12, SQL Server 2008. That’s what the tool can do. MinimumVersion is what you do, and yours is allowed to sit higher. Declaring 16 doesn’t mean SchemaSmith dropped PG12 support; it means your package did, on purpose. Your policy rides on top of the product’s capability, and it’s allowed to be stricter.
One honest note about SQL Server
Section titled “One honest note about SQL Server”I showed that refusal on PostgreSQL, and there’s a reason — it’s the same binary-versus-compat honesty we’ve kept all course.
MinimumVersion compares the detected server version, not the compatibility level. Our SQL Server sandbox is one 2022 binary hosting learn_2022, learn_2016, and learn_2008 at different compat levels — but they all report the same server version: 16. So a SQL Server MinimumVersion is all-or-nothing across those tiers. It can’t refuse the compat-100 database while accepting the compat-160 one, because it isn’t looking at compat level at all — it’s looking at the server, and the server is one machine.
PostgreSQL has a genuine 16-versus-12 gap — two real binaries, two real detected versions — so it’s the honest place to watch the floor refuse one server and accept another. On SQL Server you can still do Parts 1 and 2 exactly the same: delete the legacy variant, raise MinimumVersion. The refusal just can’t discriminate across compat tiers that live on a single binary. That’s not a gap in the tool — it’s MinimumVersion gating on the axis it actually gates on, and it’s worth knowing which axis that is before you lean on it.
What survives a re-extraction
Section titled “What survives a re-extraction”There’s one more thing a gated package has to do before you trust it: survive being re-extracted. You deploy from files, sure — but you also cast live databases back to files with SchemaTongs. If a round-trip through the tongs quietly mangled your variants, every extract would corrupt the gate. It doesn’t, and it’s worth seeing why.
Take a SQL Server table carried as two token-gated variants — dbo.Gadget.Modern.json and dbo.Gadget.Legacy.json, same table, gated on a script token instead of a version:
dbo.Gadget.Modern.json VariantName "Modern" ShouldApplyExpression '{{Edition}}'='Modern' (Id, Label)dbo.Gadget.Legacy.json VariantName "Legacy" ShouldApplyExpression '{{Edition}}'='Legacy' (Id)Product.json sets the Edition token to Modern, so the Modern variant is the live one. Deploy it, then cast the table straight back out of the database with SchemaTongs. The re-extraction resolves {{Edition}} to Modern before it evaluates the gate — the same substitution deploy does — so it knows which variant is active and folds the extracted shape into it. Four things hold:
- The active Modern variant is refreshed in place — same file,
VariantNamekept. - The gate survives raw —
'{{Edition}}'='Modern'stays authored, not baked down to'Modern'='Modern'. - The inactive Legacy variant is byte-for-byte untouched.
- No ungated
dbo.Gadget.jsongets written — the token gate folds instead of falling through, so the validator reports noSS-DUP-001duplicate.
$ schematongs --ConfigFile:SchemaTongs.settings.json Cast Json for dbo.Gadget Casting ./package\Templates\Main\Tables\dbo.Gadget.Modern.json Tables: 1 extracted, 0 errorsCasting Completed Successfully
$ ls package/Templates/Main/Tables/dbo.Gadget.Legacy.jsondbo.Gadget.Modern.json
$ schemaquench --Validate --SchemaPackagePath:./package0 error(s), 0 warning(s)That’s what makes a gated variant safe to keep in a package indefinitely: extracting the live shape doesn’t duplicate it, doesn’t lose the gate, doesn’t touch the variant that isn’t deployed. You author the gate, run with it, cast through it, and retire it by hand on your own schedule.
Two retirement stories — and why only one is loud
Section titled “Two retirement stories — and why only one is loud”One last contrast, because it sharpens the whole idea.
Back in Module 3 you built a state gate — a dbo.RolloutControl row somebody flips per tenant. That gate has a visible, deliberate off-switch. Flip the row back or drop the table, and the gate is demonstrably retired. You can see it turn off; someone did it on purpose, and there’s a record.
A version predicate retires nothing like that. {{ServerMajorVersion}} < 16 never gets switched off — it just quietly stops mattering the moment the last old server upgrades. Nothing announces it. The legacy SQL sits there as dead code that never runs, and no one notices until they’re reading a diff a year later wondering what it’s even for.
That’s exactly why the MinimumVersion bump earns its place. Deleting the legacy variant is silent housekeeping. Raising the floor is the declaration — it turns an invisible convergence into a loud pre-flight refusal you can point at and say “we don’t support that anymore, and here’s the wall that proves it.” Two retirement stories, and the version gate only becomes observable when you make it so.
What just happened
Section titled “What just happened”You closed the loop. A version gate is scaffolding with an expiration date, and you took it down in three moves: deleted the legacy variant, raised MinimumVersion to declare the floor, and let pre-flight refuse everything below it — a silent dead branch turned into a loud, early stop. You saw why that refusal reads honest on PostgreSQL’s real version gap and stays all-or-nothing across SQL Server’s compat tiers on one binary. You watched a token-gated variant survive a SchemaTongs round-trip with its gate and its twin intact. And you saw why the floor bump is the thing that makes a version-gate retirement something you can actually see.
That’s the arc. The engine adapts. You gate what it won’t touch. You pick between two valid shapes, or gate on state the server can’t see. You go to the oldest tier and the wire format bends. And then the farm converges, the package declares its new floor, and you end up holding a single clean shape again — right where a single-version shop started, except now you know exactly how you got here and exactly how to do it again.
That’s the craft. One package, every tier of the farm, and the discipline to take the scaffolding down when the building stands on its own.
Subscribe and stick around — there’s more coming from the forge. But you’ve got the whole rolling upgrade now, start to finish.
Until then, may your fleet converge in its own good time, and may every gate you forge already know the day it comes down.
— Forge
Check yourself: Your last old PostgreSQL 12 server finally upgraded, so you delete the legacy min() view and strip the version gate off the surviving any_value() view. A teammate says retirement's done. What did you skip, and what does skipping it cost you?
You skipped raising MinimumVersion. Deleting the legacy variant is only Part 1 of a three-part move; Part 2 is declaring the new floor in Product.json ("MinimumVersion": "16"), and Part 3 is the payoff — pre-flight now refuses anything below it before a single object changes. Without the floor bump, the gate is gone and the legacy SQL is gone, but nothing stops a forgotten PG12 server from being handed the package and failing deep in the deploy where any_value() doesn’t resolve. The MinimumVersion bump is what turns a silent runtime failure into a loud, early, enforced pre-flight stop — and it’s the thing that makes the retirement observable rather than just a quiet deletion nobody can point at.
Check yourself: You raise MinimumVersion to the current major on a SQL Server package, expecting pre-flight to refuse your compat-100 database while still accepting the compat-160 one on the same 2022 instance. It accepts both. Why — and where would this same MinimumVersion actually discriminate?
MinimumVersion compares the detected server version, not the database compatibility level. All three databases on that 2022 instance — compat 100, 130, 160 — report the same server version (16), because it’s one binary. So a SQL Server MinimumVersion is all-or-nothing across compat tiers on a single instance; it accepts all of them or none, because it never looks at compat level. It discriminates where there’s a real detected-version gap between separate binaries — PostgreSQL 16 versus 12, for instance, where "MinimumVersion": "16" accepts the 16 server and refuses the 12 one. This is the same two-numbers distinction from Module 0: MinimumVersion is your policy floor on the server version, a different axis from the compatibility level the course gated syntax on. On SQL Server you can still delete the legacy variant and raise the floor — the refusal just can’t tell compat tiers apart on one machine.