Skip to content

SchemaSmith v2.4.0

Choose a tag to compare

@github-actions github-actions released this 14 Aug 13:49
· 509 commits to main since this release

Added

  • PostgreSQL 12 is now a supported target (the floor was 15) — newer-version features are degraded, not refused. The PostgreSQL floor drops from 15 to 12 by generating version-correct SQL for the detected target instead of turning older servers away. Beyond the NULLS NOT DISTINCT handling described below, reaching 12 adds: per-column compression and expression statistics (both PostgreSQL 14) are routed through the same unsupported-feature policy (skip + a downgrade-manifest line, or fail) and their version-specific catalog reads (pg_attribute.attcompression, the pg_stats_ext_exprs view) are version-branched so they parse on 12/13; removing a column's generation (ALTER COLUMN … DROP EXPRESSION, PostgreSQL 13+) is done by drop-and-re-add below 13; and a latent double-declaration of an identity column's owned sequence (harmless on 13+, fatal on 12's stricter getOwnedSequence) is fixed. Verified end-to-end against real PostgreSQL 12 and 14 containers plus current PostgreSQL. NULLS NOT DISTINCT (a PostgreSQL 15 feature) is the first construct handled through a new general unsupported-feature policy (Target:UnsupportedFeaturePolicy, e.g. SmithySettings_Target__UnsupportedFeaturePolicy=fail): the default warn emits the index/constraint without the unsupported clause and records a "Unsupported Feature Downgrades" line in the deployment summary naming each object and the version it needs, so the deploy succeeds with a clear manifest; fail aborts pre-emptively with a "requires PostgreSQL 15" message for shops that would rather not deploy a silently-degraded schema. Data delivery also adapts: the generated MERGE (PostgreSQL 15+) falls back to a manual INSERT + UPDATE upsert below 15 (NULL-safe keys and non-unique match keys included). Compare-side catalog reads that reference version-specific columns are version-branched so they parse on the older server, and SchemaSmith's own one-owner tracking index uses a COALESCE-based unique index below 15. Verified end-to-end (kindle, schema deploy, and data delivery) against a real PostgreSQL 14 container; the full PostgreSQL integration suite passes on 14 and on current PostgreSQL.
  • SQL Server 2008 is now a supported target (the floor was 2017 / compatibility level 130). The SQL Server floor drops from 2017 to 2008 — compatibility level 130 down to 100 — by ingesting and comparing the schema model as XML below the JSON cliff instead of turning older databases away. SchemaSmith hands its parsed model to SQL Server as JSON (OPENJSON / FOR JSON) at compatibility level 130+ (SQL Server 2016+) and automatically switches to an XML encoding (.nodes() / .value() / FOR XML PATH) below 130 — where OPENJSON's JSON path is a parse error — so a database left at compatibility level 100 through 120 (common where a line-of-business app is certified against an older level) deploys and reverse-engineers the same schema. The encoding is selected automatically from the detected compatibility level and server version; Target:CompatEncoding (auto | legacy | modern, e.g. SmithySettings_Target__CompatEncoding=legacy) overrides it for a deployment, and Source:CompatEncoding does the same for SchemaTongs extraction. Version-gated constructs are handled the same way as on PostgreSQL: STRING_AGG … WITHIN GROUP and STRING_SPLIT (compatibility level 130) fall back to FOR XML PATH ordered aggregation and a split function, and the general unsupported-feature policy (Target:UnsupportedFeaturePolicy, default warn) now applies to SQL Server as well as PostgreSQL. Verified end-to-end (kindle, schema deploy, and extraction) against compatibility-level-100 databases; the full SQL Server integration suite passes. Object extended properties are preserved on extraction below the JSON cliff too — they are emitted attribute-encoded (arbitrary property names round-trip) and rebuilt on ingest, so a legacy-tier extract carries the same Extensions.ExtendedProperties the modern tier does. — #353, #296
  • Data delivery can encode its content file as XML — deployable on every SQL Server compatibility level. SchemaSmith's automatic table-data delivery shreds its payload with OPENJSON, which requires SQL Server compatibility level 130 (SQL Server 2016+) — so on a database left at compatibility level 100–120, a data delivery parse-errored even though the schema itself deployed against the lowered 2008 floor. A DataDelivery may now declare "ContentEncoding": "Xml" (default "Json", unchanged) to carry its content file as XML, which SchemaSmith shreds with .nodes() / .value() — a path that works at every compatibility level — so the lowered SQL Server floor is data-deliverable, not just model-deployable. Because the delivery payload is your data in a shape SchemaSmith does not own, the encoding is an explicit per-delivery author choice, never inferred or transcoded between JSON and XML. The XML row shape is a documented, stable contract: <rows><row><c n="ColumnName">value</c>…</row></rows> — an absent <c> is NULL, binary is base64, and geometry is WKT with a companion <c n="Column.STSrid"> SRID element. Reaching every compatibility level also required the shared merge-metadata helpers to stop assuming STRING_AGG (compatibility level 130): each now falls back to row-based aggregation below the cliff, with the modern STRING_AGG path unchanged. A JSON-encoded delivery aimed at a below-130 target now degrades through the unsupported-feature policy rather than parse-erroring: the default warn skips just that delivery with a clear message (re-encode it as XML to deploy it there) and delivers the rest, while Target:UnsupportedFeaturePolicy=fail aborts; XML-encoded deliveries on the same target are unaffected. To author the XML shape without hand-writing it, SchemaTongs/DataTongs gains a global --DeliveryEncoding=Xml switch (default Json) that extracts each table's data directly in the XML shape and stamps "ContentEncoding": "Xml" on the reconciled DataDelivery entry, so an extract → deploy round-trip works against a compatibility-level-100 target. SQL Server only — PostgreSQL and MySQL/MariaDB shred their delivery payload at every supported version, so they have no equivalent cliff (declaring Xml, or requesting XML extraction, on those engines is rejected). — #296
  • MySQL 5.7 and MariaDB 10.2 are now supported targets (the floors were MySQL 8.0 / MariaDB 10.6). The MySQL floor drops from 8.0 to 5.7 and MariaDB from 10.6 to 10.2 by generating version-correct SQL for the detected target. The schema model is parsed with a single version-agnostic JSON_EXTRACT shred in place of JSON_TABLE (MySQL 8.0 / MariaDB 10.6), so the same model kindles and deploys on every version 5.7–11.x. Newer DDL that a below-floor target lacks is taken by an equivalent path with the same end state: a column rename falls back from RENAME COLUMN (MySQL 8.0 / MariaDB 10.5.2) to CHANGE COLUMN reconstructing the current column definition, and an index rename falls back from RENAME INDEX (MariaDB 10.5.2) to drop-and-recreate. Features with no equivalent below their introduction degrade through the unsupported-feature policy (Target:UnsupportedFeaturePolicy, default warn → apply without the feature + a "Unsupported Feature Downgrades" manifest line naming each object and the version it needs; fail → abort pre-emptively): CHECK constraints require MySQL 8.0.16 (MariaDB enforces them at the 10.2 floor); descending index key parts are stored ascending below MySQL 8.0 / MariaDB 10.8; and automatic table-data delivery requires MySQL 8.0 — on MariaDB 10.2 it works via a recursive-CTE shred (full support), and below the MySQL floor it is skipped with a clear log (use manual data scripts). The Target:UnsupportedFeaturePolicy policy that began with PostgreSQL and SQL Server now applies to MySQL and MariaDB as well. The hard wall is the floor itself: MySQL 5.6 and MariaDB 10.1 have no JSON support and are rejected outright. Verified end-to-end (kindle, schema deploy, and data delivery) against real MySQL 5.7 and MariaDB 10.2 containers; the full MySQL and MariaDB integration suites pass on 5.7 / 10.2 and on current MySQL 8.0 / MariaDB 11.4. — #353, #296
  • {{ServerMajorVersion}} and {{CompatibilityLevel}} script tokens for version-gating. Two automatic tokens expose the target version SchemaSmith already detects, so a ShouldApplyExpression (folder, component, or the per-script sentinel) or a script body can gate on version with one portable integer comparison — {{CompatibilityLevel}} >= 130, {{ServerMajorVersion}} >= 16 — instead of hand-writing each engine's native version predicate. They resolve per target database, wherever template-scoped tokens resolve (script bodies and the Default/CheckExpression/Expression/FilterExpression/ShouldApplyExpression fields). The pair separates a real footgun: a modern binary can host a database left at an old compatibility level, where compat-gated syntax (OPENJSON, STRING_AGG, STRING_SPLIT, TRY_CONVERT) parse-errors even though the server is new — so gate syntax on {{CompatibilityLevel}}, gate features on {{ServerMajorVersion}}. CompatibilityLevel is a SQL Server concept; on PostgreSQL, MySQL, and MariaDB it resolves to the same value as {{ServerMajorVersion}} so one expression shape stays portable. Cross-platform (SQL Server, PostgreSQL, MySQL, MariaDB).
  • XML twins for every model-payload script token — shred the model as XML on the legacy SQL Server tier. The model-payload tokens ({{TableSchema}} / {{IndexedViewSchema}} / {{MaterializedViewSchema}}, their _<TemplateName> cross-template forms, and the <*SpecificTable*> / <*SpecificIndexedView*> / <*SpecificMaterializedView*> per-object tags) hand your script JSON, which OPENJSON can only shred at SQL Server compatibility level 130+. Each now has an always-present XML twin carrying the same model as ingest XML, shreddable with XQuery .nodes()/.value() at every compatibility level: {{TableXml}} / {{IndexedViewXml}} / {{MaterializedViewXml}} (+ _<TemplateName> forms) and the <*SpecificTableXml*> / <*SpecificIndexedViewXml*> / <*SpecificMaterializedViewXml*> tags. Pair the two forms behind version-gated script variants (ShouldApplyExpression + {{CompatibilityLevel}}) — JSON on the modern tier, XML on the legacy tier — so a self-service TableQuench/OPENJSON pattern keeps working on a below-130 database. The encoding cliff is SQL-Server-only (PostgreSQL and MySQL/MariaDB shred JSON at every supported version); the twins are produced on every engine for one portable authoring surface. Cross-platform tokens (SQL Server, PostgreSQL, MySQL, MariaDB).
  • Target:IntegratedSecurity — opt into Windows Authentication without clearing the credential. SchemaSmith selected Windows Authentication (SQL Server integrated security) only when no Target:User/Target:Password was configured, which made it impossible to switch a checked-in settings file to integrated auth by layering an override: an override cannot clear a value — on Windows, setting an environment variable to empty deletes it, leaving the file's "User" in place, and the two shells even differ (bash can pass an empty value, PowerShell cannot). Setting Target:IntegratedSecurity=true (for example SmithySettings_Target__IntegratedSecurity=true, settable from any shell) now forces integrated security, superseding any configured user/password. SQL Server only; honored by SchemaQuench, SchemaTongs, and DataTongs (SchemaTongs/DataTongs also accept the Source: form).
  • AUR (Arch Linux). Install with yay -S schemasmith-bin (or any AUR helper) — the schemasmith-bin package installs all four CLIs from the official release binaries; the PKGBUILD is updated on each release.
  • winget (Windows). Install with winget install SchemaSmith.SchemaSmith — all four CLI commands (SchemaQuench, SchemaTongs, DataTongs, SchemaShears) land on PATH. The manifest is submitted to microsoft/winget-pkgs on each release.
  • Docker images. SchemaQuench is now published as a multi-arch (linux/amd64 + linux/arm64) container image on Docker Hub (schemasmithyfree/schemaquench) and GHCR (ghcr.io/schema-smith/schemaquench) with each release. Tags: latest, X.Y.Z (immutable), X.Y, and X. Run a deploy with no .NET install — configure via SmithySettings_ environment variables or a mounted SchemaQuench.settings.json.
  • GitHub Action — SchemaSmith Deploy. A composite action for running SchemaQuench in CI/CD (WhatIf on pull requests, deploy on merge) across SQL Server, PostgreSQL, MySQL, and MariaDB. Fetches the matching self-contained binary for the runner OS at run time (no runtime install); inputs cover mode, product-path, connection settings (password passed via env), and raw extra-args, with exit-code / log-dir / summary-path outputs. Pinning @vX.Y.Z pins both the action and the CLI version it runs.
  • --WhatIfDetail controls WhatIf console verbosity. A WhatIf run prints one line per script (Would APPLY / Would SKIP / Would DELIVER), which is thorough but hard to scan on a large package. --WhatIfDetail:concise now collapses each section into a per-category count (e.g. 12 would apply, 3 would skip); normal (the default) is unchanged, and verbose is reserved for future extra detail. The switch affects only the console — the SchemaQuench - Summary.md/.json files always carry the full per-script listing. — #361
  • Pre-flight logs the detected server version and SQL Server compatibility level. SchemaQuench (per configured server) and SchemaTongs (the extraction source) now log the detected engine version — and, for SQL Server, the target database's compatibility_level — as part of the pre-flight, so a version-related diagnosis is self-evident in the run log. PostgreSQL's raw server_version_num (e.g. 160013) is normalized to its major (16) for display. Cross-platform (SQL Server, PostgreSQL, MySQL, MariaDB).
  • --Encrypt / --NoEncrypt transport-encryption switches. A first-class command-line toggle to force connection transport encryption on or off for a run, across SchemaQuench, SchemaTongs, and DataTongs. The switch is engine-aware — it sets the correct connection property for the target platform (Encrypt on SQL Server, SSL Mode on PostgreSQL, SslMode on MySQL/MariaDB) — and wins over any value in ConnectionProperties. --NoEncrypt is the escape hatch for an older or hardened SQL Server instance that classic sqlcmd reaches unencrypted but whose TLS handshake the modern client library cannot complete. Cross-platform (SQL Server, PostgreSQL, MySQL, MariaDB).
  • MariaDB — a first-class supported platform. SchemaSmith now manages MariaDB (10.6–11.x) alongside SQL Server, PostgreSQL, and MySQL, with full MySQL-equivalent coverage across SchemaQuench, SchemaTongs, and DataTongs. MariaDB is implemented as a MySQL variant — it reuses the MySQL comparison/DDL engine and adds targeted overrides only where MariaDB's metadata or DDL actually diverges: ALTER TABLE … DROP CONSTRAINT (MariaDB) vs DROP CHECK (MySQL 8.0) for check constraints, IGNORED vs INVISIBLE for hidden indexes, the IGNORED/IS_VISIBLE index-visibility metadata column, integer display-width reporting, COLUMN_DEFAULT quoting, and MariaDB 11.4's new utf8mb4_uca1400_ai_ci default collation in FK-aware data delivery. Declare "Platform": "MariaDb" in Product.json; everything else — packages, tokens, templates, fan-out, checkpoint/resume, WhatIf — works exactly as it does for MySQL. MariaDB-only features (SEQUENCE objects, system-versioned/temporal tables, native UUID) are deferred follow-ups. — #351
  • Releases now publish a CycloneDX SBOM (SchemaSmith-<version>.cdx.json) listing declared third-party dependencies with resolved licenses.
  • Release archives and packages now carry signed build-provenance attestations, verifiable with gh attestation verify <asset> --repo Schema-Smith/SchemaSmith.

Changed

  • SQL Server connections now declare Encrypt explicitly. The built SQL Server connection string previously omitted Encrypt, relying on the Microsoft.Data.SqlClient default (Encrypt=True). Connections are now built with Encrypt=True stated explicitly (unless you override it via ConnectionProperties or -NoEncrypt), so transport-security intent is declared rather than inherited from a driver default that has changed across major versions. No behavior change — connections were, and remain, encrypted by default. PostgreSQL and MySQL/MariaDB continue to follow their driver defaults; set SSL Mode / SslMode (or use -Encrypt/-NoEncrypt) to state intent there.
  • Clearer diagnostic when a target server drops mid-deploy. If a target server restarts, crashes, or runs out of memory after a deployment has started, SchemaQuench now reports that the connection to the named server was lost mid-run — an environment problem to fix and re-run, not a schema error — instead of the raw SocketException / "session is in the kill state" stack that read like a broken script or bad credentials. It is distinguished from an initial connect failure (already reported clearly by the pre-flight connection test), the failing server is named (including secondary servers on multi-target runs), and the full provider stack is preserved in the error log. Cross-platform (SQL Server, PostgreSQL, MySQL). — #355

Fixed

  • Re-deploying a MySQL or MariaDB product no longer spends minutes in foreign-key convergence. The pass that decides which declared foreign keys differ from the deployed ones read INFORMATION_SCHEMA once per declared key — two joins plus two correlated KEY_COLUMN_USAGE subqueries — and every comparison was wrapped so the server could neither push the filters down nor use an index. INFORMATION_SCHEMA is not a stored table on MySQL and MariaDB: each access re-collects metadata for the whole server, so the cost scaled with how many tables exist on the instance, not with the size of the package being deployed. A package declaring 90 foreign keys took over seven minutes to compare on a server holding 333 tables, and a busy shared instance would be slower still. This only showed on a re-deploy, since the first deploy has nothing to compare against. The deployed foreign keys are now collected once into working tables and compared from there — the same comparisons, roughly 360 metadata reads reduced to 3, and the phase completes in under two seconds on that same server. SQL Server and PostgreSQL were unaffected: their catalogs are ordinary indexed relations, so the equivalent lookups already resolve by index.
  • Re-deploying a MySQL or MariaDB product no longer spends minutes in index and table convergence, either. The same INFORMATION_SCHEMA-per-row cost the foreign-key fix above removed also lived in the index and table passes: the index-rename and modified-index detection (MissingIndexesAndConstraintsQuench, IndexOnlyQuench) read the catalog once per declared index, and the table ownership/drop reconciliation (ModifiedTableQuench) read it once per owned table — cost scaling with the number of tables on the whole instance, not the package, and surfacing only on a re-deploy. Each pass now snapshots the metadata it needs once into a working table and joins that, exactly as the foreign-key pass does, placing each snapshot to reflect the catalog state that pass must see (so a just-renamed, dropped, or recreated object is still decided correctly). Measured on a busy shared instance, the modified-index detection alone dropped from about 88 seconds to 1.5 seconds on MariaDB (and about 4 seconds to 14 milliseconds on MySQL); every drop/recreate decision was verified equivalent against live AdventureWorks before and after the change. SQL Server and PostgreSQL were unaffected — their catalogs are ordinary indexed relations. (The remaining per-row reads in MissingTableAndColumnQuench fire only during a rename or when adding new objects, never on the idempotent re-deploy, and are deliberately left unchanged.)
  • SchemaSmith now warns on command-line arguments it never reads. Two shapes were silently inert: a switch value written with a space (--report ./out/x left --report valueless, so the deployment summary landed in the executable's own directory and the named path was ignored — with no warning on an otherwise green run), and a bare switch that is not a known flag (a misspelled --TestConection ran a full deployment where a connection test was intended). Every tool (SchemaQuench, SchemaTongs, DataTongs, SchemaShears) now reports unrecognized arguments up front against its own known-flag list; an argument carrying a value (--Key=value) is always accepted, since that is a configuration override no list can anticipate. The --help listings were also completed — SchemaQuench's --report and DataTongs' --DeliveryEncoding were missing.
  • Re-deploying a MySQL or MariaDB product whose tables declare an empty OldName no longer fails on the second deploy with Duplicate entry '' for key 'PRIMARY'. A blank "OldName": "" — the common shape in SchemaTongs-extracted packages, which emit an OldName field on every table and column — was manufactured into a non-NULL identifier instead of being treated as "no rename", so SchemaSmith's rename tracking fired for it and two such tables duplicate-keyed on the second deploy once the tables already existed (the first deploy, which creates the tables, was unaffected). An empty or whitespace OldName is now normalized to "no rename" at the source, so the package stays idempotent across deploys. SQL Server and PostgreSQL were structurally unaffected — they resolve a blank OldName to a no-op via object-existence and empty-string checks respectively — and are now regression-guarded too. — #375
  • SchemaTongs extraction now preserves SQL Server system-versioning (temporal tables) on round-trip. Extracting a system-versioned (temporal) table emitted no IsTemporal and re-emitted the period columns (ValidFrom/ValidTo) as ordinary columns, so an extract → re-deploy silently dropped system-versioning — and would have double-declared the period columns. SchemaTongs now extracts a temporal table with "IsTemporal": true and omits the GENERATED ALWAYS AS ROW START/END period columns (SchemaSmith regenerates them from IsTemporal on apply), so a temporal table round-trips as temporal. SQL Server only — the only supported engine with system-versioned tables. — #369
  • Declarative OldName table and column rename now works on PostgreSQL, MySQL, and MariaDB — previously SQL Server only. Renaming a table or column by setting "OldName" in the package (so the object is renamed in place, preserving its data, instead of dropped and recreated) worked on SQL Server but failed on the other three when deploying over an existing, product-owned table. On PostgreSQL a target with a recyclebin-style CustomTableDrop hook aborted with P0001 — the drop-by-absence pass routed the renamed-away old name through the hook, which then failed on the now-missing table. On MySQL and MariaDB the rename ran too late in the pipeline (after the add-columns pass), so a carried-over or newly-added column targeted the post-rename table name before the table had been renamed into existence, aborting with Table '…' doesn't exist (error 1146). Renames now run ahead of the add-columns pass on all engines, a renamed table's prior name is excluded from drop-by-absence, and ownership tracking is reconciled to the new name — so OldName renames (including a rename that adds a column in the same deploy) apply cleanly across SQL Server, PostgreSQL, MySQL, and MariaDB. Regression-guarded with two-deploy rename tests (table, column, and rename-plus-add-column) on all four engines, including a recyclebin-hook target.
  • Declarative OldName rename now carries the table's own constraint and index renames too — cross-engine parity. When a package renames a table via OldName and, in the same deploy, also renames that table's own primary key, unique constraint, or index (the natural pairing — new object names to match the new table name), PostgreSQL aborted with 42P16: multiple primary keys for table (it added the new-named primary key before dropping the carried-over old-named one) and MySQL/MariaDB silently left the old-named unique index behind alongside the new one. Both came from the same gap: a renamed table's index/constraint ownership was still tracked under the old table name, so the old-named object was never reconciled — renamed nor dropped. Ownership is now migrated to the new table name as part of the rename, so the old-named primary key / unique constraint / index is renamed in place (or dropped and re-added) and the table converges cleanly. SQL Server already handled this (its ownership survives a rename). Foreign-key renames were already correct (they reconcile structurally). Regression-guarded with two-deploy rename-plus-constraint-rename tests on all four engines.
  • --help now states the correct default log location. The --LogPath help line said logs and backups default to the current path; the actual default is the executable's own directory. The help text now matches the behavior — redirect logs, backups, and the deployment summaries elsewhere with --LogPath:<dir> (value attached with : or =, not a space).
  • Environment-level PreventDrop no longer strips a preserved column's dependent objects (SQL Server). With PreventDrop active, removing a column from the product correctly keeps the column — but SchemaSmith still dropped that column's index, statistics, DEFAULT constraint, and any CHECK constraint referencing it, because the dependent-cleanup passes that clear the way for a column drop ran even though PreventDrop then suppressed the drop itself. The "never drop an object for being absent from the product" guarantee now holds for a preserved column and all of its dependents, and the deployment summary no longer contradicts itself (reporting the same index as both dropped and suppressed). PostgreSQL and MySQL/MariaDB were structurally unaffected — they build their column-drop set already gated, or drop dependents via DROP COLUMN … CASCADE — but the scenario is now regression-guarded on all four engines. — #358
  • Deploying to a latin1 MySQL or MariaDB database no longer fails at the first table with COLLATION 'utf8mb4_unicode_ci' is not valid for CHARACTER SET 'latin1'. The shared forge reconciliation procedures applied a utf8mb4 collation directly to their stored-procedure parameters (p_DatabaseName, p_ProductName), which take the target database's character set — so on a latin1 database (MariaDB's stock compiled default) the collation was rejected and table creation failed mid-deploy, even though the forge's own tracking tables (declared utf8mb4-explicit) kindled fine. Those parameters are now converted to utf8mb4 before the collation is applied at every site, so a latin1 target database deploys cleanly. — #359
  • Data delivery to a MySQL/MariaDB table with a latin1 key column no longer fails with COLLATION 'utf8mb4_unicode_ci' is not valid for CHARACTER SET 'latin1'. A second instance of the same class of bug as #359: when delivering table data with a merge type that includes Delete (full-sync), the generated DELETE … WHERE NOT EXISTS (…) key-match forced COLLATE utf8mb4_unicode_ci onto the key column based only on its data type, without checking its actual character set — so a latin1 (or legacy 3-byte utf8mb3) key column aborted that table's data delivery with error 1253. The key comparison now transcodes both sides with CONVERT(… USING utf8mb4) before applying the collation, so data delivery works on latin1/utf8mb3-keyed tables while still resolving the utf8mb4 collation mix it was added for. Regression-guarded with a latin1-keyed full-sync-delete test on MySQL and MariaDB. — #373
  • Fresh deploy on PostgreSQL 17 no longer fails with a bare ALTER TABLE (42601 syntax error) on identity-only tables. A GENERATED ALWAYS AS IDENTITY column is extracted with its (START WITH … INCREMENT BY …) sequence suffix, which the read-back strips — so the column was perpetually flagged "modified", but the only identity modification handled is removal, so its ALTER clause was empty. When such a column was the only flagged column on a table (identity plus plain columns, nothing else to change — the shape common to reference schemas like Chinook), the generated ALTER TABLE had an empty body and failed with 42601 syntax error at end of input. Exposed on PostgreSQL 17, where the pre-17 generated-column recreate path no longer runs. ModifiedTableQuench now emits nothing for a table with no real column changes instead of a bare header; SQL Server and MySQL/MariaDB were unaffected. — #356
  • WhatIf now previews engine-generated table-structure changes in the deployment summary. In WhatIf mode the summary's objectChanges block (and its details[]) was empty even when the run detected structural changes it would apply — creating a table, adding/altering/dropping a column, or reconciling an index, constraint, or foreign key — because the object-change audit was written only when DDL actually executed. WhatIf runs now record wouldCreate / wouldModify / wouldDrop audit rows, mapped into the summary's created/modified/dropped counts (distinguished from a real run by the report's mode), so a WhatIf preview surfaces the structural changes it would make — the most common and most useful case for a preview. The protection-suppressed drop action was renamed internally (wouldDropdropSuppressed) so it no longer collides with the new WhatIf drop preview; the preventDrop manifest is unchanged. Also fixes a pre-existing PostgreSQL WhatIf abort (42P01 relation … does not exist) when a package adds a new table — the existing-index snapshots cast a not-yet-created table to ::regclass, now to_regclass. Cross-platform (SQL Server, PostgreSQL, MySQL, MariaDB). — #363
  • A below-floor server now fails fast with a clear message instead of a cryptic engine error. Pointing SchemaTongs (or SchemaQuench) at a below-floor server (one older than the lowered floors above) previously died deep in "kindling" with a raw 'STRING_AGG' is not a recognized built-in function name error, because the engine scripts use STRING_AGG (SQL Server 2017+, database compatibility level 140+) pervasively. SchemaSmith now enforces an intrinsic per-engine version floor (SQL Server 2008, PostgreSQL 12, MySQL 5.7, MariaDB 10.2 — the lowered floors this release ships) on every target and extraction source — independent of the opt-in Product.MinimumVersion — and aborts before kindling with "detected version … is below the minimum supported …". For SQL Server it also detects the target database's compatibility_level and reports a database left below 140 as a distinct case from a too-old server. SchemaTongs previously ran no version pre-flight at all; it now does. Surfaced by #353. Cross-platform (SQL Server, PostgreSQL, MySQL, MariaDB).
  • No misleading "Failed DataDelivery" artifact when a delivery recovers on retry. The two-pass deferred-column data delivery wrote a SchemaQuench - Failed DataDelivery <table> artifact the moment a delivery threw — but a delivery that fails an early dependency-ordering pass usually succeeds on a later retry, so a fully successful (green) deploy could still leave an alarming "Failed" artifact on disk, reading like a broken deployment. The artifact is now deferred and written only for deliveries that never recover across all retry passes; a retried-and-recovered delivery leaves none. Cross-platform (SQL Server, PostgreSQL, MySQL, MariaDB).
  • Own-server demo helpers detect a detached-database file collision. The helpers' collision guard only inspected registered databases, so a detached database (files on disk, nothing in the catalog — a user preserving their own copy to re-attach later) slipped past it, and CREATE DATABASE then died with SQL Server's cryptic error 1802 (Cannot create file … because it already exists). The SQL Server helpers now also probe the instance default data path for an orphaned <name>.mdf and surface a friendly rename hint instead — without ever touching the file, which may be your own data.
  • The deployment summary now counts added columns. A column added to an existing table was recorded in objectChanges.details[] (as created / wouldCreate) but incremented no top-line counter — the objectChanges.created bucket had no columns field, so only modified.columns appeared in the at-a-glance counts. A run that added one column and modified another read modified.columns: 1, undercounting the real column delta. created.columns is now populated (executed and WhatIf-preview alike), so the counts are complete. Pre-existing since the summary shipped (v2.3.0); surfaced while verifying the WhatIf preview fix. Cross-platform (SQL Server, PostgreSQL, MySQL, MariaDB).
  • Identifiers containing a SQL delimiter character no longer generate broken SQL. A schema, table, column, or database name containing a single quote, closing bracket ], double quote, or backtick is now correctly escaped wherever it is interpolated into generated DDL, system-catalog introspection queries, and stored-procedure calls (SchemaQuench, SchemaTongs, DataTongs). Previously such a name produced malformed SQL (a break, not an injection risk — inputs come from the trusted schema package and catalog, never end-user data). A shared Identifier.EscapeDelimited helper now applies the platform-correct delimiter doubling, and the internal QuoteIdentifier/QuoteUseDatabase helpers escape their identifiers. Cross-platform (SQL Server, PostgreSQL, MySQL, MariaDB).
  • A connection dropped mid-run no longer turns a successful deployment into a spurious failure. The end-of-run object-change audit drain (the objectChanges section of the deployment summary) is best-effort and is meant never to disrupt a deployment, but it only tolerated database errors — a connection reset or closed during the run (for example a deadlock victim, or a transient network/server blip under heavy concurrency) surfaced as a "Connection is not open" error from the drain, which runs in the deployment's cleanup path and replaced the true outcome. A broken connection during the audit drain is now tolerated and leaves the run honestly not-instrumented instead of masking the real result.
  • Concurrent multi-tenant PostgreSQL materialized-view deployments no longer intermittently fail with XX000: could not open relation with OID. v2.3.0 scoped the materialized-view drop-detection queries to each iteration's own schema, but a residual PostgreSQL relation-cache race remained under parallel schema-template fan-out — and under heavy contention it could break the connection outright. The materialized-view convergence phase now runs one deployment at a time per target database (deployments to different databases stay fully parallel), and the transient relation-cache error is retried, so parallel tenant fan-out no longer trips the race.
  • A data-delivery failure now reports its own root cause instead of a downstream symptom. When one table's delivery failed it could leave the shared connection unusable, so later tables in the same run surfaced that downstream symptom (an "open DataReader" error) rather than their own real error — masking the actual cause in the log. Each table's first failure reason is now recorded and surfaced in the permanent-failure pass; transient dependency-retry failures that later succeed stay silent, so only genuine failures are reported.