Why partitioning and placement are refused, not converged
Here are two partition layouts for the same table.
before: p2024 (< 2025) p2025 (< 2026) p2026 (< 2027)after: p2024 (< 2025) p2025_h1 (< 2025-07) p2025_h2 (< 2026) p2026 (< 2027)What operation turns the first into the second?
You can see it: p2025 was split in half. Now answer the harder question — how would a tool know that, given only the two lists? It could equally be a drop of p2025 and a create of two new partitions, which produces the same final layout and destroys a year of financial records on the way.
The diff is identical. The operations are not. And nothing in the package distinguishes them, because a declarative model describes the state you want, never the route. Run that comparison backwards and it gets worse: a MERGE and a DROP-plus-CREATE are also indistinguishable, and one of them is data loss.
That is the whole reason placement is refused rather than converged, and it is the sharpest example in the product. It is not that repartitioning is hard. It is that the intent is genuinely absent from the input.
The posture
Section titled “The posture”Placement is applied exactly once, when the table is created. After that, a declaration that disagrees with the database is reported and the run stops.
Three engine families express it three ways, and the lab deploys all three:
// SQL Server -- a scheme NAME, plus the column the partition function applies to{ "PartitionScheme": "ps_vault_year", "PartitionColumn": "[EntryYear]" }
// MySQL / MariaDB -- partitioning on the table itself{ "Partitioning": { "Method": "RANGE", "Expression": "EntryYear", "Partitions": [ ... ] } }
// PostgreSQL -- tablespace placement{ "Tablespace": "vault_ts" }Ask any of them to move, and you get the same sentence in three dialects, at exit 2:
Table [dbo].[LedgerEntry] declares partition scheme ps_vault_year_alt, but is currently deployed onpartition scheme ps_vault_year. SchemaSmith does not move an existing table between partition schemes-- that rewrites every row. Migrate it manually, or correct the declared scheme to match.P0001: table public.ledgerentry declares tablespace pg_default, but is currently deployed on vault_ts.SchemaSmith does not move an existing object to a different tablespace (that is a rewrite) -- migrateit manually, or correct the declared tablespace to match.Declared partitioning does not match the deployed table (refused -- repartitioning rewrites every row):LedgerEntry declares RANGE(EntryId), deployed RANGE(`EntryYear`)Read what each one gives you: the object, both values, the reason, and what to do instead. That is the shape of every refusal in this course — a refusal that only said “cannot do that” would be an obstacle. One that names both sides is a diff you can act on.
Names, not creations
Section titled “Names, not creations”Look again at the SQL Server declaration. "PartitionScheme": "ps_vault_year" is a name. SchemaSmith did not create that scheme, and it never will — the same way it never creates a filegroup, and never created the vault_ts tablespace either. Your setup script did.
That asymmetry is deliberate and worth internalising. A partition scheme, a filegroup, a tablespace — these are server-side objects with sizing, storage and placement decisions behind them that no schema package can see. Your model gets to say put this table there. It does not get to say what there is, or to invent one if it is missing.
Which is why a declared scheme that does not exist fails by name, before any DDL runs. Not a silent skip, not an improvised default: your package named something the server does not have, and that is a mistake worth stopping for.
The distinction that trips everyone
Section titled “The distinction that trips everyone”Now the move that looks like a loophole. Delete the placement property from the package entirely — no PartitionScheme, no Tablespace — and deploy.
Exit 0. No refusal. And the table has not moved.
Declaring nothing and declaring something different are two completely different requests. Unset means SchemaSmith does not manage placement here — not “the default”, not “put it back”. A table a DBA placed by hand, in a package that has never mentioned placement, is left exactly where they put it.
If that reads as an inconsistency, hold it until Module 4: it is the doctrine that makes every refusal in this course coherent rather than arbitrary, and it is most of what makes this tool safe to aim at a database you did not build.
The family
Section titled “The family”The same posture, the same reason, across the roster — applied at CREATE, refused by name on change, because the change rewrites the table:
| Property | Engine |
|---|---|
PartitionScheme + PartitionColumn | SQL Server |
Partitioning (RANGE / LIST / HASH / KEY) | MySQL, MariaDB |
FileGroup, TextImageFileGroup, FileStreamFileGroup | SQL Server |
Tablespace — table and index | PostgreSQL |
Tablespace — InnoDB general | MySQL |
DataDirectory | MySQL, MariaDB |
XmlCompression | SQL Server |
| InnoDB page compression | MySQL, MariaDB |
Plus the irreversible table types, which land in the same place for a blunter reason — the engine has no ALTER for them at all: memory-optimized durability and inline-index shape, Ledger, GraphType. There is no operation to refuse; there is only “drop it and build another one”, which is not a thing a deploy gets to decide.
Eight properties and three table types, one rule. That is what makes it a posture rather than a list of special cases — and once you can predict it, you stop being surprised by it.
Check yourself: A teammate wants to switch a large table from RANGE to HASH partitioning and proposes just editing the package and deploying, since SchemaSmith 'handles partitioning'. Walk them through what happens and what they should do instead.
The deploy stops at exit 2 and names both layouts — declared HASH, deployed RANGE — without attempting anything. The table is untouched, so nothing is half-migrated.
That is the correct outcome, and the reason is worth saying out loud rather than just quoting the error: switching RANGE to HASH redistributes every row in the table. The package describes the destination, not the route, so SchemaSmith cannot tell a planned redistribution from a typo in a file someone edited on a Friday — and it will not rewrite a large table on the strength of that ambiguity.
What they should do: perform the repartition deliberately in a migration script, where the operation is explicit and reviewable, then update the package to match the new reality so the next deploy is a no-op. The declaration follows the migration; it does not perform it. And if placement is genuinely not something the package should own on that table, removing the property entirely is the honest answer — unset means unmanaged.
Every module in this course asks the same question from a different angle: does the tool have enough information to do this safely, and can it get back what it is about to disturb?
Here the answer is no in the cleanest possible way. Not “this is hard”, not “this isn’t supported yet” — the information required to choose between a SPLIT and a DROP simply is not in a declarative model, and no amount of tooling can put it there. So the tool names both layouts and hands the decision to the person who actually knows which one they meant.
Next: Module 2 — state your package never described, where the change is possible, the data is fine, and SchemaSmith still stops — because something the database knows about your table was never in your file.
Until then, may your data stay where you put it.
— Forge