Skip to content

SchemaSmith v2.1.0

Choose a tag to compare

@github-actions github-actions released this 22 Jun 05:04
48558c7

Added

  • Folder-level conditional deployment (ShouldApplyExpression on folders). Any product- or template-level script folder can now carry a ShouldApplyExpression — a SQL predicate evaluated against the target at deployment time. Blank deploys the folder (unchanged); a non-blank expression that returns true deploys it, false skips it (logged). The expression is arbitrary SQL in the target engine — read SERVERPROPERTY/@@version, call your own environment-type function, query a control table, or reference resolved tokens (including {{SchemaName}} on schema templates). Common uses: a MariaDB/ vs MySQL/ folder split by @@version, skipping Jobs/ on Azure SQL, or keeping TableData/TestData/ out of production. Evaluated per target; a malformed or erroring expression fails the deployment rather than silently skipping the folder. Same mental model as object-level ShouldApplyExpression, lifted to the folder. Cross-platform (SQL Server, PostgreSQL, MySQL). See ShouldApplyExpression and Conditional Deployment in the SchemaQuench reference. — #260
  • Resolved-SQL artifact on script failure. The exact token-expanded SQL the server rejected is written to a re-runnable file on disk at a configurable ArtifactPath (default: current working directory), with the path surfaced in the log — so a failed deployment is one open away from the SQL that ran. Covers user scripts, generated quench SQL, and data-delivery merges. Optional ScrubArtifacts produces a redacted variant safe to attach to support tickets and CI. Includes a new "my deployment failed — where do I start?" debugging guide. Cross-platform. — #245
  • Per-script runtime skip via sentinel. A script can decide at deploy time that it should not apply -- based on target-only state (row counts, role membership, version+edition, prior-deployment artifacts) -- without failing the deployment. Raise RAISERROR('SCHEMASMITH: SHOULD NOT APPLY', 16, 1) (SQL Server, severity ≥ 11 required), RAISE EXCEPTION 'SCHEMASMITH: SHOULD NOT APPLY' (PostgreSQL), or SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'SCHEMASMITH: SHOULD NOT APPLY' (MySQL) from any batch of the script. SchemaQuench recognizes the exact sentinel, logs the skip, and records run-once migration scripts as completed so they are not retried. Complements ShouldApplyExpression for decisions that can only be made from inside the script. — #259
  • Log hygiene for sensitive values. Logs that end up in CI artifacts, support tickets, and screenshots no longer leak secrets. Settings-echo and script-token logging now scrub any value whose name matches a built-in sensitive-name set (*Password*, *Pwd*, *Secret*, *ApiKey*, *Token*, *ConnectionString*, *Credential* — case-insensitive), rendering the value as *** while keeping the name visible. An embedded Password= / Pwd= inside any connection-string value is stripped even when the surrounding setting/token is not sensitively named. A new LogHygiene settings block tunes the behavior: LogTokens: false suppresses the entire token-logging section (one notice, no names or values), ScrubTokens / ScrubPatterns add names/patterns to scrub, and AllowTokens opts a false-positive back out. Applies to SchemaQuench, SchemaTongs, and DataTongs. See the Sensitive value masking reference. — #244
  • Schema Templates — Multi-Schema Fan-Out. Templates can now fan out across multiple schemas inside a single database via a new SchemaIdentificationScript field, with the active schema available to scripts and JSON as the {{SchemaName}} token. Common use: each tenant owns their own schema. New Template.json fields: SchemaIdentificationScript, CreateSchemaIfMissing (default false), AllowParallel (default true), ContinueOnSchemaFailure (default true). Supported on SQL Server and PostgreSQL. See the Multi-Tenant Deployments chapter and the new TenantCRM demo for the end-to-end walkthrough. Originally proposed by Christopher Baker.
  • Target.TemplateTargets — config-driven fan-out + declarative provisioning. New SchemaQuench.settings.json block under Target that REPLACES a named template's DatabaseIdentificationScript / SchemaIdentificationScript result with per-environment lists, optionally provisioning missing targets via CreateIfMissing: true. Unlocks the canonical-package-across-environments deployment pattern: one package, per-environment tenant rosters in settings, SchemaQuench reconciles existence (idempotent per-engine CREATE SCHEMA / CREATE DATABASE DDL). MySQL supported on the database axis only. See the TemplateTargets reference and the Region-rotated tenant rosters guide section. — #257
  • ForceReKindle — force re-install of helper objects. New SchemaQuench.settings.json setting (default false) and --ForceReKindle CLI switch that bypasses the new version-stamp skip and re-installs the SchemaSmith helper procedures/tables unconditionally. Useful after a manual edit to the helper objects or when diagnosing a kindle problem; normal deployments leave it off and pay the kindle cost only when the tooling actually changes.
  • ContinueOnDatabaseFailure setting. Failure-isolation parity at the database level on regular templates. Default true matches existing behavior.
  • Target — Selective Execution Scope. New Target:Templates, Target:Databases, and Target:Schemas array filters in SchemaQuench.settings.json. Common use: deploy to a single newly-onboarded tenant without re-running the full product. PruneObsoleteMigrationTracking is restricted to the targeted scope when Target filters are active, so excluded schemas keep their tracking rows untouched.
  • Schema-Template Extraction in SchemaTongs and DataTongs. Both tools gain schema-template extraction modes via Source:Schema (both SchemaTongs and DataTongs). Source-schema-qualified references in extracted SQL bodies are rewritten to {{SchemaName}}; cross-schema references are preserved literally. Lets you cast one canonical hand-replicated schema into a schema template that fans out to the rest.
  • VariantName labels for conditional variants. Every component that carries a ShouldApplyExpression — tables, columns, indexes, foreign keys, check constraints, plus the platform-specific carriers (SQL Server statistics, XML indexes, full-text indexes, indexed views; PostgreSQL statistics, exclude constraints, materialized views; MySQL full-text indexes) — now accepts an optional VariantName. When a variant is applied, its name appears in the deployment log alongside the object (e.g. Creating index dbo.Orders.IX_Orders_Region (variant: Modern engines)), including in WhatIf output, so you can see which variant drove a change. The label documents the intent behind a variant's expression and is metadata only — it has no effect on what gets deployed. Limited to 128 characters. — #264
  • Conditional full-text index variants (SQL Server). FullTextIndex in table JSON now accepts an array of variants, each gated by a ShouldApplyExpression — one schema package can target different full-text catalogs per server, database, or region. Exactly one variant may match a target (mutually exclusive expressions are enforced); when the deployed index already matches the selected variant, re-deployment performs no full-text work. The single-object form is unchanged. — #261

Breaking Changes

  • Template.Required renamed to RequireAtLeastOneTarget. The old name read as "this template must load" but actually meant "discovery must return ≥1 database (or ≥1 (database, schema) pair for schema templates), else fail." The new name is self-describing. Unknown JSON properties are ignored at deserialization, so an unmigrated Template.json silently picks up the new property's default (true) — which surfaces as an explicit "no targets discovered for template" error rather than a silent behavior change. Migration: find-and-replace "Required": with "RequireAtLeastOneTarget": in every Template.json in your schema packages. The change applies to every platform.

Changed

  • Script failures no longer dump SQL into the log. The progress log now references the resolved-SQL artifact path instead of embedding the full batch text — the log is the shippable surface (safe to attach to tickets), the artifact is the local re-run tool. — #245
  • Migration tracking table schema. SchemaSmith.CompletedMigrationScripts gains template_name and schema_name columns. Existing rows are preserved with empty values; reads use a permissive template_name match against legacy rows so no previously-completed migrations re-run. Schema migration is idempotent and runs as part of KindleTheForge.
  • Failure scoping consolidated per template type. ContinueOnSchemaFailure now governs every failure inside a schema template (discovery, reserved-name rejection, per-iteration script failure, CREATE SCHEMA failure, dispatcher exceptions). ContinueOnDatabaseFailure now governs every failure inside a regular template. Setting ContinueOnDatabaseFailure on a schema template has no effect; setting ContinueOnSchemaFailure on a regular template has no effect. Prior behavior: the two flags layered ambiguously — a schema template's discovery failure (e.g., a reserved name like dbo returned by SchemaIdentificationScript) was incorrectly classified as a database-level failure and aborted under ContinueOnDatabaseFailure: false, even when ContinueOnSchemaFailure: true should have let it continue. The new contract is "the template's type determines which flag governs its failures" — no more cross-flag mental gymnastics.
  • Target.Templates template-name matching is now case-insensitive. The previous case-sensitive ordinal comparison was inconsistent with how Template.IsIterationScoped and token resolution already worked; the new behavior aligns the three. Users with casing typos in their Target.Templates filter list will now match instead of being silently filtered out. — #257
  • Template.CreateSchemaIfMissing: true log text unified with TemplateTargets.CreateIfMissing: true. The old shape Creating schema (CreateSchemaIfMissing=true) is now Creating schema [<name>] (CreateIfMissing: true) (per-engine quoting applied). Both paths share one DDL surface; users with log parsers depending on the old shape will see a one-time text change. — #257

Fixed

  • Product names containing an apostrophe broke deployment on SQL Server and PostgreSQL. _product.Name was interpolated as a raw SQL string literal on the SQL Server (EXEC … @ProductName = '…') and PostgreSQL (CALL …(p_ProductName := '…')) stored-procedure dispatch paths, so a product whose name contained a single quote (e.g. O'Brien's Database) terminated the literal early and failed the deployment. The MySQL dispatch path already escaped it. Product names are now escaped consistently across all three engines via the existing EscapeSqlLiteral helper. — #274
  • Template.CreateSchemaIfMissing: true now correctly previewed under WhatIf. The legacy schema-creation path executed CREATE SCHEMA against the target even when WhatIf was active. Surfaced while implementing the symmetric TemplateTargets.CreateIfMissing: true path; schema creation is now consolidated on a single SchemaProvisioner code path that respects WhatIf uniformly. — #257
  • SchemaTongs re-extraction collapsed conditional-variant sets and dropped gated-out objects. Re-extracting a product that authored multiple same-named conditional variants (e.g. an index with Modern engines and Legacy engines variants) collapsed the set to a single entry, and a component gated out on the source server was dropped entirely instead of preserved. Authored variant sets and source-gated objects now survive re-extraction intact. — #264
  • Indexed-view ShouldApplyExpression was not evaluated per-target (SQL Server). Only the literal string false gated an indexed view; any real SQL expression was ignored and the view always deployed. The expression is now resolved and evaluated against each target like every other component. — #265
  • Index-only quench ignored ShouldApplyExpression on indexes, XML indexes, and statistics (SQL Server). The index-only deployment path deployed these objects regardless of their ShouldApplyExpression; gating is now honored consistently with the full table-quench path. — #266
  • Index-only quench ignored ShouldApplyExpression on full-text indexes. The index-only deployment path deployed (or retained) full-text indexes whose ShouldApplyExpression evaluated false on the target; gating is now honored consistently with the full table-quench path. — #261
  • Same-named ShouldApply-gated objects silently dropped across all engines — When a table JSON declared two same-named objects (columns, indexes, foreign keys, check constraints) with mutually exclusive ShouldApplyExpression values — the natural "variant" pattern used to deploy different shapes to different engine versions — both rows were silently dropped during JSON parsing, and the object never landed on any engine. The per-row DELETE/UPDATE statements generated by the parser matched on the natural key (Schema/Table/Name) only, so any one row whose expression evaluated false would wipe its sibling that was supposed to survive. The parser now assigns a synthetic _RowId (SQL Server / PostgreSQL) or RowId AUTO_INCREMENT (MySQL) to each source row and scopes the per-row UPDATE/DELETE by that identifier. Fix applied uniformly across Schema/Scripts/SqlServer/ParseTableJsonIntoTempTables.sql, Schema/Scripts/PostgreSQL/ParseTableJsonIntoTempTables.sql, and Schema/Scripts/MySQL/SchemaSmith_ParseTableJson.sql; MySQL composite PRIMARY KEYs on _SchemaSmith_* temp tables were replaced with RowId PK + UNIQUE-after-filter for ONLY_FULL_GROUP_BY compatibility. Regression tests added for Columns, Indexes, ForeignKeys, and CheckConstraints on all three platforms.
  • Checkpoint-resume left SQL Server / PostgreSQL parser temp tables empty after MissingTablesAndColumns checkpointed_checkpointing.Track("MissingTablesAndColumns", …) recorded the step complete and skipped it on resume. The step parses the table JSON into session-scoped temp tables (#Tables on SQL Server, temp_tables on PostgreSQL) that don't survive across connections; on resume the next tracked steps (ModifiedTables, IndexesAndConstraints) hit downstream procs reading from those temp tables and crashed with Invalid object name '#Tables' / relation "temp_tables" does not exist. MySQL had the equivalent defense from the start (MySqlTempTablesExist + ParseMySqlTableJson re-parse inside QuenchModifiedTables / QuenchIndexesAndConstraints); SQL Server and PostgreSQL didn't. Fix drops the _checkpointing.Track wrapper around MissingTablesAndColumns so it always runs on every quench — the action is database-idempotent (the engine procs add MISSING tables/columns and no-op on existing ones), so always running is safe and cheap. Regression test added.
  • --ConnectionString override database retargeting — Per-database operations (schema discovery, per-iteration execution) now retarget the override connection string to the actual target database instead of reusing the override's embedded database (e.g., master / postgres) for every operation. Thanks to @noctelvirei. — #248
  • Completed migration script tracking SQL literals — Product names, quench slots, and script paths are now escaped before being embedded in completed-script tracking SQL. Thanks to @noctelvirei and @zacnaloen.
  • DataDelivery content file failures — Declared data delivery files now abort deployment when missing or unreadable instead of logging SKIPPING and continuing without delivering the table data. Thanks to @noctelvirei and @zacnaloen.
  • ZIP package file reads for data delivery and binary tokens — DataDelivery content files now use the package-aware file wrapper, and ZIP-backed packages can resolve binary file tokens through ReadAllBytes. Thanks to @noctelvirei and @zacnaloen.
  • Product-level script routing for SQL Server secondary servers — Product-level script folders configured for secondary servers now open the command against the routed server instead of always using the primary server connection. Thanks to @noctelvirei (first PR — welcome to the Forge!) and @zacnaloen. — #231
  • TaskQueueManager wedge on uncaught work-procedure exceptions — When a work procedure threw, the failed worker was never removed from the queue's working set, hanging WaitForAll and reducing effective capacity by one per failure. Parallel work in ProductQuench (server/database quench), Template (per-table token resolution), ScriptFolder (parallel file load), and TokenHelper (file-token resolution) could silently hang on any uncaught exception inside a work item. The worker now wraps the work procedure in try/finally so the completion handshake always runs.
  • Deadlock resilience for parallel deployments — When many schemas (or databases) deploy concurrently, the database engine can choose one iteration as a deadlock victim while it mutates the shared system catalog. SchemaSmith now recognizes the deadlock (SQL Server 1205, PostgreSQL 40P01, MySQL 1213) and automatically retries the affected table/index/constraint/view quench — which is idempotent — with backoff until it converges, instead of failing that iteration. This makes AllowParallel deployments robust at high schema/database fan-out across SQL Server, PostgreSQL, and MySQL.
  • Parallel kindle collisions on a shared database — When multiple deployments installed the SchemaSmith helper objects into the same database at the same time (parallel product loads targeting a shared admin database, or high-fan-out schema-template iterations), concurrent CREATE OR REPLACE / CREATE OR ALTER could collide and abort one of the runs. SchemaSmith now records a content-hash stamp of the kindled object set per database and installs them at most once per content-version, serialized by a session lock. Re-installs only fire when the kindle content actually changes, when the stamp is missing, or when ForceReKindle is set. — #251
  • PostgreSQL statement splitter mis-split on -- line comments — A semicolon inside a -- line comment (outside any dollar-quoted block) was incorrectly treated as a statement boundary, splitting a single PostgreSQL statement in two and breaking deployment of scripts that carried inline trailing comments. The splitter now consumes -- line comments verbatim through end-of-line, so embedded semicolons no longer terminate the statement. PostgreSQL only.