Why an omitted property is not a default
Someone put a table on a dedicated tablespace two years ago. Storage separation, an audit requirement, a retention policy — good reasons, written down somewhere you’ve never read, made by a DBA who did not consult you and was not required to.
Your package has never mentioned tablespaces. It declares columns and a primary key, nothing more.
You deploy. What should happen?
There is a reading of a declarative model where the answer is obvious and wrong: the model is the complete desired state, the model does not say vault_ts, therefore the table should not be on vault_ts. Under that reading your deploy moves a large archive table to the default tablespace — a full rewrite — on a Tuesday, because of a property your package never had an opinion about.
That is not what happens.
exit=0vault_tsThe table stayed exactly where the DBA put it.
Silence is not a value
Section titled “Silence is not a value”Omitting a property means SchemaSmith does not manage it. It never means “set it to the default.”
The lab proves it by changing exactly one thing. Same table, same database, same deployed state — add one key to the file:
{ "Tablespace": "pg_default" }P0001: table public.retaineddocument declares tablespace pg_default, but is currently deployed onvault_ts. SchemaSmith does not move an existing object to a different tablespace (that is a rewrite)-- migrate it manually, or correct the declared tablespace to match.Exit 2. Opposite outcome, and nothing about the database changed between the two runs. What changed is whether your package claimed placement:
| Package says | Meaning | Result |
|---|---|---|
| nothing | ”placement is not mine to manage” | exit 0, left alone |
"Tablespace": "pg_default" | ”placement is mine, and it should be pg_default” | exit 2, refused as a move |
Declaring nothing and declaring the default are different statements. Most tools collapse them, and that collapse is how a deploy ends up reverting work nobody asked it to touch.
The same reading, everywhere
Section titled “The same reading, everywhere”This is not a tablespace special case. It runs through the product:
| Property | Unset means |
|---|---|
Tablespace — PostgreSQL table and index, MySQL general | placement unmanaged; a hand-placed object stays put |
DataDirectory — MySQL, MariaDB | placement unmanaged, not “has none” |
PartitionScheme / Partitioning | partitioning unmanaged; a hand-partitioned table is left as it is |
Encryption / Encrypted | encryption unmanaged; a tablespace someone encrypted stays encrypted |
ReplicaIdentity — PostgreSQL | leave the server’s current setting alone |
Statistics | only the named statistics your product defines; auto-created statistics are never touched |
Six properties, one idea: your package’s authority reaches exactly as far as what it declares, and stops there.
Which also tells you how to take ownership when you want it. There is nothing to enable — you declare the property. From that deploy on, it is compared like everything else you declare, with all the consequences this course has shown you, including a refusal if the change would be a rewrite.
What ties the four modules together
Section titled “What ties the four modules together”Every refusal in this course turns on one question:
Is the thing I am about to disturb written down in the model?
Module 1 — placement is applied at CREATE and refused on change, because a diff of two partition layouts cannot tell a SPLIT from a MERGE; the intent was never in the input. Module 2 — a rebuild is refused when the shadow copy would discard state the package never held, and DropSchemaBoundDependents acts freely in the one case where the recipe is in your package. Module 3 — a partitioned table is never dropped by absence, even though you asked, because a deleted file is a likelier mistake than an irreversible loss is a decision.
And this module is what makes those three coherent instead of arbitrary. “Unset means unmanaged” is what keeps the answer to that question honest: a property you never declared was never yours, so it never enters the comparison, so no deploy of yours can lose it.
That is the whole design in one line, and it’s the answer to the objection this course opened with. SchemaSmith doesn’t refuse because it’s cautious, or immature, or missing a feature. It refuses in exactly the cases where acting would destroy something it has no recipe to rebuild — and it is precise about which cases those are, which is why the rest of it is safe to run unattended.
Check yourself: Your team wants every table's tablespace pinned in the package so nothing drifts. You have 200 tables, most hand-placed years ago across three tablespaces. What happens if you add the correct current Tablespace to all 200 at once, and what is the risk you should raise?
If every declared value genuinely matches where the table already lives, the next deploy is a no-op — a declaration that agrees with reality converges to nothing. So the mechanical answer is “it works.”
The risk is what you just took on. Before the change, placement was unmanaged: a mismatch was impossible because the package had no opinion. After it, all 200 tables are in the comparison, and any future disagreement is a refused deploy — including ones you didn’t cause. A DBA moves one table for an IO problem at 2am and your next unrelated deploy fails at exit 2 on a table nobody touched.
That may well be what you want; it is drift detection, and it is loud on purpose. But raise it as the trade it is rather than as pure tidiness: you are converting a class of silent divergence into a class of blocked deploys, and somebody needs to own responding to them. And get the values from the database, not from what you assume — one wrong entry among 200 is a refused deploy on a table that was fine.
You started this course with ten courses’ worth of evidence that SchemaSmith converges things, and one question: if converging is good, why not converge everything?
Because converging everything requires either information the model does not contain, or permission to destroy things it cannot rebuild. Where the tool has both the information and the recipe, it acts — and you have watched it do that in every course before this one. Where it has neither, it names both sides and stops.
The refusals are not the edges of what this tool can do. They are the reason you can let the rest of it run without watching.
Got a refusal you disagreed with? Email me at forgebarrett@schemasmith.com — those are the most useful messages I get, because a guard that fires on the wrong thing is worth more to me than a hundred that fire correctly. More’s coming from the forge.
Until then, may your tools own exactly what you gave them, and not one thing more.
— Forge