You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
SQL Server memory-optimized (Hekaton) tables can now be declared, deployed, and round-tripped. Set MemoryOptimized: true and a Durability of SCHEMA_AND_DATA (the default) or SCHEMA_ONLY on a table, and BucketCount on a hash index; SchemaSmith creates the table with its indexes declared inline — the only form the engine accepts, since CREATE INDEX is rejected on a memory-optimized table — extracts all of it, and re-deploys it as a no-op. Because a memory-optimized table's storage engine, durability, and index shape are fixed at creation (SQL Server has no ALTER for any of them), a declaration that disagrees with the deployed table is refused by name — the memory-optimized flag, the durability, and any change to the inline index set or a hash bucket count — rather than silently ignored or attempted and failed; migrate such a change by recreating the table. Ownership is tracked in a new SchemaSmith.ProductOwnership table rather than the ProductName extended property every other SQL Server table carries, because memory-optimized tables reject extended properties outright — so drop-by-absence, cross-product protection, and PreventDrop all work on them exactly as they do elsewhere. Requires a server with In-Memory OLTP support and a database with a MEMORY_OPTIMIZED_DATA filegroup; without them the deploy fails with the engine's own message rather than degrading a memory-optimized table to an ordinary one, which would silently change its durability and concurrency semantics.
A PostgreSQL index can now declare its storage parameters, not just its fill factor. An index's StorageParameters map carries the WITH (...) reloptions PostgreSQL attaches to an index — gin with fastupdate = off, brin with pages_per_range = 64, a pgvector hnsw index with its m and ef_construction, and so on — and SchemaSmith extracts them, deploys them, and round-trips them. Before this only fillfactor was understood, so any other storage parameter was invisible to extraction and re-applied by no one; a gin index tuned with fastupdate = off came back as an ordinary one, and a change to one was never detected. fillfactor keeps its own dedicated handling and is deliberately excluded from the map so the two never contend over the same clause, and an index that declares no storage parameters emits no WITH clause exactly as before. The comparison is order-insensitive, so re-ordering the map is not a change; changing a value is.
PostgreSQL domain types can now be declared instead of scripted — and doing so fixes a silent no-op. A Domain Types/*.json file declares the base type, NotNull, Default and a named CheckConstraints list, and SchemaSmith converges them. The scripted form it replaces could not be written correctly at all: there is no CREATE OR REPLACE DOMAIN, so a scripted domain is a guarded CREATE DOMAIN — and once the domain exists that guard skips. Editing the CHECK in the .sql file changed nothing, on every deploy, forever, while the run reported success. Constraints, the default and NOT NULL converge in place, via ALTER DOMAIN, without dropping the domain or touching a single column that uses it; a constraint the package stops declaring is dropped, which is safe in a way removing an enum value is not — it removes a rule, not data, and cascades to nothing. The base type is the exception and is refused by name: PostgreSQL has no ALTER DOMAIN … TYPE at all, so delivering that change would mean dropping the domain and every column typed by it. Adding a constraint validates the existing data and fails loudly if a row violates it — that is the engine protecting you, and it is surfaced rather than worked around. The Domain Types/ folder still accepts .sql files exactly as before, so no existing package changes behaviour.
SQL Server tables and indexes can now be placed on a partition scheme.PartitionScheme and PartitionColumn declare where a table's data lives, and the same pair on an index places it independently -- an index is not required to be aligned with its table, and either can be partitioned without the other. Like FileGroup, the scheme is a name only: SchemaSmith never creates a partition function or scheme, and a declared scheme that does not exist on the target fails by name before any DDL runs. Both halves are declared together or not at all, and a table cannot declare a filegroup and a scheme at once. A change to a deployed table is refused, not applied -- moving a table onto, off, or between partition schemes rewrites every row, and a state-based comparison cannot tell a SPLIT from a MERGE from two layouts. The refusal names both the declared placement and the live one. Partitioned tables and indexes now also round-trip through extraction, where previously they extracted as ordinary unpartitioned objects and a redeploy silently built the wrong physical layout. Because nothing here ever creates a scheme, there is no edition or version gate: if the scheme exists, your server already supports it. --Validate catches the two authoring mistakes before you reach a server: SS-PART-001 for half a declaration, SS-PART-002 for a filegroup and a scheme together.
MySQL and MariaDB tables can now declare their partitioning. A Partitioning object carries Method (RANGE, LIST, HASH, KEY, and the COLUMNS forms), Expression, PartitionCount for HASH and KEY, and an ordered Partitions list of names and boundaries for RANGE and LIST -- order is part of the definition, since RANGE boundaries must ascend. It is applied when the table is created and round-trips through extraction. A change to a deployed table is refused, not applied, for the same reason as SQL Server: ALTER TABLE ... PARTITION BY rewrites every row. The comparison normalizes backticks, whitespace and case before deciding, because the engines do not agree on how they report a partition expression back -- MySQL 5.7 echoes what you wrote while MySQL 8, MariaDB 10.2 and MariaDB 11.4 all return a rewritten form -- so the same package deploys identically across all of them. A table your package says nothing about is left alone, so a package targeting a database someone partitioned by hand keeps deploying exactly as it does today.
A column change blocked by SCHEMABINDING can now resolve itself (SQL Server) — #323. SQL Server refuses to alter a column while a schema-bound view or function references it, and error 4922 says only that "one or more objects access this column". SchemaSmith already names the module, the column, and the remedy; DropSchemaBoundDependents now applies the remedy for you — it drops the blocking modules, applies the column change, and the after-tables object pass recreates them from your package. SchemaSmith deliberately does not save and replay the definition it found on the server: your package is the authority on what the module should be, the server's copy is only what happens to be deployed. That means your scripts must run after the table work, so SchemaTongs now extracts schema-bound views and functions into SchemaBound Views/ and SchemaBound Functions/ — folders on the AfterTablesObjects slot — whether or not the setting is on, so an extracted package is already shaped correctly the day you turn it on. Off by default, because a drop that fired unasked would destroy a scripted object in packages that never opted in. Dropping discards every GRANT on the module and SchemaSmith does not restore them — it manages permissions on no object — so re-grant in the recreating script or from whatever process you already use. An encrypted module (WITH ENCRYPTION) is refused even with the setting on, and refused before anything is dropped: OBJECT_DEFINITION returns NULL for one, so nothing could put it back. Indexed views are unaffected — those were already dropped and recreated around column changes.
PostgreSQL row-level security policies are now declarable. A Policies array on a PostgreSQL table declares CREATE POLICY definitions -- Name, Permissive, Command, Roles, UsingExpression and WithCheckExpression -- and they round-trip through extraction. This completes a feature that shipped half finished: RowLevelSecurity could turn row-level security on, but with no way to declare a policy, and a table with row-level security enabled and no policy returns no rows at all to anyone but its owner. So the half that existed could lock a table with no supported way to unlock it. A policy that leaves the package is dropped, and unlike an index there is no opt-out flag for it: a stale policy is a live access-control rule, and leaving one behind is a security posture nobody declared. Editing an expression on an existing policy is not detected -- PostgreSQL stores USING and WITH CHECK normalised, so comparing them against the declared text would report a change on every deploy. SchemaQuench converges the set of policies; rename the policy, or remove and re-add it, to change an expression.
Ledger tables are now declarable (SQL Server)."Ledger": "AppendOnly" or "Updatable" creates a tamper-evident ledger table, and the setting round-trips through extraction. It cannot be combined with IsTemporal — a ledger table manages its own history and SQL Server reports it as non-temporal — and that combination is refused rather than guessed at. Requires SQL Server 2022; below that the table deploys as an ordinary one and the change is reported through UnsupportedFeaturePolicy. Ledger tables are close to permanent: SQL Server has no ALTER that converts a table to or from one, and DROP does not remove it — the table is retained under a generated name. So changing Ledger on a deployed table is refused, and the objects the engine retains are neither extracted nor considered for removal on later deploys.
Graph tables are now declarable (SQL Server)."GraphType": "Node" or "Edge" creates the table AS NODE / AS EDGE, and the setting round-trips through extraction. SQL Server has no ALTER that converts a table to or from a graph table, so changing GraphType on a deployed table is refused by name rather than attempted — recreate the table or correct the declaration. Requires SQL Server 2017; below that the table deploys as an ordinary one and the change is reported through UnsupportedFeaturePolicy. The system-generated graph columns are never treated as yours: they are kept out of extracted packages and are never considered for removal.
PostgreSQL extensions have a documented recipe. Declare a folder ({ "FolderPath": "Extensions", "QuenchSlot": "Objects" }) and put an idempotent CREATE EXTENSION IF NOT EXISTS … script in it. This needed no new SchemaSmith feature — an extension is database-scoped and part of no table, so it is a scripted object like a schema or a collation, created on every run and never dropped by absence. The reference now covers the ordering (extensions before the tables whose column types they supply), why the script must be idempotent, and why SchemaSmith will not remove or upgrade one.
SchemaTongs can now extract from a read-only replica. Extraction used to install its helper procedures into the source database on every run, which needs write access — so it could not run against a SQL Server Availability Group readable secondary, a PostgreSQL hot standby, or a MySQL/MariaDB replica, which is usually the copy you are allowed to query freely. Against a read-only target it now verifies instead of installing: helpers missing is a clear error telling you to run once against the primary, helpers older than the current build is a warning and the extraction proceeds, and helpers whose version cannot be determined warns that it skipped the install and they might be out of date. Deploying is unchanged — it genuinely needs a writable target.
FILESTREAM columns are now declarable (SQL Server).FileStream on a VARBINARY(MAX) column stores its value on an NTFS filegroup instead of in the row, and FileStreamFileGroup names the table's FILESTREAM_ON filegroup, applied by ALTER immediately before the FILESTREAM column is added -- the clause cannot ride the CREATE TABLE, because the column is deliberately withheld from it until a covering unique constraint exists. The table needs a ROWGUIDCOL column covered by a PRIMARY KEY or a UNIQUE constraint -- a unique index does not satisfy SQL Server here, and declaring one gets a message naming the exact package change rather than SQL Server's error 5505. Declare the column itself as "DataType": "UNIQUEIDENTIFIER ROWGUIDCOL", the same way IDENTITY is declared. FILESTREAM has to be enabled on the server with a FILESTREAM filegroup on the database, neither of which SchemaSmith creates; without them the column still deploys as a plain VARBINARY(MAX) and the storage change is reported through UnsupportedFeaturePolicy rather than applied silently.
Table-level Change Tracking is now declarable (SQL Server).EnableChangeTracking turns SQL Server change tracking on for a table, with TrackColumnsUpdated to record which columns changed rather than only that the row did. Both round-trip through extraction. Change Tracking needs to be enabled on the database first (ALTER DATABASE ... SET CHANGE_TRACKING = ON); SchemaSmith does not turn that on for you, because it sets retention and auto-cleanup for every table in the database -- a package that asks for tracking without it is reported through UnsupportedFeaturePolicy rather than deployed green and left untracked. Changing TrackColumnsUpdated on an already-tracked table discards the tracking baseline (SQL Server offers no in-place alter), so that reset is announced by name in the deploy log and every consumer must re-synchronize.
DropPeriodsRemovedFromProduct — remove a MariaDB application-time period the package no longer declares. Periods previously converged one way: created with a new table, added to an existing one, and never removed. This is the one drop-by-absence setting that defaults to OFF, and deliberately so: extraction omits the Periods key entirely when a table has none, so a package written before periods were supported — or extracted from MariaDB 10.4.3–11.3, where the catalog cannot report them — carries no periods even when the table has one. Dropping on that absence would remove a declaration the package never had the chance to make. Turn it on to say the package is the authority. Set it in SchemaQuench.settings.json or per table. Dropping a period leaves its columns and their data untouched — only the period, and the check constraint MariaDB backs it with, are removed.
MariaDB application-time periods now round-trip. A PERIOD FOR validity(start, end) — the interval a row's data is valid for, as distinct from system versioning's record of when it was stored — is read into a Periods list on the table, and a declared one is created with the table on deploy. Declaring a period against a server too old to accept one degrades the clause away and records it, rather than failing the whole CREATE on syntax the engine cannot parse. A table can carry both, and they stay separate: the SYSTEM_TIME period MariaDB lists alongside them is deliberately not reported here, because the table already declares that through IsSystemVersioned and a package that said it twice could contradict itself. One version caveat worth knowing before you rely on it: periods themselves work from MariaDB 10.4.3, but the catalog that reports them only arrives in 11.4 — so extracting from a 10.4.3–11.3 server returns no periods even where the table has them, and a package round-tripped through such a server loses them. Deploying a declared period to those versions is unaffected; it is only the read that is blind.
MariaDB system-versioned tables now round-trip. A table created WITH SYSTEM VERSIONING keeps its own row history, and MariaDB reports it as SYSTEM VERSIONED rather than BASE TABLE. SchemaSmith recognises it, extracts it, and deploys it through the IsSystemVersioned property — detected from the table type, the only signal that answers for both authoring forms (declare the period columns yourself, or let the engine hide them). The engine-owned row-start/row-end columns of the explicit form are left out of the extracted package, the same way SQL Server's GENERATED ALWAYS AS ROW START/END columns already are, so a re-deploy never tries to manage columns the engine owns. A table declaring IsSystemVersioned: true is created WITH SYSTEM VERSIONING; an existing ordinary table that starts declaring it converges via ALTER TABLE ... ADD SYSTEM VERSIONING. Removing versioning is refused by name, never dropped — MariaDB's DROP SYSTEM VERSIONING purges the row history rather than just switching the attribute off, so the refusal points you at a migration script instead, and it fires under --WhatIf too. Version-gated at MariaDB 10.3+; below the floor, and on MySQL, which has no system versioning at any version, it degrades through Target:UnsupportedFeaturePolicy — warn (the default) deploys an ordinary table and records a downgrade, fail aborts — on both the create and converge paths — #412.
SystemVersioningAlterHistory — opt in before a column change rewrites recorded history (MariaDB). MariaDB refuses any column change on a system-versioned table unless @@system_versioning_alter_history is KEEP, and KEEP does not merely permit the change: it applies it to the stored history as well, so rows are rewritten to a shape they never actually had. That is a data-retention decision rather than a syntax one, so SchemaSmith does not make it for you. Left unset, the engine refuses the change exactly as it does today — and only when a change genuinely needs it, never on a re-deploy where nothing differs. Set it to KEEP when rewriting the history is what you actually want.
Re-extracting a package no longer reshuffles it. SchemaTongs sorted every list alphabetically on each extraction, so refreshing a package produced a whole-file diff that buried the one thing that actually changed — and a file whose ordering had been arranged by hand lost that arrangement every time. Product:PreserveExistingOrder (default true) keeps the order a file already had for everything still present, appends genuinely-new entries, and drops what the database no longer has. It covers Columns, Indexes, ForeignKeys, CheckConstraints and, on SQL Server, Statistics and XmlIndexes. Product:ObjectOrder chooses the sequence used when there is nothing to preserve: Name (default, alphabetical) or Physical, the table's own column order. Neither setting changes a deployment — they decide how the file is written. The extraction procedures accept the same choice when called by hand: a @p_ObjectOrder argument on SQL Server and PostgreSQL, and a SET @SchemaSmith_ObjectOrder session variable on MySQL and MariaDB, whose stored procedures cannot declare default parameter values.
PostgreSQL and MySQL data extraction now carries a spatial column's SRID. A geometry or geography value extracted from those engines was written as bare WKT, so the spatial reference system was dropped. Delivered into a destination column that declares its SRID the value is coerced back and nothing is lost, but delivered into an untyped column it silently became SRID 0 — correct coordinates in the wrong reference system, with no error and nothing in the output to notice. Extraction now emits the same <column>.STSrid companion SQL Server has always produced, on both the JSON and XML encodings, so a package extracted from PostgreSQL or MySQL keeps its reference system wherever it is deployed. Delivery reads the companion on all three engines, so a spatial value now round-trips into an untyped destination column on any of them, in both the JSON and XML encodings. Packages extracted before this release carry no companion and are unaffected — they continue to apply as SRID 0, exactly as they did.
A column change on a CDC-tracked table no longer discards captured change history (SQL Server). Deploying a column add, drop, or type change to a table with "EnableCDC": true disabled Change Data Capture before the column work and re-enabled it afterwards, which dropped the capture instance and its change table — every row a downstream reader had not yet consumed went with it, silently, with the deploy reporting success. SchemaSmith now leaves CDC running through the column work and adds a second capture instance covering the new column set, which is SQL Server's own supported pattern. The original instance keeps its history and is deliberately not dropped, because only you can know when your readers have drained it; the deploy log names it, gives the exact sys.sp_cdc_disable_table call to remove it, and warns that the next column change will fail until you do. SQL Server permits only two capture instances per table, so when both are already in use the deploy refuses up front — before touching any column — naming the tables and how to clear them, rather than failing partway through or silently discarding history — #398.
SQL Server full-text indexes now support STATISTICAL_SEMANTICS. The per-column clause completes the full-text trio alongside TYPE COLUMN and LANGUAGE, and is extracted, compared, and deployed on both the modern and the pre-2016 encodings. It requires the Semantic Language Statistics Database on the server and SQL Server 2012 or later; below that the clause is simply absent, as the feature does not exist there.
New SQL Server columns can now populate the rows already in the table. Adding a nullable column with a Default left existing rows NULL, and there was no way to ask for anything else. "BackfillExistingRows": true on a SQL Server column emits WITH VALUES so those rows get the default. It is opt-in because turning it on by default would rewrite existing data, and --Validate reports SS-COL-001 when it is set without a Default. PostgreSQL, MySQL and MariaDB already backfill, so the setting is SQL Server only.
A table can now be rebuilt instead of altered column by column. A deploy that changes several columns on one table emits one ALTER per column, and on a large table each of those is its own full pass over the data — so a package that reshapes a table pays for the reshape many times over. RebuildPolicy lets you ask for the other trade instead: build the table to the declared definition once, copy the rows across, and swap. Declare it on a table, a template, a product, or the environment (RebuildPolicyMode, RebuildPolicyThreshold, RebuildPolicyOnOrderMismatch in SchemaQuench.settings.json); the nearest level that declares one wins whole, so a table asking only for "Mode": "ALWAYS" never inherits a threshold from above it. Mode is NEVER (the default — always alter in place), ALWAYS (rebuild whenever a column change is detected), or THRESHOLD with a Threshold count (rebuild once that many columns need modifying; column additions and removals are not counted, because a rebuild saves nothing on those). Rebuilds are opt-in and stay opt-in: a package that declares no RebuildPolicy anywhere deploys exactly as it did before, and no rebuild is ever elected for it. Indexes, keys, constraints and defaults are re-created by the ordinary deploy passes that follow, and the identity/sequence position is carried across rather than re-derived from the copied rows. A table whose live state cannot survive a copy — system versioning, Change Data Capture, replication, Change Tracking, logical replication, inheritance or partitioning — is refused by name with the blocking state named, in --WhatIf as well as a real run, rather than quietly falling back to altering in place. --WhatIf prints the full rebuild sequence and records it in the change manifest without executing anything. OnOrderMismatch is a separate switch that composes with any Mode rather than replacing it, so { "Mode": "THRESHOLD", "Threshold": 3, "OnOrderMismatch": true } reads rebuild if three modifications pile up or if the deployed column order has drifted — and pairing it with the default NEVER asks for a rebuild on order drift and nothing else. Reordering existing columns is impossible in place on every supported engine, so a rebuild is the only thing that can deliver it. The comparison is of relative order over the columns the package and the table share, not of absolute positions: a column dropped from the middle of a table leaves a permanent gap in the engine's ordinal numbering, and treating that as drift would rebuild a correctly-ordered table on every deploy forever. A column the package adds in a mid-file position does count, because the engine can only append it and a rebuild is the only way to move it into place.
TextImageFileGroup places a table's large-object data (SQL Server).TEXTIMAGE_ON is the third filegroup clause alongside FileGroup (ON) and FileStreamFileGroup (FILESTREAM_ON), and it decides where text, ntext, image, xml and (MAX) column data lives. A FILESTREAM column does not count as a large-object column — SQL Server's own error says "non-FILESTREAM varbinary(max)" — and declaring the property on a table with no large-object column is refused by name rather than surfacing the engine's error 1709, which names neither the table nor the property. Create-time only, like both siblings: there is no ALTER for large-object placement, so a declared filegroup that differs from where the data already lives fails rather than being quietly ignored. A declared filegroup that does not exist fails by name too — SchemaSmith does not create filegroups.
Two SQL Server index options are now declarable: IgnoreDuplicateKey and PadIndex.IgnoreDuplicateKey (IGNORE_DUP_KEY) is the one that matters: it changes what your application sees, not how fast it runs. Off, inserting a duplicate into a unique index fails the whole statement with 2601 and nothing is written; on, the duplicate is discarded with a warning and the rest of the statement succeeds — so a multi-row INSERT containing one duplicate lands the other rows instead of rolling back. Two databases whose index definitions otherwise match will disagree about whether the same INSERT works, which is why it belongs in a schema package. PadIndex (PAD_INDEX) applies FillFactor to intermediate index pages; it does nothing without a FillFactor, which is why it is declared alongside it. Both round-trip through extraction and are handled on the modern and pre-2016 encodings alike. On indexed views: SQL Server rejects IGNORE_DUP_KEY on a view index outright, so there is nothing to declare; PadIndex is supported on an index inside an Indexed Views/ definition.
Editor schemas no longer offer settings your engine ignores.Template.json and Product.json are shared shapes, so every engine's generated .json-schema advertised settings that do nothing on it — DropExcludeConstraintsRemovedFromProduct appeared for MySQL, UpdateFillFactor for MariaDB, and so on. Editors offered them and nothing said they were inert. Those settings are now scoped to the engines they actually apply to, and the schema says which — "SQL Server and PostgreSQL only." — so a setting that applies to two engines out of four reads correctly in both files rather than looking universal. Nothing changes at deploy time: a package that already sets one of these on an engine that ignores it deploys exactly as before. --Validate will now report it, which is the point.
PostgreSQL sequences can now be declared instead of scripted. A Sequences/ folder holding .json files declares a sequence's type, increment, bounds, cache and cycle; SchemaSmith compares each against the server and alters only what differs, so an unchanged sequence produces no statement at all. The current value is never managed — a sequence's position records which numbers have already been handed out, so a deploy that reset it would re-issue keys already in use. Start applies when the sequence is created; SchemaSmith never issues RESTART and extraction never captures the current value. A .sql file in the same folder still runs exactly as before.
PostgreSQL enum types can now be declared instead of scripted — and doing so fixes a silent no-op. An Enum Types/ folder holding .json files declares an enum's value list; SchemaSmith compares it against the server and adds what is missing. As a scripted object this was worse than merely manual: the extracted script is a guarded CREATE TYPE, and once the type exists that guard skips — so editing the value list in the .sql file changed nothing, on every deploy, forever, while the run reported success. Order is preserved and is not cosmetic: PostgreSQL sorts and compares enum values by declared position, so a value you add in the middle of the list is added in the middle of the type rather than appended. Removing a value is reported, never performed — PostgreSQL cannot remove or reorder one without recreating the type, which would mean dropping every column that uses it, so the value stays and is named in the log and the change manifest. Nothing you have breaks: a .sql file in the same folder still runs exactly as before, and extraction now writes the declarative form.
MySQL and MariaDB scheduled events can now be declared instead of scripted. An Events/ folder holding .json files declares events the way Tables/ declares tables: they are compared against the server, converge when they differ, and can be removed when they leave the package (DropEventsRemovedFromProduct, off by default). As scripted objects they were re-run on every deploy — dropped and recreated whether or not anything had changed — and were never removed by absence, so a retired event kept firing until someone dropped it by hand. Nothing you have breaks: a .sql file in the same Events/ folder still runs exactly as before, so migration is per-event and optional, and --Validate reports SS-EVT-001 if the same event is described both ways. One behaviour worth knowing: an event that omits Starts leaves the server's own start time alone rather than managing it. MySQL fills in an unspecified STARTS with the moment the event was created, so treating it as declared would make every later deploy see a difference, recreate the event, and reset its schedule — a nightly job would drift forward on every deploy. Set Starts explicitly if you want a fixed one. Extraction writes the declarative form, and drop-by-absence only ever considers events SchemaSmith created — one made by hand, or by a scripted Events/ file, is never removed.
XmlCompression compresses XML column data in place (SQL Server 2022+). Declarable on a table and on an index, independent of CompressionType — a table can carry both. The version story is asymmetric and worth knowing before you rely on it: the clause DEPLOYS from SQL Server 2022, but sys.partitions.xml_compression does not exist there — on 2022 it lives only on sys.internal_partitions, which reports nothing for an ordinary table — and arrives on sys.partitions in 2025. So 2022–2024 honour the setting and cannot report it back. SchemaTongs handles that by carrying the declared value forward from the package it is refreshing rather than silently stripping a property the server is applying; on 2025+ the server is authoritative and the value round-trips normally. For the same reason, a change to the setting on an already-deployed table converges on 2025+ but is not re-evaluated on 2022–2024 — there the setting is applied when the table is created. Below 2022 the clause is suppressed, the table or index deploys uncompressed, and the loss is reported through UnsupportedFeaturePolicy — nothing an application can observe changes, only the storage saving. Unlike TextImageFileGroup, SQL Server accepts the clause on a table with no XML column, so no declaration is refused for that.
MySQL and MariaDB InnoDB compression options are now declarable.Compression (MySQL), PageCompressed and PageCompressionLevel (MariaDB), and KeyBlockSize (both) round-trip through extraction and are applied on create. These four ship together because they share a hiding place: each surfaces in exactly one column, INFORMATION_SCHEMA.TABLES.CREATE_OPTIONS, a single free-text blob that extraction did not read at all. The engines disagree in three ways that all had to be handled: MySQL double-quotes the value (COMPRESSION="zlib"), leaves others bare (KEY_BLOCK_SIZE=8) and reports the key uppercase; MariaDB backtick-quotes the key (`PAGE_COMPRESSED`=1) and reports it lowercase. Compression is MySQL-only and PageCompressed MariaDB-only — each is a hard syntax error on the other engine, so neither appears in the other's schema and neither is ever emitted to it. KeyBlockSize is the compressed-page size ofRowFormat: "COMPRESSED", so it is declared alongside it. A combination both engines refuse is now caught before deploy:Compression or PageCompressed together with RowFormat: "COMPRESSED" fails with MySQL error 1031 or MariaDB errno 140, neither of which names the option that caused it — --Validate reports SS-CO-001 instead, and SS-CO-002 for a PageCompressionLevel set without PageCompressed. Extraction emits these only for a table that declares them, so existing packages are unchanged.
MySQL and MariaDB at-rest table encryption is now declarable.Encryption (MySQL ENCRYPTION='Y') and Encrypted with an optional EncryptionKeyId (MariaDB ENCRYPTED=YES / ENCRYPTION_KEY_ID) round-trip through extraction and are applied on create, and changing the setting on an existing table converges by rebuild (ALTER TABLE … ENCRYPTION='Y' / ENCRYPTED=YES). Encryption is MySQL-only and Encrypted/EncryptionKeyId MariaDB-only — each is the other engine's syntax — so neither appears in the other's schema. Encryption needs a server-side key-management backend, exactly as a filegroup must exist before you can place a table on it: without one the engine rejects the clause with its own error, which SchemaSmith does not pre-empt with a fabricated capability gate. Extraction emits the properties only for a table that declares them, so existing packages are unchanged.
MySQL tables can now declare the general Tablespace they are placed in.Tablespace names an InnoDB general tablespace, is applied at create, and round-trips through extraction. Create-time only, matching FileGroup on SQL Server and Tablespace on PostgreSQL: moving a table between tablespaces is a physical relocation, so a declared tablespace that differs from where the table already lives is refused by name rather than moved, under --WhatIf as well as a real run. Omitting it means placement is not managed — not a declaration of the default. MySQL-only: MariaDB has no general tablespaces (CREATE TABLESPACE is a syntax error there), so the property never appears in a MariaDB package. Extraction emits it only for a table in a named general tablespace, so existing packages are unchanged.
MySQL and MariaDB tables can now declare a DataDirectory (InnoDB DATA DIRECTORY). The filesystem directory a table's data file is placed in is applied at create and round-trips through extraction. Create-time only, the same placement posture as Tablespace: a declared directory that differs from where the table already lives is refused by name, never moved, under --WhatIf too. On MySQL the directory must be listed in the server's innodb_directories or the engine rejects the create with its own error — server configuration, like a missing filegroup, not something SchemaSmith gates. INDEX DIRECTORY is deliberately not supported: InnoDB rejects it on both engines (it is a MyISAM-only clause). A table-level directory on a partitioned table is applied at create but does not round-trip, because the InnoDB catalog names such a table's files per-partition; per-partition placement is out of scope. Extraction emits the property only for a table that declares a directory, so existing packages are unchanged.
PostgreSQL tables and indexes can now declare their Tablespace. Materialized views have been able to since they shipped, so supporting placement on one relation kind and not the other two was an accident of what got built rather than a decision. Tablespace on a table or an index places it at create time and round-trips through extraction. Omitting it means placement is not managed — it does not declare the database default. That distinction is the whole contract: reading an omitted value as "the default" would make every object a DBA had placed by hand fail its second deploy, in packages that never mentioned placement at all. An index is declared separately from its table because it does not inherit the table's tablespace — with no clause it follows default_tablespace, which is usually but not always the same place. Create-time only, matching FileGroup on SQL Server: moving an existing table rewrites it under an ACCESS EXCLUSIVE lock and moving an index rebuilds it, so a declared tablespace that differs from where the object already lives is refused by name — naming the object, the declared tablespace and the live one — rather than silently moving your data. Clearing a declared value back to unset is a no-op. Extraction emits the property only for an object that is not on the database default, so existing packages are unchanged.
PostgreSQL REPLICA IDENTITY is now declarable, and round-trips — #407.ReplicaIdentity (DEFAULT, FULL, NOTHING or INDEX) and ReplicaIdentityIndex declare what a logical-replication publication sends for an UPDATE or DELETE — and, on a published table, whether either is permitted at all. That is the part worth knowing: a table in a publication with no usable replica identity does not replicate badly, it makes PostgreSQL refuse the write with cannot update table ... because it does not have a replica identity and publishes updates. Extraction previously carried neither the setting nor the index it names, so a table extracted from a replicated source and redeployed came back at DEFAULT — two databases whose columns and indexes matched, one of which rejected the application's writes. Both properties now extract and deploy. Omitting ReplicaIdentity means "leave the server's setting alone", not "reset to DEFAULT", and extraction emits it only for a table that is not already at DEFAULT — so existing packages are unchanged and a table you set out of band is not quietly reverted. It is applied after indexes are created, so INDEX mode works on a table's first deploy rather than only on a later one. --Validate reports a declaration that cannot work before you deploy it: SS-RI-001 (INDEX mode naming no index), SS-RI-002 (naming an index the table does not declare), SS-RI-003 (naming a non-unique index) and SS-RI-004 (naming an index while the mode is not INDEX, so it is ignored).
MariaDB per-column WITHOUT SYSTEM VERSIONING is now declarable, and round-trips — #408. A system-versioned table can exclude a column from its row history, so an UPDATE touching only that column writes no history row — usually because the column is large or high-churn. SchemaSmith supported the table-level half (IsSystemVersioned) and not this one, so extracting such a table and redeploying it silently re-enabled history on a column the author had deliberately excluded. Nothing errored; the difference only showed up in what the history table accumulated. WithoutSystemVersioning on a MariaDB column now extracts and deploys. It only means anything on a system-versioned table — MariaDB accepts the clause on an ordinary table and silently discards it, so --Validate reports SS-SV-001 rather than letting a declaration that does nothing look applied. Changing it on a column that is already deployed is an ALTER, which MariaDB refuses on a versioned table unless you have opted in with SystemVersioningAlterHistory: "KEEP" — the same data-retention decision that setting already governs. Requires MariaDB 10.3.4; below that the clause is suppressed and the column deploys ordinarily. MySQL has no system versioning at any version, so the property is MariaDB-only and never appears in a MySQL package.
Fixed
Loading and saving a package no longer adds keys you never wrote. A domain property that defaults itself -- a table's Engine, an index's CompressionType, a full-text index's ChangeTracking and StopList, a foreign key's MatchType, a sequence's DataType, Increment and Cache, a policy's Permissive, Command and Roles, a product's BranchNameFile and BeforeBranchNameMask, and more -- carried its default only as a field initialiser, with nothing declaring that value as the default. Serialisation therefore had nothing to compare against and always wrote the key, so a hand-authored file that deliberately omitted it gained it the first time any tool loaded and saved the file: {"OnOrderMismatch": true} came back as {"Mode": "NEVER", "OnOrderMismatch": true}. The defaults are materialised when the file is read, so this was never an editor artifact -- any save, from any tool, churned the file and buried real changes in a diff. Every such property now declares its default, so an omitted key stays omitted. Nothing about deployment changes: an absent key still means exactly what it always did, and a value you wrote explicitly is still written back. A guard test now fails the build if a new self-defaulting property is added without declaring its default, so the set cannot quietly grow again.
A table rebuild no longer silently de-partitions the table (SQL Server, MySQL, MariaDB) -- #410.RebuildPolicy replaces a table with a copy built from the declared definition, and the guard that refuses to do so when the live state cannot be reconstructed -- system versioning, Change Data Capture, replication, Change Tracking -- did not list partitioning on two of the three engines. On SQL Server the copy carried no placement clause at all and landed on the default filegroup, taking any partition-aligned index with it; on MySQL and MariaDB the partition definition lives in the table DDL, so a copy built from the column list was unpartitioned by construction. Every row survived and the layout the table existed for did not, with nothing reporting it. PostgreSQL already refused. All three now do, naming partitioning so you know what to migrate around.
A serial column's sequence is no longer extracted as a standalone object (PostgreSQL) — #409. Extraction excluded sequences owned by a column using pg_depend.deptype = 'i', which is correct for an IDENTITY column but not for serial, whose sequence is recorded as 'a'. Every serial column's generated sequence was therefore extracted as if you had created it yourself. Redeploying such a package created that sequence first, so CREATE TABLE ... serial found the name taken, generated a second sequence named <name>1, and pointed the column at that one — leaving an orphan sequence behind and a column whose sequence name no longer matched the database it came from. Each extract-and-redeploy cycle added another. Both dependency kinds are now excluded, so an engine-generated sequence stays with the column that declares it.
A column change blocked by SCHEMABINDING now names what is blocking it — #323. SQL Server's error 4922 says only that "one or more objects access this column", leaving you to go and find which. SchemaSmith now names the module, the column it blocks, and the remedy. It does not drop the module unless you ask it to — see DropSchemaBoundDependents above — because a schema-bound view or function is a scripted object SchemaSmith does not own, and dropping one unasked would destroy something the package never described. Indexed views are unaffected — those are already dropped and recreated around column changes.
Extraction no longer writes a table file for a temporal history table — #403. A database containing a system-versioned table extracted into a package holding both the versioned table and its history table. The history table is created by the versioned table's own declaration (IsTemporal plus the HistoryTable* properties), so a separate file for it made the next deploy try to create it as an ordinary table and the package stopped round-tripping. The same applied to the whole family of objects a ledger table generates: the MSSQL_LedgerHistoryFor_* history table, the live <table>_Ledger view, and the MSSQL_DroppedLedgerTable_*, MSSQL_DroppedLedgerHistory_* and MSSQL_DroppedLedgerView_* objects SQL Server retains when a ledger table is dropped. Those names carry an object id or a GUID from the source server, so they could not be deployed anywhere. The ledger table itself is still extracted — only what the engine generates around it is skipped.
Extracting a SQL Server graph table no longer produces an undeployable package — #402. A node or edge table (AS NODE / AS EDGE) carries system-generated columns whose names end in a per-table GUID, and extraction emitted them as ordinary columns: a node table with two real columns came out with four, an edge table with one came out with nine, plus the auto-created GRAPH_UNIQUE_INDEX_<guid>. The resulting package could not be deployed anywhere, including back to the database it was extracted from. The existing filters could not catch them — graph columns report generated_always_type = 0 like any user column, and four of them are not hidden either — so the exclusion now keys off sys.columns.graph_type, which is set for exactly these and null for every real column. Fixed on both the JSON and XML extraction paths.
EnableCDC was silently ignored when CDC was not enabled on the database (SQL Server) — #401. A table declaring "EnableCDC": true deployed successfully against a database where Change Data Capture had not been turned on at the database level: the run reported success, the table was not tracked, and nothing in the output said so. A declared feature that cannot be applied is now reported rather than skipped — the default warn deploys the table, records a downgraded row naming it, and logs the sys.sp_cdc_enable_db call that would allow it, while UnsupportedFeaturePolicy: fail refuses the deploy. SchemaSmith still will not enable CDC on your database for you: that changes retention, cleanup jobs and storage database-wide, so doing it because one table asked would trade a silent no-op for a silent side effect on every other table in it.
A credential inside a URL is now masked in logs. Log scrubbing masked a Password=/Pwd= connection-string field but not a credential in a URL's userinfo component, so ?password=secret in a value was masked while user:pass@host a few characters earlier in the same value was not. Any URL-shaped value under an unremarkable name — a webhook, a custom endpoint, a URL inside a map-typed setting — could therefore carry its password into a log or a saved artifact. The username, host, port and path are all preserved, so a scrubbed line is still diagnosable; only the secret is replaced. Your database connection strings were never exposed by this: SchemaSmith builds those from discrete fields, and a raw --ConnectionString is masked whole by name before this stage runs.
Re-deploying a MariaDB table that has an application-time period failed outright — #399. MariaDB implements a period as a CHECK constraint named after it, and nothing in the catalog distinguishes that from a check constraint you wrote yourself. Drop-by-absence therefore saw an undeclared check and tried to remove it, which the engine refuses: Can't DROP CONSTRAINT ... Use DROP PERIOD ... for this. The first deploy of such a table succeeded and every one after it failed. Period-backed constraints are now left alone. This is fixed from MariaDB 11.4 only — identifying them requires the catalog that reports periods, which arrives in that release, and below it a genuine user check constraint comparing two columns cannot be told apart from a period.
A MariaDB system-versioned table was silently missing from every extracted package — #399. Extraction matched only TABLE_TYPE = 'BASE TABLE', and MariaDB reports a system-versioned table as SYSTEM VERSIONED, so such a table was dropped from the package with no error and no warning. A package re-extracted from a database containing one came back short, and because drop-by-absence works from what SchemaSmith owns rather than from what the catalog reports, a table that had been made system-versioned after deployment could then look removed-from-product on the next deploy.
MariaDB no longer tries to create a system-versioned table that already exists. MariaDB reports such a table as SYSTEM VERSIONED rather than BASE TABLE, and the snapshot that decides whether a table is new only looked for BASE TABLE -- so the table was invisible and every deploy emitted CREATE TABLE for it and failed.
A SQL Server full-text index declared with a null ChangeTracking is no longer silently skipped. The value was concatenated straight into CREATE FULLTEXT INDEX, and because concatenation with NULL yields NULL the entire statement vanished -- no index was created, with no error and no log line, and every later deploy repeated it. A null now falls back to AUTO, matching the default an omitted value already got.
PostgreSQL VIRTUAL generated columns are now listed in the capability registry. Declaring one against PostgreSQL below 18 degrades through Target:UnsupportedFeaturePolicy like any other version-gated feature, but the degrade was missing from the capability list SchemaSmith publishes, so tooling that reads that list to describe what an engine supports could not see it. Behaviour is unchanged — the degrade already worked and already recorded a manifest row.
A table file that will not parse no longer aborts the whole extraction. SchemaTongs reads the file it is about to replace so it can carry forward settings the database cannot report. If that file was corrupt or hand-edited into invalid JSON the read threw and the entire cast failed — leaving you with the broken file and no extract, when the extract was the thing that would have repaired it. It is now a warning naming the file and the parse error, extraction continues, and the file is replaced. The warning is explicit that the unreadable file's authored settings (data delivery, ShouldApplyExpression, drop overrides) could not be carried forward, so the replacement should be checked before committing.
Re-extracting a table no longer silently reverts eight deploy-behaviour settings. SchemaTongs overwrites a package in place and carries forward the settings extraction cannot read back from the database — but eight were missing from that list, so authoring one and re-extracting quietly returned it to its default. Affected UpdateFillFactor (on tables and indexes), a statistic's SampleSize, and every table-level Drop...RemovedFromProduct override. The Drop... family is the one to check: a table set to stop removing objects that left the product would start removing them again after a re-extract, with nothing in the package or the output to show what changed. All eight now survive, and the carry-forward is driven by the property definition itself rather than a hand-maintained list, so a future setting cannot go missing the same way.
A new table is now created with its columns in the order the package declares. SQL Server re-sorted them alphabetically and PostgreSQL left the order to the query planner, so the column sequence you authored was not the sequence you got — and on PostgreSQL it was not guaranteed to be the same twice. Both now follow the file, which is what MySQL and MariaDB already did. ALTER TABLE likewise adds new columns in declared order (still appended after the existing ones — placing a column among existing ones is a table rebuild, not a formatting choice). Packages whose columns are already alphabetical — which is what extraction produces — deploy exactly as before.
MySQL and MariaDB extraction now order a table's columns the same way SQL Server and PostgreSQL do. Those two engines emitted columns in the table's ordinal order while the other two emitted them alphabetically, so the same table extracted from different engines produced a whole-file diff that was entirely noise. All four now sort by column name, which is also stable when a source table's ordinal order changes. Values are addressed by name everywhere, so nothing about deployment behaviour changes — but the first re-extract of an existing MySQL or MariaDB package will show its columns reordered once.
A blocked rename during bootstrap now tells you which objects clashed. When a database holds both a column (or table) and its declared OldName, SchemaSmith refuses to guess which one holds your data and stops — correct, but on SQL Server and MySQL/MariaDB the error named neither object, leaving "rename manually" with nothing to act on. PostgreSQL always named them; the other two now match. SQL Server interpolates the schema, table and both column names into the error. MySQL and MariaDB cap error text at 128 characters, so the short summary is unchanged and the full detail is written to the status log the run already prints. Reachable with mixed CLI versions across environments: a newer CLI renames the column, an older one re-adds the old name.
--Validate printed the location twice on many findings. The reporter renders every finding as SEVERITY [Code] Location: Message, but ten checks also opened their message with the same location, so the table, column, or file path appeared twice in a row on one line. Foreign-key, index-column, token and .json-schemas findings were all affected; the output is now the single prefix it was always meant to be. Message wording is otherwise unchanged, so anything grepping for a phrase still matches.