Course 4 · Recipe 2 — Policy that enforces itself
Your retention policy says documents archive after 90 days and may never be kept past a year. So those numbers are sitting in a column default somewhere, and in a check constraint, and in a wiki page, and in a comment in the app — and the day the policy changes, you’ve got to find every one of them. Miss one and your schema is quietly enforcing last year’s rule. The numbers want to live in exactly one place. Let’s put them there.
The lever: declare the numbers once, on the object
Section titled “The lever: declare the numbers once, on the object”Drop the policy into the table’s Extensions as a custom property — nested, because it’s a policy with parts:
"Extensions": { "Retention": { "ArchiveDays": "90", "MaxRetentionDays": "365" } }Now read each number back where it’s needed. Nested values flatten with dots, so {{Table.Retention.ArchiveDays}} and {{Table.Retention.MaxRetentionDays}} are the tokens — and they resolve in a column’s Default and in a check constraint’s expression, both evaluated at deploy time:
{ "Columns": [ { "Name": "ArchiveAfterDays", "DataType": "INT", "Default": "{{Table.Retention.ArchiveDays}}" } ], "CheckConstraints": [ { "Name": "CK_Document_RetentionWithinPolicy", "Expression": "RetentionDays <= {{Table.Retention.MaxRetentionDays}}" } ]}You never typed 90 or 365 into the default or the check. Both read from the one place you declared them. The default seeds new rows; the check rejects anything past the ceiling. Deploy it and prove the guard:
INSERT INTO Document (DocumentId, Title, RetentionDays) VALUES (1, 'Contract', 50); -- OK, ArchiveAfterDays defaults to 90INSERT INTO Document (..., RetentionDays) VALUES (2, 'Forever', 90, 500); -- rejected: 500 > 365The recipe: change the policy, the default follows
Section titled “The recipe: change the policy, the default follows”Here’s the move. Change the archive number — 90 to 30 — in the one place it lives, and re-quench:
"Extensions": { "Retention": { "ArchiveDays": "30", "MaxRetentionDays": "365" } }schemaquench --ConfigFile:deploy.settings.jsonRead the column default back:
ArchiveAfterDays default → 30You didn’t touch the column. You changed the policy on the table, and the next ordinary quench re-derived the default from it — the same way on SQL Server, PostgreSQL, and MySQL. Each number you declared is the only place that value lives, and the schema rebuilds itself around it on every deploy.
The aha: one source of truth, and the schema reads it
Section titled “The aha: one source of truth, and the schema reads it”That’s the shift from Recipe 1. There, metadata decided whether an object deployed. Here, metadata supplies values that other objects compute from — a default seeded from one, a guard rail checked against the other. The retention numbers stop being copied into four places and start being declared in one each, with the column reading your intent instead of holding a stale copy of it.
Want the exact rules — which expression fields resolve tokens, how nested values flatten, the full scope table? The Custom Properties reference lays it out. The mechanism is identical on all three engines; only the constraint syntax differs.
Check yourself: You change the archive number in your table's Extensions and re-quench. What happens to the column default that reads it, and what's the check constraint doing?
The default re-derives from the new value — the token resolves at deploy time, so the default follows the metadata automatically. The check constraint is the guard rail built from its own declared number (the max-retention ceiling): it keeps enforcing that bound, rejecting any row past it. One number on the table drives the default new rows get; another drives the ceiling they’re held to — and each lives in exactly one place.
Think of it as the stamp on a die. You set the depth once on the jig, and every part that comes off it carries that exact measure — you don’t re-measure each piece, you don’t hope they match. Change the jig and the next run changes with it. The retention rule is that stamp: cut once into the metadata, pressed into every default the schema builds from it.
Got a number you’re keeping in sync by hand across a default, a constraint, and three documents — retention, a max length, a tier ceiling? Email me at forgebarrett@schemasmith.com — I read every one.
Next up: Course 4 · Recipe 3 — The package asks the server, where a token pulls a live value off the target database at deploy time, before a single script runs.
Until then, may the rule you set once hold true in every column it touches, and never drift from the number you meant.
— Forge