The full SchemaQuench reference for SQL Server deployments — configuration, execution flow, WhatIf previews, migration tracking, checkpoint/resume, and FK-aware data delivery.
By the SchemaSmith Team · Last reviewed
Take your declared schema and harden it onto a live database — that's what SchemaQuench does.
It reads a schema package, connects to the target server, and transforms each database to match the desired state. No hand-written ALTER scripts, no guessing what changed. Run it against dev, staging, and production with the same package, the same confidence, and the same boring, predictable result every time. SchemaQuench compares current state against desired state, makes only the changes necessary, and tracks migration scripts so they execute only once.
One executable, three platforms. The product's Platform value (SqlServer, PostgreSQL, or MySQL) tells SchemaQuench which adapter, which DDL flavor, and which set of helper procedures to use. Everything else looks the same.
SchemaQuench is included in the SchemaSmith distribution. Run it from the directory containing SchemaQuench.settings.json:
SchemaQuench
Point SchemaQuench at an alternate settings file or log directory:
SchemaQuench --ConfigFile:path\to\alternate.settings.json
SchemaQuench --LogPath:path\to\logs
SchemaQuench --ConfigFile:path/to/alternate.settings.json
SchemaQuench --LogPath:path/to/logs
Run a pre-flight check that validates connections and version floors, then exits without deploying — see Pre-flight diagnostics:
SchemaQuench --TestConnection
SchemaQuench --PreviewTargets
Point SchemaQuench at a SQL Server target with --ConnectionString:
SchemaQuench --ConnectionString:"Data Source=db1;User ID=sa;Password=secret;"
The --ConnectionString switch bypasses all Target settings and passes the value directly to the SQL Server driver.
SchemaQuench reads configuration from SchemaQuench.settings.json (or the file specified by --ConfigFile), environment variables with the SmithySettings_ prefix, and command-line switches. Later sources override earlier ones. For the full loading-order and precedence rules, see Configuration.
| Key | Type | Default | Description |
|---|---|---|---|
Target:Server | string | (required) | Database server hostname or IP. |
Target:Port | string | 1433 | TCP port for SQL Server. |
Target:User | string | (empty) | Login username. SQL Server allows blank for Windows auth. |
Target:Password | string | (empty) | Login password. |
Target:SecondaryServers | string | (empty) | Comma-separated list of Availability Group secondary servers to quench in parallel with the primary. See Secondary servers. |
Target:ConnectionProperties | object | {} | Arbitrary key-value pairs appended to the connection string — e.g., TrustServerCertificate, Encrypt, ApplicationIntent. |
| Key | Type | Default | Description |
|---|---|---|---|
SchemaPackagePath | string | (required) | Path to the schema package directory or ZIP file. |
WhatIfONLY | bool | false | Dry-run mode. Generates SQL without executing. |
KindleTheForge | bool | true | Deploy SchemaSmith helper procedures and the migration tracking table to each target database before quenching. |
UpdateTables | bool | true | Apply table structure changes (columns, indexes, constraints, foreign keys) from the schema package. |
DropTablesRemovedFromProduct | bool | true | Drop tables that exist in the database but aren't defined in the schema package. Also settable as a Product.json property — see DropTablesRemovedFromProduct. |
DropColumnsRemovedFromProduct | bool | true | Drop columns that exist in the database but aren't defined in the schema package. Resolves across a four-tier cascade (env → product → template → table) with explicit-false-sticky semantics. See Per-type drop protection. |
DeliverData | bool | true | Run the per-table DataDelivery step and the TableData-slot scripts. Set to false to ship a structure-only deployment that leaves reference data untouched — pairs naturally with UpdateTables: true for "deploy schema, skip data" pipelines. |
RunScriptsTwice | bool | false | Run object scripts twice to verify idempotency. A CI/testing tool. |
TrackRunOnceMigrations | bool | true | Track run-once migration scripts. When false, all scripts run on every deployment. |
PruneObsoleteMigrationTracking | bool | true | Remove tracking entries for scripts no longer in the package. When Target filters are active, prune is restricted to the targeted scope — see PruneObsoleteMigrationTracking. |
CheckpointDirectory | string | "" | Directory for checkpoint files used by --ResumeQuench. When blank, defaults to a per-platform temp location. See Checkpoint and Resume. |
MaxThreads | int | 10 | Maximum parallel work units — covers both database-level and schema-level iterations. Range 1–20. See MaxThreads. |
VerboseLogging | bool | false | Include PRINT informational output from user scripts in logs. |
ScriptTokens | object | {} | Config-level overrides for product script tokens. |
The two drop-control flags above are shown as representative environment-tier entries. All eight drop-control flags are settable here (and via SmithySettings_<FlagName> environment variables) and resolve through a tiered cascade — see DropTablesRemovedFromProduct and Per-type drop protection for the full set and semantics.
{
"Target": {
"Server": "localhost",
"Port": "",
"User": "",
"Password": "",
"SecondaryServers": "",
"ConnectionProperties": {
"TrustServerCertificate": "True"
},
"Templates": [],
"Databases": [],
"Schemas": []
},
"WhatIfONLY": false,
"SchemaPackagePath": "./MyProduct",
"KindleTheForge": true,
"UpdateTables": true,
"DropTablesRemovedFromProduct": true,
"DropColumnsRemovedFromProduct": true,
"DeliverData": true,
"RunScriptsTwice": false,
"TrackRunOnceMigrations": true,
"PruneObsoleteMigrationTracking": true,
"CheckpointDirectory": "",
"MaxThreads": 10,
"VerboseLogging": false,
"ScriptTokens": {}
}
For environment variable mapping, see Environment variables.
SQL Server deployments targeting Availability Groups can quench to a primary plus one or more secondary servers in parallel. Configure secondaries on Target:
{
"Target": {
"Server": "primary-replica",
"SecondaryServers": "secondary-1,secondary-2"
}
}
When a secondary list is configured, SchemaQuench routes each product-level folder to the right server based on its ServerToQuench setting (Primary, Secondary, or Both). Templates target the primary; product-level scripts can target either side. See Products & Templates — Platform Differences for the package side of the configuration.
Selective execution scope narrows a deployment to a subset of the work the product would otherwise perform. The most common use is deploying to a single newly-onboarded tenant without re-running the full product, canary-deploying a hotfix to one tenant to verify it before rolling out, or re-running a single template after a configuration change. Without Target, every template runs against every discovered database and schema.
| Key | Type | Default | Description |
|---|---|---|---|
Target:Templates | string array | [] | Run only these templates. Empty array means no filter — all templates run. |
Target:Databases | string array | [] | Run only against these databases. Empty array means no filter — all discovered databases run. |
Target:Schemas | string array | [] | Run only against these schema names. Empty array means no filter — all discovered schemas run. Applies only to schema-template iterations; regular-template work units bypass this filter entirely. |
The three dimensions filter AND together. Setting Target:Templates: ["TenantWorkspace"] and Target:Schemas: ["tenant_newco"] runs only the TenantWorkspace template, and within that template only the iteration where the schema is tenant_newco. Unmatched work units are skipped before any database connections open for them.
SchemaQuench validates filter values against the discovered universe before dispatching any work. A value that doesn't match anything in the discovered set fails immediately with a diagnostic that lists the available options, so a typo surfaces as a clear error rather than a silent empty run.
Deploy TenantWorkspace to a newly-onboarded tenant without touching any existing tenants:
{
"Target": {
"Server": "production-db",
"Templates": ["TenantWorkspace"],
"Schemas": ["tenant_newco"]
}
}
With this configuration, SchemaQuench runs TenantWorkspace and skips every other template in TemplateOrder. Within TenantWorkspace, it runs only the tenant_newco iteration — tenant_acme, tenant_beta, and all other tenants are untouched, and their tracking rows in CompletedMigrationScripts are preserved exactly. For a full narrative walkthrough of tenant onboarding, see Multi-tenant deployments.
When Target filters are active, PruneObsoleteMigrationTracking is restricted to the targeted scope. This is intentional — pruning tracking rows outside the targeted scope would delete correct records of migrations applied against databases and schemas you explicitly excluded from this run. See PruneObsoleteMigrationTracking for the full rule.
Target.TemplateTargets lets the deployment system own the universe a schema template fans out across, instead of asking the target server to enumerate it. A template's DatabaseIdentificationScript / SchemaIdentificationScript still defines the package's contract — this block replaces the script's result at runtime for one named template, per environment. The pattern unlocks single-canonical-package deployments where each environment's settings file declares which tenants belong on that target, and SchemaQuench reconciles existence (optionally provisioning what's missing) before deploying.
{
"Target": {
"TemplateTargets": {
"TenantBody": {
"Databases": ["tenant_acme", "tenant_globex"],
"Schemas": ["acme", "globex"],
"CreateIfMissing": true
},
"Shared": {
"Databases": ["tenant_acme"]
}
}
}
}
Each key under TemplateTargets is a template name as declared in Product.json.TemplateOrder. The value is an object with three optional properties.
String array. Replaces the result of the named template's DatabaseIdentificationScript for this run. When set, the listed databases ARE the universe — the discovery script does not run. The template must declare a DatabaseIdentificationScript in its Template.json; if you don't need real discovery, the recommended marker is "SELECT 'CONFIG-DRIVEN' AS DatabaseName WHERE 1=0" — a placeholder that returns no rows and signals "this template is database-fan-out, the universe lives in settings."
String array. Replaces the result of the named template's SchemaIdentificationScript for this run. Same shape, same recommended placeholder: "SELECT 'CONFIG-DRIVEN' AS SchemaName WHERE 1=0". When both axes are overridden on a schema template, the cross-product becomes the work-unit set: two databases × two schemas = four iterations.
Boolean. Default false. Controls what happens when an entry in Databases or Schemas doesn't exist on the target server:
| State | CreateIfMissing: true | CreateIfMissing: false (default) |
|---|---|---|
| Target exists | Deploy normally | Deploy normally |
| Target missing | Provision (DDL), then deploy | Skip with info log, no error |
When true, SchemaQuench issues idempotent per-engine DDL — the sys.schemas-guarded EXEC('CREATE SCHEMA …') pattern on SQL Server — before deploying into the new target. Database provisioning runs against master by re-targeting the connection, so the credential the user supplied to SchemaQuench must carry CREATE DATABASE privilege there. When false and a target is missing, the engine emits an info log naming the skipped target and continues with the rest of the override list; no work units run for the missing target, no error.
TemplateTargets is validated against the loaded product before any deployment work runs. Six rules fail fast with a precise diagnostic naming the offending entry: unknown template name, template excluded by Target.Templates, empty entry (no Databases and no Schemas), Schemas declared without a SchemaIdentificationScript on the template, Databases declared without a DatabaseIdentificationScript, and filter values composing with Target.Databases / Target.Schemas to produce an empty universe. A misconfiguration cannot reach a deployment connection.
TemplateTargets replaces the source of a template's fan-out universe; Target.Templates / Target.Databases / Target.Schemas still filter the result. The override produces the universe, then the filters narrow it. See Target for the filter semantics — composition is straightforward: Target.Databases keeps only entries that match its allow-list (whether those entries came from discovery or an override), and the same applies for Target.Schemas.
CreateIfMissing: true on the database axis needs CREATE DATABASE on master; on the schema axis it needs CREATE SCHEMA on the target database. A permission denial surfaces an actionable diagnostic naming the missing privilege, but the deployment fails fast at that target. If your deployment account is intentionally low-privilege, leave CreateIfMissing: false and provision externally; SchemaQuench will pick the targets up as soon as they exist.
For users who don't need declarative provisioning and are happy letting discovery scripts return the live universe, the existing DatabaseIdentificationScript / SchemaIdentificationScript (which can interpolate query-tokens, read tenant tables, or query system catalogs) remains the right tool. Reach for TemplateTargets when the deployment system needs to own the universe declaratively — typically when one canonical package ships to multiple environments with per-environment tenant rosters.
For a worked end-to-end example — single canonical package, per-region settings files, first-run provisioning, subsequent-run idempotent refresh, onboarding a new tenant — see Multi-tenant deployments — region-rotated tenant rosters.
The MaxThreads setting controls how many work units run concurrently across the entire product deployment. A work unit is one database iteration for a regular template, or one (database, schema) iteration for a schema template. All work unit types share the same pool — there is no separate budget per template type.
Default: 10. Range: 1–20.
With schema templates, a single database can contribute many work units — one per discovered schema. A product with a single TenantWorkspace template applied to one database hosting 100 tenant schemas produces 100 work units. At MaxThreads: 8, the dispatcher runs up to 8 schema iterations concurrently regardless of how many templates or databases are in scope. If you also have regular-template work units queued alongside schema-template units, they all draw from the same pool.
Templates with AllowParallel: false get their own serial queue. At most one of that template's iterations runs at a time, but other templates' parallel-eligible units continue to run concurrently alongside them. See Products & Templates — AllowParallel for the per-template parallel-disable case.
Two template-level flags decide whether one failure stops the run or is isolated so the rest proceeds. Both are set in Template.json, not in SchemaQuench.settings.json, and both default to true (continue).
Failure isolation at the database level applies to all templates — both regular templates and schema templates. When ContinueOnDatabaseFailure is true (the default), one database's failure does not abort the product run; SchemaQuench logs the failure, continues processing remaining databases, and exits with code 2 after all work units have completed or failed.
When false, the first database-level failure aborts subsequent iterations. In-flight work units drain naturally — SchemaQuench does not cancel active database connections because an incomplete transaction is more hazardous than a completed one. The product run exits with code 2.
{
"Name": "CustomerDB",
"DatabaseIdentificationScript": "...",
"ContinueOnDatabaseFailure": false
}
For schema templates, ContinueOnDatabaseFailure governs database-level failures during work unit enumeration (a bad DatabaseIdentificationScript, an unreachable server). Schema iteration failures inside a schema template are governed separately by ContinueOnSchemaFailure.
Schema templates fan out across multiple schema iterations inside a database. ContinueOnSchemaFailure controls what happens when one of those iterations fails.
When true (the default), a single schema iteration's failure does not halt the others. The failed iteration logs an error, the remaining iterations continue, and the product run exits with code 2 after all iterations have completed or failed. This is the appropriate default for production multi-tenant deployments where one tenant's problem should not block every other tenant.
When false, the first iteration failure stops the dispatcher: no new iterations start, in-flight iterations drain naturally, and subsequent templates in TemplateOrder do not run.
How failures surface. Each iteration's log lines carry a [Schema: <name>] prefix, so failures are traceable per tenant even in a parallel run. The deployment log will show the per-iteration error line for the failed schema, then continue with remaining iterations (in continue mode) or stop (in abort mode). The exit code is 2 whenever any iteration failed, regardless of mode.
ContinueOnSchemaFailure is ignored on regular templates. If set non-default on a regular template, SchemaQuench logs a warning at load time. For database-level failure isolation on any template, use ContinueOnDatabaseFailure.
When SchemaQuench runs, the product quench executes these steps in order:
Product.ValidationScript is configured, executes it against master. Aborts if the result is falsy.Product.BaselineValidationScript is configured, executes it. Aborts if the result is falsy.Before Product folder(s). With secondary servers, scripts run in parallel to all eligible servers.Product.TemplateOrder:
Template.json and merges template-level ScriptTokens over the product token set.DatabaseIdentificationScript against master to discover target databases.SchemaIdentificationScript against each discovered database to produce one work unit per (database, schema) pair. For regular templates: one work unit per discovered database.MaxThreads concurrent workers. Each worker runs the full database quench sequence for its assigned iteration.ContinueOnDatabaseFailure (regular templates) or ContinueOnSchemaFailure (schema templates) settings.After Product folder(s).Product.VersionStampScript is configured, executes it.After the quench returns, the calling program backs up log files to a numbered directory and exits with code 0 (see Exit codes).
For each work unit dispatched by a template — one identified database for regular templates, one (database, schema) pair for schema templates — the database quench runs the following sequence. All steps execute on the identified database. For schema templates, the active schema name is available throughout as {{SchemaName}}.
KindleTheForge is false.Template.BaselineValidationScript if configured. Aborts if falsy.Objects-slot folders using the dependency retry loop. If RunScriptsTwice is enabled, resets all scripts and runs a complete second pass to verify idempotency.Tables/*.json definitions into temp/staging tables for the modular procedures to consume.Before slot. Sequential and tracked.BetweenTablesAndKeys slot. Sequential and tracked.AfterTablesScripts slot. Sequential and tracked.AfterTablesObjects-slot folders (triggers, DDL triggers, rules, post-table views) using the dependency retry loop. Also retries any still-unresolved Objects-slot scripts.DataDelivery blocks, ordered by foreign key dependencies. See Table data delivery. Then executes any hand-written scripts in the TableData slot using the dependency retry loop.IndexedViewQuench procedure.After slot. Sequential and tracked.Template.VersionStampScript if configured.When UpdateTables is false, steps 4 through 16 are skipped entirely. When IndexOnlyTableQuenches is enabled on a template, steps 4–8 (parse JSON, missing tables, second Objects pass, Before scripts, modified tables) are replaced by a single call to the IndexOnlyQuench procedure. Steps 9–16 still execute, with MissingIndexesAndConstraintsQuench (step 11) and ForeignKeyQuench (step 15) skipped.
SchemaSmith deploys the same package to SQL Server, PostgreSQL, and MySQL — and within each platform, it adapts to the target server's version rather than demanding uniformity. You declare one package; SchemaQuench detects the engine version of each target and does the right thing for that target. When you need to enforce a version floor, declare it once in Product.json.
These are the minimum versions SchemaSmith supports for deployment:
| Platform | Minimum supported |
|---|---|
| SQL Server | 2017 (major version 14) |
| PostgreSQL | 15 |
| MySQL | 8.0 |
You can raise the floor for a specific product by declaring MinimumVersion in Product.json. Before any deployment work begins, SchemaQuench detects the version of every resolved target. If any target is below the declared floor, the entire run aborts with a manifest naming each below-floor server and its detected version. Nothing is deployed — no partial work, no side effects on any target.
For SQL Server, the accepted value is the major version (16) or the marketing year (2022, 2019, 2017). If a target's version cannot be determined, that is a hard error — SchemaQuench never deploys blind against an unknown version. An unparseable MinimumVersion value fails at startup before any connections open. See Products & Templates — Product.json for the accepted formats on every platform.
When the supported range across your targets diverges, SchemaSmith adapts the DDL it generates automatically — there is nothing to configure; you deploy the same package to older and newer engine versions and SchemaSmith picks the right form for each target. The specific version-branching cases it handles today — in-place vs. drop-and-re-add generated columns, and single-statement vs. two-step delete-on-absence — apply to PostgreSQL, whose supported range spans versions with different available DDL. See the PostgreSQL SchemaQuench reference for the version-adaptive table.
SchemaQuench assigns every script folder to a quench slot that determines when the folder's scripts execute and how they are handled. The slot list is the same on every platform; the default folders vary by platform.
| Slot | Execution style |
|---|---|
Before | Sequential, tracked |
Objects | Dependency retry loop |
BetweenTablesAndKeys | Sequential, tracked |
AfterTablesScripts | Sequential, tracked |
AfterTablesObjects | Dependency retry loop |
TableData | Dependency retry loop |
After | Sequential, tracked |
| Slot | Execution style |
|---|---|
Before | Sequential |
After | Sequential |
Product scripts run against the administrative connection, outside the per-database template loop.
CompletedMigrationScripts so they only run once (unless marked [ALWAYS]).See exactly what SchemaQuench would do before it touches a single table. Set WhatIfONLY to true to perform a dry run. In WhatIf mode:
@WhatIf = 1, generating the SQL that would be executed and logging it without applying changes.Would APPLY: {script} for scripts that haven't yet been tracked.Would SKIP (previously quenched): {script} for scripts already recorded in CompletedMigrationScripts.WhatIf shows the top level of changes, not the full cascade. Because nothing actually executes, WhatIf can't show ripple effects that depend on earlier changes having been applied. For example, if an object script drops an index, that script doesn't run in WhatIf mode, so the index still exists when WhatIf analyzes table changes — meaning the table diff won't show the index as needing to be recreated. WhatIf is a confidence check, not a guarantee. It catches the majority of issues but the full deployment may produce additional changes that WhatIf couldn't predict.
During both normal and WhatIf runs, SchemaQuench writes the SQL generated by the table quench process to files in the working directory:
SchemaQuench - ParseJson {DatabaseName}.sqlSchemaQuench - MissingTableAndColumnQuench {DatabaseName}.sqlSchemaQuench - ModifiedTableQuench {DatabaseName}.sqlSchemaQuench - MissingIndexesAndConstraintsQuench {DatabaseName}.sqlSchemaQuench - ForeignKeyQuench {DatabaseName}.sqlSchemaQuench - IndexedViewQuench {DatabaseName}.sqlSchemaQuench - IndexOnlyQuench {DatabaseName}.sql (when IndexOnlyTableQuenches is enabled)These files can be reviewed to understand exactly what structural changes were (or would be) made.
Reach for WhatIf while you're debugging a tricky deployment or while you're still building confidence with the tooling. Inspect the generated SQL, confirm the changes match intent, then run for real. Once you trust the package and the pipeline, direct quenches are the normal mode — WhatIf isn't a required gate on every deployment.
You don't have to quench to know whether your configuration is ready. Two CLI switches run targeted diagnostic passes against a live server and exit before touching any schema — so you can validate connectivity, version constraints, and per-environment target rosters as early as your pipeline allows, without deploying a single byte.
SchemaQuench --TestConnection
Opens a connection to every configured server (primary plus any Target:SecondaryServers), runs a SQL Server liveness query, and validates that each server meets the product's declared MinimumVersion floor (if one is set). Nothing is deployed. No schema is read, no helper procedures are installed, no migration scripts are touched.
Use this in your pipeline's readiness check before you commit to a full deployment window — catch a bad connection string, a firewall rule that didn't propagate, or a server below your version floor before the quench itself begins.
What it validates:
MinimumVersion floor against every server's detected versionExit codes: 0 on pass, 2 on any connection failure or version violation.
SchemaQuench --PreviewTargets
Everything --TestConnection does, plus a read-only per-template report of the databases and schemas the deployment would target. For each template in scope, the report lists every (database, schema) work unit that would run — exactly what the full quench would touch, without touching any of it.
This is the right tool before a large fan-out deployment, before onboarding a new environment, or any time you want human eyes on the scope before committing to the run. The preview respects the same Target filters and TemplateTargets overrides a real deployment would use.
What it shows:
Template: TenantWorkspace [required]
db: acme_prod
schemas: acme, acme_reporting
db: globex_prod (would be created)
schemas: globex
Read-only guarantee: the preview never provisions databases or schemas and never deploys DDL. A database entry labeled (would be created) means CreateIfMissing: true is configured for that entry in TemplateTargets and the database does not yet exist on the server — the preview reports the intent without acting on it.
RequireAtLeastOneTarget enforcement: if a template has RequireAtLeastOneTarget: true and discovery or filtering produces zero targets, the preview fails with a FAIL result and exit code 2 — the same enforcement that applies at quench time, caught here before any deployment begins. Exit codes: 0 on pass, 2 on any connection failure, version violation, or required-template match failure.
Neither switch performs WhatIf analysis (no SQL generation, no schema diff). They validate connectivity and enumerate targets only. For a preview of the structural changes a quench would make, use WhatIfONLY: true — see WhatIf mode.
Before SchemaQuench can shape your database, it needs its tools in place. KindleTheForge deploys the SchemaSmith infrastructure to each target database. The infrastructure includes the SQL Server helper functions, the modular table-quench procedures, the IndexedViewQuench procedure, the reverse-engineering procedures used by SchemaTongs, and the CompletedMigrationScripts tracking table.
KindleTheForge runs on every quench, but the install itself is version-stamped and self-skipping: SchemaSmith records a content-hash stamp of the helper objects in each target database and the call returns immediately when the stamp matches the current tooling — so a normal deployment pays the install cost only when the tooling actually changes. In a normal release pipeline, always leave this true. See ForceReKindle for the override that re-installs unconditionally.
Data-fix and patch deployments turn this off so the run can't alter structure. See Data fixes — the datafix profile for the full flag combination and rationale.
Default false. SchemaSmith records a content-hash stamp of the helper procedures and tables it installs in each target database. On every subsequent run it compares the stamp to the current tooling and skips the re-install when nothing has changed, so a normal deployment pays the helper-install cost only when the tooling actually moves. ForceReKindle overrides that skip and re-installs the helper objects unconditionally — handy after a manual edit to the helpers, when diagnosing a deploy problem, or any time you want a known-good baseline regardless of stamp state.
Set it in SchemaQuench.settings.json, or pass --ForceReKindle on the command line (presence enables it, no value needed). When both are present the CLI switch wins.
Forcing a re-kindle is safe to run concurrently. SchemaSmith serializes the helper re-install per database with a session lock, so parallel deployments don't collide even when every one of them is forcing.
If you can't change the configuration or CLI invocation but still need a re-kindle, dropping the SchemaSmith.KindleStamp marker table has the same effect — the gate sees the missing stamp on the next run and re-installs. (On MySQL the table is SchemaSmith_KindleStamp.)
The table quench is broken into modular stored procedures, each handling a specific aspect of the table schema. The procedures are deployed during the KindleTheForge step and called in sequence during the database quench.
| Procedure | Responsibility |
|---|---|
| MissingTableAndColumnQuench | Creates tables that exist in the schema package but not in the database. Adds columns that exist in the table definition but are missing from the existing table. |
| ModifiedTableQuench | Alters existing columns to match the schema package definitions. Handles data type, nullability, default constraint, and computed column changes. Drops removed tables when DropTablesRemovedFromProduct is enabled. |
| MissingIndexesAndConstraintsQuench | Creates indexes, check constraints, default constraints, and statistics that exist in the schema package but are missing from the database. |
| ForeignKeyQuench | Creates, modifies, and drops foreign keys to match the schema package. Runs late in the sequence so all referenced tables and columns exist. |
| IndexOnlyQuench | Alternative to the full sequence. Manages indexes only — doesn't create tables, add columns, or manage foreign keys. Used when IndexOnlyTableQuenches is enabled on a template. |
| IndexedViewQuench | Deploys indexed views with diff-based change detection. |
The implementation lives in the deployed SQL on the target database — which means a DBA can read it on the server with sp_helptext. No black boxes.
The quench procedures are deployed to the target database during the KindleTheForge step and remain there afterward. You can call them directly from Before Scripts, After Scripts, or any migration script to bootstrap specific tables or views as part of a data migration.
The typical pattern uses specific-object tokens to quench individual objects that your migration script depends on, rather than passing the entire schema. First, define a token in your Product.json or Template.json:
{
"ScriptTokens": {
"AuditLogTable": "<*SpecificTable*>dbo.AuditLog"
}
}
TableQuench — ensures a specific table exists with the right structure before your migration script runs:
-- Bootstrap the AuditLog table so we can insert into it during this migration
EXEC SchemaSmith.TableQuench
@ProductName = '{{ProductName}}',
@TableDefinitions = '[{{AuditLogTable}}]',
@WhatIf = 0,
@DropUnknownIndexes = 0,
@DropTablesRemovedFromProduct = 0,
@UpdateFillFactor = 1;
The same pattern works for views. Define the token, then pass it to the procedure:
{
"ScriptTokens": {
"OrderSummaryView": "<*SpecificIndexedView*>dbo.vw_OrderSummary"
}
}
IndexedViewQuench:
EXEC SchemaSmith.IndexedViewQuench
@ProductName = '{{ProductName}}',
@IndexedViewSchema = '[{{OrderSummaryView}}]',
@WhatIf = 0,
@UpdateFillFactor = 0;
You can also pass the full schema tokens ({{TableSchema}}, {{IndexedViewSchema}}) to quench all objects of that type, but the specific-object pattern is more common in migration scripts where you need one table or view to exist before proceeding.
| Parameter | TableQuench | IndexedViewQuench |
|---|---|---|
| ProductName | Required | Required |
| Definitions (JSON) | Required | Required |
| WhatIf | Default: off | Default: off |
| DropUnknownIndexes | Default: off | — |
| DropTablesRemovedFromProduct | Default: on | — |
| DropColumnsRemovedFromProduct | Default: on | — |
| UpdateFillFactor | Default: on | Default: off |
When to use direct calls: When a migration script needs a table or view to exist before it can run — for example, bootstrapping an audit table in a Before Script before inserting migration tracking data, or ensuring an indexed view is deployed before populating dependent tables.
SchemaQuench remembers what it has already run, so you never have to worry about a migration script executing twice. Migration scripts (scripts in the Before, BetweenTablesAndKeys, AfterTablesScripts, and After slots) are tracked in the SchemaSmith.CompletedMigrationScripts table:
| Column | Description |
|---|---|
ProductName | The product name from Product.json. |
QuenchSlot | The slot the script belongs to. |
ScriptPath | The relative path of the script within the template. |
QuenchDate | Timestamp when the script was executed. |
Scripts with [ALWAYS] in the filename (before the .sql extension) run on every quench regardless of tracking:
001_SeedReferenceData [ALWAYS].sql
002_RefreshPermissions [ALWAYS].sql
[ALWAYS] scripts are never recorded in the tracking table.
Migration scripts within each slot execute in alphabetical order by filename. Use numeric prefixes to control execution order:
001_CreateStagingTable.sql
002_MigrateData.sql
003_DropStagingTable.sql
When SchemaQuench processes a slot, it compares the tracking table entries against the scripts currently present in the package. Entries for scripts that no longer exist in the package are automatically removed.
To force a tracked script to run again, either delete the corresponding row from SchemaSmith.CompletedMigrationScripts in the target database, or rename the script file (tracking is by path, so a renamed script is treated as new).
You shouldn't have to name your files in dependency order just so they deploy correctly. Scripts in the Objects, AfterTablesObjects, and TableData slots execute using a dependency retry loop rather than simple sequential execution:
On the final attempt (the last pass when errors are reported), failures are logged as errors and the quench fails.
This mechanism allows scripts with interdependencies to coexist in the same folder without requiring a specific naming order. For example, if View B references View A and is alphabetically first, it will fail on the first pass but succeed on the retry after View A has been created.
The Objects slot gets four opportunities to resolve: (1) before the table quench, (2) after missing tables are created, (3) after table modifications are complete, and (4) during the AfterTablesObjects pass alongside triggers. This handles cases where a view or function references a table column that doesn't yet exist on the first pass.
When DropTablesRemovedFromProduct is true (the default), ModifiedTableQuench drops tables that:
This keeps the database clean as tables are removed from the schema package over time.
The setting resolves across three tiers — environment → product → template — evaluated from broadest to narrowest:
DropTablesRemovedFromProduct in SchemaQuench.settings.json (or the SmithySettings_DropTablesRemovedFromProduct environment variable). Controls all products deployed in that environment.DropTablesRemovedFromProduct in Product.json. Controls a single product regardless of environment.DropTablesRemovedFromProduct in Template.json. Controls a single template within a product.Explicit false is sticky. A false set at any tier locks the effective value to false for all lower tiers — a true below can never override an ancestor's false. Absent means inherit. So an environment (or product) that sets false is a hard guardrail that suppresses the drop pass regardless of what any lower tier declares. Unlike the per-type flags, DropTablesRemovedFromProduct has no table tier — see Per-type drop protection.
| Environment | Setting | Rationale |
|---|---|---|
| CI and local dev | true | Catch product areas that reference tables you plan to remove. |
| Test/staging | true | Same rationale, but verify the drop is intentional before promoting to production. |
| Production | Often false | Dropping a table is a hard drop with no built-in recovery. Teams that need rollback-friendly deployments should leave this off in production. |
DropTablesRemovedFromProduct: false in the production config.For an alternative that keeps auto-drops on while still protecting data, see Recyclebin — soft-drop and restore hooks (drop-but-recoverable via the SchemaSmith.CustomTableDrop / CustomTableRestore hooks).
Beyond whole tables, SchemaQuench reconciles individual object types removed from a table's JSON — columns, foreign keys, check constraints, statistics, and indexes. Each has its own Drop…RemovedFromProduct flag, all default true (except DropUnknownIndexes), and each gates only removal by absence: an object whose definition merely changed is always dropped and recreated so the new definition takes effect.
Four-tier cascade — environment → product → template → table. The per-type flags add a fourth, table-level tier that DropTablesRemovedFromProduct does not have: a table's own .json can tighten a flag to false to protect its objects even when higher tiers permit drops — but it can only tighten, never re-enable. Explicit false stays sticky at every tier. For the full cascade across all eight drop-control flags, see Drop control.
Default true — columns absent from the schema package are dropped, keeping the deployed table in sync with the product definition. Set it false when the drop is unsafe: a production column other systems still read, a column you want to retire gradually with a migration script, or any environment where you want human review before structural column removal. Before this flag existed, the only way to suppress column-drop-by-absence was UpdateTables: false, which also blocks column additions and type changes; this is the narrower knob.
Default true — foreign keys absent from the schema package are dropped. Set it false to preserve an out-of-band constraint or require review before removal. Only by-absence removal is gated: a modified foreign key — one whose name still appears in the product but whose columns, referenced table/columns, or ON DELETE / ON UPDATE action changed — is always dropped and recreated regardless of this flag.
Default true. Governs table-level CHECK constraints (the CheckConstraints array). A column-level check driven by a column's CheckExpression is reconciled by the column passes, not this flag. Only by-absence removal is gated; a check whose expression merely changed is always dropped and recreated.
Default true — a user-created statistics object removed from a table's JSON is dropped. Auto-created statistics are never touched, only the named statistics your product defines. Only by-absence removal is gated.
Default true — a product-owned index (one SchemaSmith created and tracks) that dropped out of the definition is removed. Applies to nonclustered indexes SchemaSmith manages; a primary key is never dropped by this path. This is distinct from DropUnknownIndexes below.
DropUnknownIndexes is the eighth drop-control flag and the only one that defaults to false. Where DropIndexesRemovedFromProduct targets indexes SchemaSmith owns, DropUnknownIndexes targets out-of-band indexes SchemaSmith never created. It defaults off because most teams adopting SchemaSmith inherit environments with years of index drift — turn it on only after every index you need is captured in your repository. See Drop control for its full cascade and adoption guidance.
When RunScriptsTwice is true, the Objects-slot scripts are executed twice in succession during step 3 of the database quench sequence. On the second pass, all scripts are reset to unquenched and processed through the dependency retry loop again. Both runs must succeed — if either fails, the deployment fails.
This is an idempotency testing tool, not a dependency resolution mechanism. Dependency resolution is already handled by the retry loop, which retries failed scripts as long as progress is being made. RunScriptsTwice answers a different question: "Can my [ALWAYS] scripts and object scripts run again safely?"
[ALWAYS] scripts are truly idempotent. If a script fails on the second run, you have caught an idempotency bug before it reaches production.[ALWAYS] scripts.When TrackRunOnceMigrations is false, SchemaQuench treats all migration scripts as if they had the [ALWAYS] suffix — no script is recorded in CompletedMigrationScripts, no script is skipped based on prior runs. Every migration script in every slot runs on every deployment.
When tracking is off, PruneObsoleteMigrationTracking is forced off regardless of its configured value.
For how this flag fits partial-package deployments, see Data fixes — the datafix profile.
When PruneObsoleteMigrationTracking is true (the default), SchemaQuench removes entries from CompletedMigrationScripts for scripts that no longer exist in the current package. This is correct for full release deployments where the package represents the complete truth.
When false, existing tracking entries are left alone regardless of what scripts are in the current package. This setting is ignored when TrackRunOnceMigrations is false (no tracking means no pruning). For how this flag fits partial-package deployments, see Data fixes — the datafix profile.
When Target:Templates, Target:Databases, or Target:Schemas is set, prune is restricted to the iterations that ran in that deployment. A prune pass only examines tracking rows that match the active (template, schema) scope for each executed iteration — it does not touch rows belonging to templates, databases, or schemas the filter excluded.
This is correctness-critical, not a limitation. Without it, a deployment scoped to tenant_newco would delete tracking rows for tenant_acme, tenant_beta, and every other schema excluded from the run — rows that are correct records of migrations already applied against scopes you explicitly chose not to touch. The boundary of the prune exactly matches the Target filter.
ShouldApplyExpression is a SchemaQuench feature that lives on the schema package side. Whenever SchemaQuench evaluates a table component that has a ShouldApplyExpression set, it resolves any tokens in the expression, runs the expression against the target database, and skips the component if the result is falsy. This means a single table file can declare components that only apply on certain databases, certain environments, or certain server versions — no per-environment file copies, no branching logic in your deployment pipeline. See Conditional application for the JSON shape and worked examples, and Custom Properties as drivers for how to drive ShouldApplyExpression values from team-defined metadata.
The same primitive works one level up: a script folder can carry a ShouldApplyExpression too. Put it on any product- or template-level folder definition (alongside ServerToQuench / QuenchSlot). Blank deploys the folder always; a non-blank expression is evaluated against the target and the folder's scripts deploy only when it returns true — false skips the entire folder (and its sub-folders), logged so you can see why.
A folder expression runs as a scalar query against the target, so write it as a SELECT that returns a boolean (or 1/0) — for example SELECT CASE WHEN SERVERPROPERTY('EngineEdition') <> 5 THEN 1 ELSE 0 END to keep a folder out of Azure SQL. Product folders are evaluated per server; template folders are evaluated per database (and per schema for schema templates), so the same folder can deploy to one target and be skipped on another in a single run.
A product-folder expression runs against the server's admin connection (master), because product-level scripts are server-scoped — use server-scoped predicates there (server properties, version, edition). A template-folder expression runs against the actual target database (and schema for schema templates), so it can also query target-database state.
A folder's ShouldApplyExpression must return a boolean. If it errors — a SQL mistake, a missing function — the deployment fails with a clear message naming the folder, rather than silently skipping it. A gate that quietly dropped schema folders would be the dangerous failure mode, so the engine fails closed.
ShouldApplyExpression covers skip decisions a SQL expression can make from outside the script. When the decision requires logic that can only run from inside the script — querying row state, checking role membership, branching on a result from a prior batch — the script raises a sentinel error instead. SchemaQuench recognizes the sentinel as an intentional skip, logs it, and continues the deployment without an error.
SCHEMASMITH: SHOULD NOT APPLY
The match is trimmed and case-insensitive. The message must be the entire error message — an unrelated error that merely contains the phrase does not trigger a skip. Any error with a different message still surfaces as a real failure. On SQL Server, raise it with:
RAISERROR('SCHEMASMITH: SHOULD NOT APPLY', 16, 1)
The sentinel may appear in any batch of a multi-batch script, not only at the top. When it fires, SchemaQuench stops processing the remaining batches. Earlier batches that already ran are committed — the engine does not wrap the script in a transaction, so the user owns the partial-work semantics.
A migration script (in the Before, BetweenTablesAndKeys, AfterTablesScripts, or After slot) that raises the sentinel is recorded in CompletedMigrationScripts as completed — it will not be retried on the next deployment. Tracking is per-database and per-schema, so a skip in one database never affects another.
| Surface | Sentinel honored |
|---|---|
| Before / After scripts | Yes |
| Object scripts (procedures, views, functions) | Yes |
| Migration scripts | Yes |
[ALWAYS] scripts | Yes |
| Validation scripts | No — express N/A through conditional logic inside the validation |
| Tool-generated SQL | No — use ShouldApplyExpression on the component |
RAISERROR at severity ≤ 10 is an informational message — SchemaQuench does not see it and the script continues executing. Severity 16 is the conventional choice.
| Code | Meaning |
|---|---|
0 | Successful quench (or a passing pre-flight). All databases quenched, logs backed up. |
2 | Failure. One or more database quenches failed; or a --TestConnection / --PreviewTargets pre-flight found a connection error, version violation, or required-template target miss. |
3 | Unhandled exception. An unexpected error occurred outside the normal quench flow. |
4 | Unable to back up log files. |
Reference data doesn't have to live in a pile of hand-rolled MERGE scripts. Each table that participates carries a DataDelivery block in its JSON; SchemaQuench walks every table JSON, keeps the ones that declare delivery, orders them by foreign key dependencies, and merges each one — on SQL Server, with the native MERGE statement. Tables without a DataDelivery block are left untouched. See Data delivery for the full property reference and worked examples.
Foreign keys turn "load the data" into a graph problem, which SchemaQuench solves automatically:
A circular dependency among NOT NULL foreign keys fails the dependency sort — SchemaQuench logs the cycle and the quench fails. Make one side of the cycle nullable so delivery can break the loop.
Insert — missing rows inserted; existing and extra rows left alone. The seed-data pattern.Insert/Update — missing rows inserted, changed rows updated; extra rows left alone. Good for reference tables environments may append to.Insert/Update/Delete — full sync: missing rows inserted, changed rows updated, and target rows not present in the source deleted. Default, and what the demo products use.You can use both. For each target database, SchemaQuench first delivers every table with a DataDelivery block in FK order, then runs any .sql files you dropped into the template's TableData-slot folders through the dependency retry loop. Use declarative DataDelivery for bulk reference data and keep the script slot for special cases — conditional seeds, one-off rebuilds, procedural loads.
Long deployments fail. Network blips, transient lock timeouts, a migration script that tripped on bad data at step 14 of 20. Without checkpointing, a failure in the final stretch means the next run starts from zero — re-running every step you've already successfully applied.
SchemaQuench writes checkpoints as it goes. Every completed quench step and every completed migration script is recorded to disk. On the next run, already-completed work is skipped and execution resumes at the first incomplete step.
SchemaQuench --ResumeQuench
With --ResumeQuench, SchemaQuench reads the existing checkpoint files (if any) and skips anything already recorded as complete. Without the switch, the resume logic is off — every step executes regardless of prior state.
SchemaQuench --CheckpointDirectory:C:\schemasmith\checkpoints
By default, checkpoints live in %TEMP%/schemaquench-checkpoints. Override with --CheckpointDirectory:<path> when you need them on a specific volume — for a CI runner with ephemeral temp storage, a shared build server, or a mounted volume that outlives the container. The directory is created if it doesn't exist.
The same value can be set in SchemaQuench.settings.json via the CheckpointDirectory key. The CLI switch wins if both are present.
SchemaQuench tracks two kinds of progress:
Product-scoped — Cross-database work shared by all templates:
Before and After product-level scripts (per server, for Availability Group deployments).Database-scoped — One checkpoint file per {product, template, server, database} combination:
| Step name | What it covers |
|---|---|
KindleForge | Helper procedure deployment for this database. |
ValidateBaseline | Baseline validation script. |
MissingTablesAndColumns | Adding missing tables and missing columns. |
ModifiedTables | Altering existing columns, computed columns, dropping tables. |
IndexesAndConstraints | Creating missing indexes, check constraints, defaults, statistics. |
TableDataDelivery | Both passes of FK-aware data delivery for tables with DataDelivery blocks. |
ForeignKeys | Creating, modifying, and dropping foreign keys. |
IndexedViewQuench | Indexed view deployment. |
VersionStamp | Version stamp script. |
In addition, each template slot (Before, Objects, BetweenTablesAndKeys, AfterTablesScripts, AfterTablesObjects, TableData, After) records the exact scripts that ran, so resumed runs skip each individual script that already succeeded.
Checkpoints exist to protect against failures. When the quench completes without error, SchemaQuench deletes every checkpoint file associated with the product. A clean run leaves no residue to mislead the next deployment. A failed run leaves the checkpoint files in place, ready for the next --ResumeQuench invocation.
A 90-minute deployment to a large production database fails at minute 75 because a migration script hit a transient deadlock. You fix the data, re-run the deployment:
SchemaQuench --ResumeQuench
SchemaQuench reads the checkpoints, sees that KindleTheForge, ValidateBaseline, missing tables, modifications, indexes, constraints, and every Objects-slot script already succeeded, logs what it's skipping, and picks up at the first incomplete step. Minutes of work instead of starting from the top.
Use --ResumeQuench when you specifically expect that a prior run may have left partial state — typically when re-running after a real failure in a non-trivial deployment.