MariaDB Reference

SchemaQuench for MariaDB

The full SchemaQuench reference for MariaDB deployments — configuration, execution flow, WhatIf previews, migration tracking, checkpoint/resume, and FK-aware data delivery.

By the SchemaSmith Team · Last reviewed

SchemaQuench: a stacked database being quenched with water and ember sparks

Take your declared schema and harden it onto a live database — that's what SchemaQuench does.

Overview

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, every supported platform. The product's Platform value (SqlServer, PostgreSQL, MySQL, or MariaDb) tells SchemaQuench which adapter, which DDL flavor, and which set of helper procedures to use. Everything else looks the same.

Installation and invocation

SchemaQuench is included in the SchemaSmith distribution. Run it from the directory containing SchemaQuench.settings.json:

SchemaQuench

Common switches

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 lints the package, or validates the connection and version floor, then exits without deploying — see Pre-flight diagnostics:

SchemaQuench --Validate
SchemaQuench --TestConnection
SchemaQuench --PreviewTargets

Point SchemaQuench at a MariaDB target with --ConnectionString:

SchemaQuench --ConnectionString:"Server=db1;Database=mysql;User=deploy"

The --ConnectionString switch bypasses all Target settings and passes the value directly to the MariaDB driver.

Configuration reference

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.

Target connection settings

KeyTypeDefaultDescription
Target:Serverstring(required)Database server hostname or IP.
Target:Portstring3306TCP port for MariaDB.
Target:Userstring(required)Login username. MariaDB requires an explicit user.
Target:Passwordstring(empty)Login password.
Target:ConnectionPropertiesobject{}Arbitrary key-value pairs appended to the connection string — e.g., SslMode, AllowUserVariables, DefaultCommandTimeout.

Behavior settings

KeyTypeDefaultDescription
SchemaPackagePathstring(required)Path to the schema package directory or ZIP file.
WhatIfONLYboolfalseDry-run mode. Generates SQL without executing.
KindleTheForgebooltrueDeploy SchemaSmith helper procedures and the migration tracking table to each target database before quenching.
UpdateTablesbooltrueApply table structure changes (columns, indexes, constraints, foreign keys) from the schema package.
DropTablesRemovedFromProductbooltrueDrop tables that exist in the database but aren't defined in the schema package. Also settable as a Product.json property — see DropTablesRemovedFromProduct.
DropColumnsRemovedFromProductbooltrueDrop 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.
DeliverDatabooltrueRun 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.
RunScriptsTwiceboolfalseRun object scripts twice to verify idempotency. A CI/testing tool.
TrackRunOnceMigrationsbooltrueTrack run-once migration scripts. When false, all scripts run on every deployment.
PruneObsoleteMigrationTrackingbooltrueRemove tracking entries for scripts no longer in the package. When Target filters are active, prune is restricted to the targeted scope — see PruneObsoleteMigrationTracking.
CheckpointDirectorystring""Directory for checkpoint files used by --ResumeQuench. When blank, defaults to a per-platform temp location. See Checkpoint and Resume.
MaxThreadsint10Maximum parallel database quench operations. Range 1–20. See MaxThreads.
VerboseLoggingboolfalseInclude informational output from user scripts in logs.
ScriptTokensobject{}Config-level overrides for product script tokens.

The two drop-control flags above are shown as representative environment-tier entries. All six drop-control flags that apply on MariaDB 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.

Full settings file example

{
  "Target": {
    "Server": "localhost",
    "Port": "3306",
    "User": "deploy",
    "Password": "",
    "ConnectionProperties": {
      "SslMode": "Required"
    },
    "Templates": [],
    "Databases": []
  },
  "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": {}
}

MariaDB has no schema-inside-database concept, so the Target.Schemas filter array does not apply and is omitted here — selective scope narrows by Templates and Databases only. See Target.

For environment variable mapping, see Environment variables.

Target — selective execution scope

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.

Filter dimensions

MariaDB has no schema-inside-database concept, so there is no Target:Schemas dimension — scope narrows by template and database only.

KeyTypeDefaultDescription
Target:Templatesstring array[]Run only these templates. Empty array means no filter — all templates run.
Target:Databasesstring array[]Run only against these databases. Empty array means no filter — all discovered databases run.

The two dimensions filter AND together. Setting Target:Templates: ["TenantWorkspace"] and Target:Databases: ["tenant_newco"] runs only the TenantWorkspace template, and within that template only the iteration where the database 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.

Onboarding example

Deploy TenantWorkspace to a newly-onboarded tenant database without touching any existing tenants:

{
  "Target": {
    "Server": "production-db",
    "Templates": ["TenantWorkspace"],
    "Databases": ["tenant_newco"]
  }
}

With this configuration, SchemaQuench runs TenantWorkspace and skips every other template in TemplateOrder. Within TenantWorkspace, it runs only the tenant_newco database — tenant_acme, tenant_beta, and all other tenant databases are untouched, and their tracking rows in CompletedMigrationScripts are preserved exactly. For a full narrative walkthrough of tenant onboarding, see Multi-tenant deployments.

Prune is scoped to the filter

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 you explicitly excluded from this run. See PruneObsoleteMigrationTracking for the full rule.

TemplateTargets

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 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 tenant databases belong on that target, and SchemaQuench reconciles existence (optionally provisioning what's missing) before deploying.

{
  "Target": {
    "TemplateTargets": {
      "TenantBody": {
        "Databases": ["tenant_acme", "tenant_globex"],
        "CreateIfMissing": true
      },
      "Shared": {
        "Databases": ["tenant_acme"]
      }
    }
  }
}

Each key under TemplateTargets is a template name as declared in Product.json.TemplateOrder. On MariaDB the value carries the database axis; the schema axis does not apply (see the callout below).

Databases

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."

CreateIfMissing

Boolean. Default false. Controls what happens when an entry in Databases doesn't exist on the target server:

StateCreateIfMissing: trueCreateIfMissing: false (default)
Target existsDeploy normallyDeploy normally
Target missingProvision (DDL), then deploySkip with info log, no error

When true, SchemaQuench issues idempotent CREATE DATABASE IF NOT EXISTS against information_schema before deploying into the new target — so the credential the user supplied to SchemaQuench must carry CREATE DATABASE privilege. 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.

Validation

TemplateTargets is validated against the loaded product before any deployment work runs. Rules fail fast with a precise diagnostic naming the offending entry: unknown template name, template excluded by Target.Templates, empty entry (no Databases), Databases declared without a DatabaseIdentificationScript, and filter values composing with Target.Databases to produce an empty universe. On MariaDB, declaring a Schemas axis is itself rejected by the validation that rejects SchemaIdentificationScript on MariaDB templates. A misconfiguration cannot reach a deployment connection.

Filter composition

TemplateTargets replaces the source of a template's fan-out universe; Target.Templates / Target.Databases 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.

The schema axis does not apply on MariaDB

MariaDB has no schema-inside-database concept. TemplateTargets.<template>.Schemas is rejected on MariaDB templates by the same validation that rejects SchemaIdentificationScript on MariaDB. Use the database axis instead; multi-tenant on MariaDB is database-per-tenant.

Provisioning requires elevated privileges

CreateIfMissing: true needs CREATE DATABASE privilege — provisioning runs against information_schema. 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 the database externally; SchemaQuench will pick the target up as soon as it exists.

When to reach for it

For users who don't need declarative provisioning and are happy letting discovery scripts return the live universe, the existing DatabaseIdentificationScript (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.

MaxThreads

The MaxThreads setting controls how many database quench operations run concurrently across the entire product deployment. On MariaDB, a work unit is one database iteration — MariaDB has no schema-inside-database concept, so there are no (database, schema) work units and {{SchemaName}} is not available.

Default: 10. Range: 120.

A product whose templates fan out across many databases dispatches up to MaxThreads database quenches at once regardless of how many templates are in scope. Every template's database iterations draw from the same pool — there is no separate budget per template.

Serial queue for AllowParallel: false

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.

Failure isolation

Failure isolation is controlled by template-level flags set in Template.json, not in SchemaQuench.settings.json. On MariaDB, database-level isolation is the operative case — MariaDB has no schema templates, so the schema-level flag has no effect.

ContinueOnDatabaseFailure

Failure isolation at the database level applies to every template. 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
}

ContinueOnSchemaFailure

ContinueOnSchemaFailure governs schema-iteration failures inside a schema template. Schema templates are a SQL Server / PostgreSQL feature — MariaDB has no schema-inside-database concept and therefore no schema templates, so this flag has no effect on MariaDB. Database-level failure isolation on any MariaDB template is governed by ContinueOnDatabaseFailure above.

Deployment execution flow

When SchemaQuench runs, the product quench executes these steps in order:

  1. Log product info. Logs the product name, platform, template order, validation script, and any configured script tokens.
  2. Test server connection. Opens a connection to the target server and runs a MariaDB liveness check. Aborts if the connection fails.
  3. Validate server. If Product.ValidationScript is configured, executes it against information_schema. Aborts if the result is falsy.
  4. Validate baseline. If Product.BaselineValidationScript is configured, executes it. Aborts if the result is falsy.
  5. Product Before scripts. Executes scripts from Before Product folder(s) against the administrative connection.
  6. Quench each template. For each template name in Product.TemplateOrder:
    • Loads Template.json and merges template-level ScriptTokens over the product token set.
    • Executes DatabaseIdentificationScript against information_schema to discover target databases.
    • Creates one work unit per discovered database (MariaDB has no schema templates, so there are no (database, schema) pairs).
    • Dispatches all work units to a pool of up to MaxThreads concurrent workers. Each worker runs the full database quench sequence for its assigned database.
    • If any iteration fails, logs the failure. Failure routing follows the template's ContinueOnDatabaseFailure setting.
  7. Product After scripts. Executes scripts from After Product folder(s).
  8. Stamp product version. If Product.VersionStampScript is configured, executes it.
  9. Log completion. Logs "Completed quench of {ProductName}".

After the quench returns, the calling program backs up log files to a numbered directory and exits with code 0 (see Exit codes).

Database quench sequence

For each database identified by a template's DatabaseIdentificationScript, the database quench runs the following sequence. All steps execute on the identified database.

  1. Kindle the Forge. Deploys SchemaSmith helper procedures and the migration tracking table for MariaDB. Skipped if KindleTheForge is false.
  2. Validate baseline. Executes Template.BaselineValidationScript if configured. Aborts if falsy.
  3. Object scripts (first pass). Executes scripts from all Objects-slot folders using the dependency retry loop. If RunScriptsTwice is enabled, resets all scripts and runs a complete second pass to verify idempotency.
  4. Parse table JSON. Serializes all Tables/*.json definitions into temp/staging tables for the modular procedures to consume.
  5. MissingTableAndColumnQuench. Creates missing tables and adds missing columns.
  6. Object scripts (second opportunity). Re-attempts any Objects-slot scripts that failed in step 3, now that missing tables exist.
  7. Before scripts. Executes migration scripts from any folder in the Before slot. Sequential and tracked.
  8. ModifiedTableQuench. Alters existing columns (type changes, nullability, defaults, generated columns) and manages indexes and check constraints.
  9. Object scripts (third opportunity). Re-attempts any remaining failed Objects-slot scripts now that table modifications are complete.
  10. BetweenTablesAndKeys scripts. Executes migration scripts from any folder in the BetweenTablesAndKeys slot. Sequential and tracked.
  11. MissingIndexesAndConstraintsQuench. Creates missing indexes, check constraints, and default constraints.
  12. AfterTablesScripts. Executes migration scripts from any folder in the AfterTablesScripts slot. Sequential and tracked.
  13. AfterTablesObjects scripts. Executes scripts from AfterTablesObjects-slot folders (triggers, post-table views) using the dependency retry loop. Also retries any still-unresolved Objects-slot scripts.
  14. Table data delivery. Merges table data described by per-table 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.
  15. ForeignKeyQuench. Creates, drops, and modifies foreign keys to match the schema package.
  16. After scripts. Executes migration scripts from any folder in the After slot. Sequential and tracked.
  17. Stamp version. Executes Template.VersionStampScript if configured.

When UpdateTables is false, steps 4 through 15 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–15 still execute, with MissingIndexesAndConstraintsQuench (step 11) and ForeignKeyQuench (step 15) skipped.

Engine version compatibility

SchemaSmith deploys the same package to SQL Server, PostgreSQL, MySQL, and MariaDB — 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.

Supported engine floors

These are the minimum versions SchemaSmith supports for deployment:

PlatformMinimum supported
SQL Server2008 (major version 10)
PostgreSQL12
MySQL5.7
MariaDB10.2

These floors are enforced automatically — you do not declare anything. Before any deployment (SchemaQuench) or extraction (SchemaTongs) work begins, the target server's version is detected and logged. A below-floor server aborts the run with a clear "unsupported version" message instead of failing later with a raw engine error. MinimumVersion, below, is a separate opt-in gate for raising the floor further per product.

MinimumVersion pre-flight gate

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 MariaDB, the accepted value is the major.minor version (for example 10.6). It may be any supported version, including 10.2 to pin exactly at the floor. 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.

The detected version is reported in each engine's own form

Pre-flight logs the version it detected for every server, and — with one exception — that string is whatever the engine itself publishes; SchemaSmith does not reshape them into a common format. They report their versions in different forms, which reflects how each engine versions itself rather than an inconsistency in SchemaSmith's logging.

PlatformDetected version reads likeWhy that precision
SQL Server16.0.4260.1 (full build)Servicing level is load-bearing: CREATE OR ALTER arrived in 2016 SP1, so 13.0.4001 and 13.0.1601 are the same major and behave differently
PostgreSQL16 (major)PostgreSQL gates features on the major; the minor carries no capability difference to report. The server reports server_version_num as a packed integer (160013); SchemaSmith normalizes it to the major for display, making PostgreSQL the one engine whose logged version is not the raw string the server published
MySQL8.0.45Feature boundaries land mid-major — CHECK constraints at 8.0.16 — so a major alone cannot answer the question
MariaDB11.4.12-MariaDB-ubu2404Same reason, plus the vendor and build tail the server appends — RENAME COLUMN at 10.5.2, native UUID at 10.7

Normalizing every engine to a bare major would read tidier and tell you less. On all but one it would discard the digits SchemaSmith's own version gates turn on — servicing levels on SQL Server, mid-major feature boundaries on MySQL and MariaDB — and a degrade you needed to diagnose would become invisible in the log.

The comparison is unaffected either way: the floor check and MinimumVersion both parse each engine's own form correctly, so a passing or failing verdict never depends on how the string is printed. What you declare is a separate, friendlier grammar — 16 or 2022 on SQL Server, 15 on PostgreSQL, 8.0 on MySQL, 10.6 on MariaDB. You never have to match the detected string's shape.

Version-adaptive code generation

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. These cases apply to MariaDB, whose supported range (10.2 through current) spans versions that differ in available DDL and JSON support.

The schema model itself parses on every supported version. A version-agnostic JSON_EXTRACT shred stands in for JSON_TABLE (MariaDB 10.6 and later), so kindling and ingest do not depend on the target version.

Beyond that, SchemaSmith takes one of two paths. For a feature with an equivalent form that reaches the same end state, it uses the equivalent syntax automatically — nothing is degraded, only the implementation changes. For a feature with no equivalent, the unsupported-feature policy (Target:UnsupportedFeaturePolicy) decides what happens: the default warn applies the object without the unsupported aspect and lists each affected object under Unsupported Feature Downgrades in the deployment summary, while fail aborts instead and names the required version.

Authored featureRequiresBelow that version, SchemaSmith…
Column rename (RENAME COLUMN)MariaDB 10.5.2reproduces the rename with CHANGE COLUMN, reconstructing the current column definition (same end state)
Index rename (RENAME INDEX)MariaDB 10.5.2drops and recreates the index under the new name from its live definition (same end state)
Invisible index (IGNORED)MariaDB 10.6stores the index visible — the visibility clause is suppressed — and records a downgrade. The modified-index compare ignores the visibility difference below the floor, so re-deploys stay idempotent
Descending index key parts (… DESC)MariaDB 10.8stores the key part ascending (the engine silently does so anyway) and records a downgrade

The version-sensitive catalog reads (CHECK constraints, index visibility) are branched so they parse on the older server too, and integer display widths and foreign-key default actions are normalized on compare so an unchanged table doesn't phantom-modify across versions. The end state is identical — deploy the same package to MariaDB 10.2 through current and you get the same database, minus only the features the target genuinely cannot support, which the deployment summary names.

Native DDL on MariaDB

SchemaQuench emits MariaDB's own native DDL, and a few forms are specific to the engine. You do not configure any of this — SchemaSmith detects the target and emits the correct form automatically.

  • An index is hidden with CREATE INDEX … IGNORED, and its visibility is read back from the IGNORED column of INFORMATION_SCHEMA.STATISTICS, where 'YES' means the index is ignored.
  • A check constraint is dropped with the generic ALTER TABLE … DROP CONSTRAINT.
  • INFORMATION_SCHEMA.COLUMNS.COLUMN_DEFAULT quotes string literals, reports a literal NULL marker for a nullable column that has no default, and parenthesizes function defaults such as current_timestamp(). SchemaSmith folds these back to a canonical form on compare, so an unchanged column does not phantom-modify on every deploy.

Collation in the SQL you write on MariaDB 11.4 and later

This one is not about the DDL SchemaQuench generates — it is about comparison SQL you write, in a migration script, an After Script, or a ValidationScript. MariaDB 11.4 changed the default collation for utf8mb4 to utf8mb4_uca1400_ai_ci.

When you compare a string produced at run time (a value derived from JSON_TABLE, for instance, which takes that new default) against a column stored under a different collation, MariaDB raises Illegal mix of collations rather than coercing. Add an explicit COLLATE to one side so both operands share a collation, for example WHERE t.name = j.name COLLATE utf8mb4_general_ci. This is distinct from a latin1 target database charset, which SchemaSmith handles internally.

Quench slots

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.

Template quench slots

SlotExecution style
BeforeSequential, tracked
ObjectsDependency retry loop
BetweenTablesAndKeysSequential, tracked
AfterTablesScriptsSequential, tracked
AfterTablesObjectsDependency retry loop
TableDataDependency retry loop
AfterSequential, tracked

Product quench slots

SlotExecution style
BeforeSequential
AfterSequential

Product scripts run against the administrative connection, outside the per-database template loop.

  • Sequential, tracked — scripts run in alphabetical order and are recorded in CompletedMigrationScripts so they only run once (unless marked [ALWAYS]).
  • Dependency retry loop — scripts are retried in rounds until all succeed or no progress is made. See Dependency retry loop.

WhatIf mode

See exactly what SchemaQuench would do before it touches a single table. Set WhatIfONLY to true to perform a dry run. In WhatIf mode:

  • Validation scripts execute normally (server validation, baseline validation).
  • Table quench procedures run with p_WhatIf = 1, generating the SQL that would be executed and logging it without applying changes.
  • Migration scripts show detailed status for each script:
    • Would APPLY: {script} for scripts that haven't yet been tracked.
    • Would SKIP (previously quenched): {script} for scripts already recorded in CompletedMigrationScripts.
  • Object scripts (Objects, AfterTablesObjects, Table Data) are logged but not executed.
  • Product Before/After scripts are logged but not executed.
  • Version stamp scripts aren't executed; a log message indicates the stamp would occur.

Important limitation

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.

Debug SQL output

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}.sql
  • SchemaQuench - MissingTableAndColumnQuench {DatabaseName}.sql
  • SchemaQuench - ModifiedTableQuench {DatabaseName}.sql
  • SchemaQuench - MissingIndexesAndConstraintsQuench {DatabaseName}.sql
  • SchemaQuench - ForeignKeyQuench {DatabaseName}.sql
  • SchemaQuench - IndexOnlyQuench {DatabaseName}.sql (when IndexOnlyTableQuenches is enabled)

These files can be reviewed to understand exactly what structural changes were (or would be) made.

When to use WhatIf

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.

Pre-flight diagnostics

You don't have to quench to know whether your configuration is ready. Three read-only CLI switches run targeted diagnostics and exit without deploying anything — so a pipeline can fail fast on a broken package, a bad connection string, an unpropagated firewall rule, a below-floor server, or a target roster that resolved to the wrong set, long before the deploy window opens. They layer by what each one needs: the first reads nothing but the files on disk, the second opens a connection, and the third resolves the full target roster as well.

--Validate

SchemaQuench --Validate

--Validate is SchemaQuench's static linter: it loads your schema package through the same domain model the real quench uses, runs a battery of structural checks against it, and tells you exactly what's wrong — in seconds, from a laptop or a CI runner with no database anywhere in sight. It's the fastest, cheapest gate in the whole pre-flight family, and the only one that needs nothing but the files on disk.

Run it from the directory containing SchemaQuench.settings.json, or point SchemaPackagePath at the package you want to check:

SchemaQuench --Validate --SchemaPackagePath:./MyProduct

No Target, no --ConnectionString, no credentials of any kind — it never opens a connection. It reads Product.json to determine the declared Platform, loads the package through that platform's domain types, runs every check, prints the findings, and exits.

What it checks:

  • Package load — anything that would otherwise crash mid-deployment. A load failure short-circuits everything else, so it's the only finding you'll see on that run.
  • Duplication — two entries sharing a name at the same level, told apart from a legitimate set of conditional variants gated by ShouldApplyExpression.
  • Cross-object coherence — every foreign key and index, confirming the columns it references actually exist.
  • Token validation — every {{Token}} reference across every script and JSON file in the package, resolved as raw text.
  • Schema lint & staleness — your package against its committed .json-schemas/*.schema files, in a staleness pass followed by a structural and governance pass.
  • File naming — a table file's on-disk name against the canonical name derived from its content.

Checks run at every level a name collision could hide: columns, indexes, foreign keys, check constraints, tables within a template, the product's TemplateOrder, and MariaDB's full-text indexes.

CodeSeverityMeaning
SS-LOAD-001ErrorThe package failed to load. The message carries the underlying load error.
SS-DUP-001ErrorSame-name entries exist and at least one isn't gated by ShouldApplyExpression — an accidental duplicate.
SS-DUP-VAR-002WarningEvery entry in the group is gated (a legitimate variant set), but not every entry declares VariantName — label them for clarity.
SS-FK-001ErrorA foreign key's Columns entry names a column that doesn't exist on the local table.
SS-FK-002ErrorA foreign key's RelatedTable doesn't resolve to any known table in the package.
SS-FK-004ErrorA foreign key's RelatedColumns entry names a column that doesn't exist on the related table.
SS-FK-005ErrorColumns and RelatedColumns have different entry counts — the column lists must be the same length.
SS-IDX-001ErrorAn index's IndexColumns entry names a column that doesn't exist on the table.
SS-TOK-001ErrorA {{Token}} reference has no matching definition anywhere in the package.
SS-TOK-002ErrorA file contains an unmatched {{ with no closing }}.
SS-TOK-003WarningA ScriptTokens entry is defined but never referenced anywhere in the package.
SS-STALE-001ErrorThe committed schema no longer matches what the current domain model would generate — regenerate it.
SS-JSON-001ErrorA package JSON file violates its schema — a misnamed property, a missing required field, a value outside a declared enum, or a violation of a hand-authored Extensions governance fragment.
SS-FILE-NAME-003WarningA table file's on-disk name differs from the canonical <table>[.<VariantName>].json derived from its Name and VariantName.

A table's identity lives in its file content, never its filename, so a misnamed file still deploys correctly — file naming is a lean, not a gate. The canonical name keeps a table's conditional variants sorted together in source control and makes a file's name a reliable pointer to the table it holds. MariaDB packages carry no per-table schema, so the canonical name has no schema segment.

Type correctness is deliberately left to deployment. DataType is an open-ended field by design — it carries engine user-defined types and platform-specific aliases that are only resolvable against a real, connected engine, so a static linter has no reliable way to tell a genuine typo from a legitimate custom type it's never heard of.

Exit codes: 0 when there are no findings or warnings only, 2 on at least one Error-severity finding (including a load failure). Warnings never fail the run on their own — they're advisory.

--TestConnection

SchemaQuench --TestConnection

Opens a connection to the configured server, runs a MariaDB liveness query, and validates that the 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:

  • Connects to the configured server
  • Enforces the product's MinimumVersion floor against the server's detected version

Exit codes: 0 on pass, 2 on any connection failure or version violation.

--PreviewTargets

SchemaQuench --PreviewTargets

Everything --TestConnection does, plus a read-only per-template report of the databases the deployment would target. For each template in scope, the report lists every database 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
  db: globex_prod (would be created)

Read-only guarantee: the preview never provisions databases 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.

Not a WhatIf preview

None of the three performs WhatIf analysis (no SQL generation, no schema diff). They lint the package, validate connectivity, and enumerate targets — nothing more. For a preview of the structural changes a quench would make, use WhatIfONLY: true — see WhatIf mode.

KindleTheForge

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 MariaDB helper procedures, the modular table-quench procedures, 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.

When to set it false

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.

ForceReKindle

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.

Safe to run concurrently

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.

Re-kindle without touching config

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.

Modular quench procedures

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.

ProcedureResponsibility
MissingTableAndColumnQuenchCreates 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.
ModifiedTableQuenchAlters existing columns to match the schema package definitions. Handles data type, nullability, default constraint, and generated column changes. Drops removed tables when DropTablesRemovedFromProduct is enabled.
MissingIndexesAndConstraintsQuenchCreates indexes, check constraints, and default constraints that exist in the schema package but are missing from the database.
ForeignKeyQuenchCreates, modifies, and drops foreign keys to match the schema package. Runs late in the sequence so all referenced tables and columns exist.
IndexOnlyQuenchAlternative 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.

The implementation lives in the deployed SQL on the target database — which means a DBA can read it on the server with SHOW CREATE PROCEDURE. No black boxes.

Calling procedures directly from migration scripts

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 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*>AuditLog"
  }
}

TableQuench — ensures a specific table exists with the right structure before your migration script runs. On MariaDB the procedure is SchemaSmith_TableQuench and takes a positional DatabaseName parameter (the database the migration is running against):

-- Bootstrap the AuditLog table so we can insert into it during this migration
CALL SchemaSmith_TableQuench(
    '{{ProductName}}',
    '{{MainDB}}',
    '[{{AuditLogTable}}]',
    0,  -- p_WhatIf
    0,  -- p_DropUnknownIndexes
    0   -- p_DropTablesRemovedFromProduct
);

The {{MainDB}} token resolves to the database name the migration script is running against. You can also pass the full schema token ({{TableSchema}}) to quench all tables, but the specific-object pattern is more common in migration scripts where you need one table to exist before proceeding.

Parameter reference

ParameterTableQuench
ProductNameRequired
DatabaseName (MariaDB only)Required
Definitions (JSON)Required
WhatIfDefault: off
DropUnknownIndexesDefault: off
DropTablesRemovedFromProductDefault: on

Fill factor is not a MariaDB concept, so SchemaSmith_TableQuench on MariaDB has no UpdateFillFactor parameter — the six positional arguments above are the full signature. When to use direct calls: when a migration script needs a table to exist before it can run — for example, bootstrapping an audit table in a Before Script before inserting migration tracking data.

Migration script tracking

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:

ColumnDescription
IdAuto-increment primary key.
ProductNameThe product name from Product.json.
QuenchSlotThe slot the script belongs to.
ScriptPathThe relative path of the script within the template.
template_nameThe template the script ran under.
schema_nameAlways empty — schema templates do not apply on MariaDB.
CompletedAtTimestamp when the script was executed.

Execution rules

  • On each quench run, SchemaQuench checks which scripts in each slot have already been recorded.
  • Scripts that appear in the tracking table are skipped.
  • Scripts that don't appear are executed, and on success a tracking entry is inserted.

The [ALWAYS] suffix

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.

Ordering

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

Obsolete entry cleanup

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.

Forcing re-execution

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).

Dependency retry loop

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:

  1. Execute all pending (not yet quenched) scripts in the slot.
  2. For each script, attempt to execute all its batches. If any batch fails, record the error and move on.
  3. If at least one script succeeded in this iteration, loop back to step 1 with only the remaining failed scripts.
  4. If zero scripts succeeded in an iteration, the loop terminates.

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.

DropTablesRemovedFromProduct

When DropTablesRemovedFromProduct is true (the default), ModifiedTableQuench drops tables that:

  • Exist in the target database.
  • Aren't defined in any table JSON file in the schema package.
  • Were previously managed by this product.

This keeps the database clean as tables are removed from the schema package over time.

Three-tier cascade

The setting resolves across three tiers — environment → product → template — evaluated from broadest to narrowest:

  • EnvironmentDropTablesRemovedFromProduct in SchemaQuench.settings.json (or the SmithySettings_DropTablesRemovedFromProduct environment variable). Controls all products deployed in that environment.
  • ProductDropTablesRemovedFromProduct in Product.json. Controls a single product regardless of environment.
  • TemplateDropTablesRemovedFromProduct 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 guidance

EnvironmentSettingRationale
CI and local devtrueCatch product areas that reference tables you plan to remove.
Test/stagingtrueSame rationale, but verify the drop is intentional before promoting to production.
ProductionOften falseDropping a table is a hard drop with no built-in recovery. Teams that need rollback-friendly deployments should leave this off in production.

The rollback-friendly removal pattern

  1. Remove the table from your product definition.
  2. Keep DropTablesRemovedFromProduct: false in the production config.
  3. Write a migration script that renames or archives the table (or verifies no dependencies remain).
  4. After the retention period, either enable the setting for one deployment or add an explicit DROP in a migration script.

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 / SchemaSmith_CustomTableRestore hooks).

PreventDrop

The Drop…RemovedFromProduct flags gate the by-absence drop pass, so they only see a table whose definition is still present. Delete a table's .json and there is nothing left to carry a false — the table becomes a drop candidate. PreventDrop closes that gap: set it on a table and SchemaSmith persists the intent in the database itself, so the protection outlives the table's own definition.

It is a per-table boolean, default false. When true, the table is never dropped by absence — even after you remove it from the package entirely.

{
  "Name": "`Orders`",
  "PreventDrop": true,
  "Columns": [ /* ... */ ]
}

Sticky by design

The protection is persisted in SchemaSmith's ownership tracking, so it survives the table leaving the package. On MariaDB it is a PreventDrop column on the ProductOwnership tracking table. Each run, while the table is still in the package, SchemaSmith refreshes the marker to match the package value — so the stored protection always tracks what your JSON declares.

Removed, not dropped

When a protected table is later removed from the package, SchemaSmith reads the persisted marker, logs that it is retaining the table, and skips the drop. Its inbound foreign keys — constraints on other tables that reference the protected table — are preserved too, so the table stays fully wired into the schema rather than left as an orphan.

Not a cascade flag

Unlike DropTablesRemovedFromProduct (an environment → product → template cascade that suppresses the drop pass), PreventDrop is a positive, per-table guard that lives with the table and persists in the database. The cascade flag answers “should this deployment run the drop pass at all?”; PreventDrop answers “should this specific table ever be a drop candidate?” — and keeps answering it after the definition is gone.

Un-protecting a table

Because the marker is sticky, clearing it is a deliberate, reviewed step — you cannot un-protect a table by deleting its JSON, since that is exactly the case the stickiness defends against. Two ways:

  1. Refresh, then remove. Set PreventDrop: false and re-deploy while the table is still in the package. That run refreshes the sticky marker to false. Remove the table on a later deployment and it drops normally.
  2. Drop via migration script. Migration scripts run outside the drop-by-absence pass, so they are not gated by PreventDrop at all.

Ownership is reconciled every run

If a protected table is dropped out-of-band — by a migration script, a DBA, or a manual change — SchemaSmith prunes its ownership record, including the sticky marker, because the table no longer exists in the catalog. No stale protection lingers to confuse a future deployment; the marker only ever protects a table that is actually there.

Environment-level protection

Per-table PreventDrop protects tables one at a time. The environment-level setting is the blanket: an entire target where the deployment tool is simply not allowed to remove anything by omission — production, a shared staging fleet, anywhere an accidental drop is unacceptable.

Set PreventDrop: true in SchemaQuench.settings.json (or the SmithySettings_PreventDrop environment variable) and, for the whole run, SchemaQuench suppresses every drop-by-absence pass — tables, columns, foreign keys, check constraints, product-owned indexes, and unknown out-of-band indexes. Nothing is dropped for being absent from the product, regardless of what any package, template, or table declares. Off by default.

{
  "PreventDrop": true
}

It doesn't drop — it doesn't explode. A protected run still completes normally (exit code 0). SchemaQuench applies every additive and modifying change as usual, skips the drops, logs each one it withheld, and records them in the deployment summary under a preventDrop manifest — a precise list of what was not removed (objectType + objectName) without the run failing.

Transient drops are untouched. Protection suppresses only removal by absence. An object that is still declared but has to be dropped and recreated to apply a change — dropping an index to alter the column it covers, modifying a constraint, recreating a computed column whose expression changed — reconciles exactly as it always does.

Per-type drop protection

Beyond whole tables, SchemaQuench reconciles individual object types removed from a table's JSON — columns, foreign keys, check constraints, 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 six drop-control flags, see Drop control.

DropColumnsRemovedFromProduct

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.

DropForeignKeysRemovedFromProduct

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. On MariaDB this flag also closes a gap — foreign-key cleanup previously required enabling DropUnknownIndexes, but is now governed solely by DropForeignKeysRemovedFromProduct, matching the other engines.

DropCheckConstraintsRemovedFromProduct

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. This closes a normalization gap — previously an orphaned table-level check by absence was dropped only on PostgreSQL, and now every engine reconciles it identically.

DropIndexesRemovedFromProduct

Default true — a product-owned index (one SchemaSmith created and tracks) that dropped out of the definition is removed. Applies to secondary indexes SchemaSmith manages; a primary key is never dropped by this path. This is distinct from DropUnknownIndexes below. Every engine gates the removed-from-product drop directly through this flag — MariaDB previously coupled it to DropUnknownIndexes, so a removed index survived unless that flag was on, and is now at parity with SQL Server and PostgreSQL: a product-owned index removed from the definition is dropped by default.

DropUnknownIndexes — the out-of-band case

DropUnknownIndexes is the sixth 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.

RunScriptsTwice

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?"

When to use

  • CI pipelines — Verify that [ALWAYS] scripts are truly idempotent. If a script fails on the second run, you have caught an idempotency bug before it reaches production.
  • Local development — Verify idempotency as you author [ALWAYS] scripts.

When not to use

  • Production deployments — It doubles the execution time for the object script phase with no production benefit. This is a testing tool.

TrackRunOnceMigrations

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.

PruneObsoleteMigrationTracking

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.

With Target scope

When Target:Templates or Target:Databases 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, database) scope for each executed iteration — it does not touch rows belonging to templates or databases the filter excluded.

Why the scope restriction matters

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 database 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 and conditional deployment

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.

Folder-level gating

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 @@version LIKE '%MariaDB%' THEN 1 ELSE 0 END to branch a folder on a MariaDB-vs-MySQL target. Product folders are evaluated per server; template folders are evaluated per database, so the same folder can deploy to one target and be skipped on another in a single run.

Where the expression runs

A product-folder expression runs against the server's admin connection (information_schema), because product-level scripts are server-scoped — use server-scoped predicates there (version, edition). A template-folder expression runs against the actual target database, so it can also query target-database state.

Fails closed on error

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.

Script-level runtime skip

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.

Sentinel constant

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 MariaDB, raise it with:

SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'SCHEMASMITH: SHOULD NOT APPLY';

Batch and tracking semantics

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, so a skip in one database never affects another.

Script surface coverage

SurfaceSentinel honored
Before / After scriptsYes
Object scripts (procedures, views, functions)Yes
Migration scriptsYes
[ALWAYS] scriptsYes
Validation scriptsNo — express N/A through conditional logic inside the validation
Tool-generated SQLNo — use ShouldApplyExpression on the component

Exit codes

CodeMeaning
0Successful quench (or a passing pre-flight). All databases quenched, logs backed up.
2Failure. One or more database quenches failed; or a pre-flight found an Error-severity package finding, a connection error, a version violation, or a required-template target miss.
3Unhandled exception. An unexpected error occurred outside the normal quench flow.
4Unable to back up log files.

Engine error codes

Exit codes tell a pipeline whether the run passed. When a run fails, the engine's own error code tells you what went wrong. MariaDB prints its classic error message along with the numeric code.

FailureCode
Foreign-key violation (orphan)1452
NOT NULL violation1048
Duplicate / unique-key1062
String or binary truncation1406
Type / conversion mismatch1366
Deadlock (retried automatically)— (message-matched)

Deadlocks are retried for you — SchemaSmith detects the deadlock and re-runs the operation with backoff, so a transient lock collision resolves itself rather than failing the deploy. To recognize the same fault across engines, see per-platform error codes.

Table data delivery

Reference data doesn't have to live in a pile of hand-rolled 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 MariaDB, delivery is applied with INSERT ... ON DUPLICATE KEY UPDATE plus a conditional delete pass — MariaDB has no native MERGE, so SchemaQuench composes the equivalent idiom from primitives every MariaDB version supports. Tables without a DataDelivery block are left untouched. See Data delivery for the full property reference and worked examples.

Two-pass FK-aware delivery

Foreign keys turn "load the data" into a graph problem, which SchemaQuench solves automatically:

  1. Pass 1 — Tables whose required (NOT NULL) foreign keys all point to already-loaded tables are merged first. Nullable FK columns pointing to tables not yet delivered are deferred — the initial merge inserts rows with those columns NULL so the load doesn't block on a row that doesn't exist yet.
  2. Pass 2 — After every pass-1 table is delivered, each deferred table's nullable FK columns are back-filled by re-merging the same data with only the deferred columns in play.

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.

MergeType options

  • 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.

DataDelivery vs hand-written Table Data scripts

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.

Checkpoint and resume

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.

Enabling resume

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.

Where checkpoints live

SchemaQuench --CheckpointDirectory:/var/schemasmith/checkpoints

By default, checkpoints live in the platform's temp directory under 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.

Checkpoint scopes

SchemaQuench tracks two kinds of progress:

Product-scoped — Cross-database work shared by all templates:

  • Before and After product-level scripts.
  • Completed templates (a template with every database finished is itself recorded as complete).

Database-scoped — One checkpoint file per {product, template, server, database} combination:

Step nameWhat it covers
KindleForgeHelper procedure deployment for this database.
ValidateBaselineBaseline validation script.
MissingTablesAndColumnsAdding missing tables and missing columns.
ModifiedTablesAltering existing columns, generated columns, dropping tables.
IndexesAndConstraintsCreating missing indexes, check constraints, and defaults.
TableDataDeliveryBoth passes of FK-aware data delivery for tables with DataDelivery blocks.
ForeignKeysCreating, modifying, and dropping foreign keys.
VersionStampVersion 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.

Automatic cleanup after success

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.

Practical resume workflow

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.

When to leave resume off

  • Normal, clean deployments — The resume flag is opt-in. Without it, every step executes, and at the end the checkpoint files get cleaned up regardless. There's no cost to leaving it off for fast, green runs.
  • CI pipelines that rebuild databases from scratch — Each run is a fresh slate, so resume has nothing to do.

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.

Deployment summary report

One command fans out across dozens of tenants, and one of them comes back red. Which target? Which phase? Was it the migration script or a modified table? Did the whole run drag because a single database took ninety seconds in one slot? The deployment summary report is the machine-readable receipt for the run — every target, every timing, every failure, and every verified object change, in one structured file you can read, diff, or feed to a dashboard.

Every quench writes one. You don't ask for it, you don't switch it on — it lands next to your logs on success, on partial failure, and even when the run hard-aborts.

What SchemaQuench writes

The report is two files carrying the same run, in two shapes: a Summary.json for machines and a Summary.md for humans. Both are produced from the identical in-memory model, so they never disagree — the JSON is the contract, the Markdown is the same facts rendered to read at a glance. By default both land in the log directory alongside the run's other logs:

SchemaQuench - Summary.json
SchemaQuench - Summary.md

They're archived with the rest of the logs when a run finishes, so a report travels with the progress log, the errors log, and the failure roll-up it describes — one bundle per run, nothing to collect separately.

Redirecting the report

The default location keeps the report with its logs, which is what you want most of the time. But CI pipelines often want the summary at a known path — a build artifact to publish, a file a later step parses — independent of wherever the logs happen to rotate. The --report switch pins both files wherever you name them.

SchemaQuench --report:./artifacts/deploy-summary

That writes ./artifacts/deploy-summary.json and ./artifacts/deploy-summary.md. You give the path without an extension; SchemaQuench appends .json and .md to the base you provide. Omit the switch and both files fall back to SchemaQuench - Summary.json / .md in the log directory.

Attach the value with : or =, as with every other SchemaSmith switch — not with a space. A space-separated --report ./artifacts/deploy-summary leaves the switch with no value, so the report silently falls back to the default location instead of the path you named.

Tuning bottleneck detection

A big fan-out has a long tail. Most targets finish in a second or two; a handful crawl. The report's bottlenecks list exists to surface exactly those outliers — the individual slot-on-a-target measurements that ran long enough to be worth a look — without you scanning every timing by hand. The cutoff is one setting.

BottleneckThresholdMs sets the millisecond bar an individual slot measurement must exceed to be listed as a bottleneck. The default is 30000 (30 seconds). Lower it to catch smaller stalls on a fast fleet; raise it on a heavy release where a minute per slot is normal and you only care about the true stragglers.

SchemaQuench --BottleneckThresholdMs=10000

Set it in the settings file ("BottleneckThresholdMs": 10000), as an environment variable (SmithySettings_BottleneckThresholdMs=10000), or on the command line as above. It only governs which measurements appear in timing.bottlenecks — every slot is still timed and rolled up in bySlot and byDatabase regardless of the threshold. See Reading Summary.json for the full field reference, and objectChanges for the verified-change data.

Best-effort, and written on every exit path

Writing the report is wrapped so that a failure to assemble or serialize the summary can never disrupt the run's real logging, exit code, or control flow — a broken report never breaks a deployment. If it can't be written, you get a one-line warning in the progress log and the run proceeds exactly as it would have.

Success, partial failure, and all three hard-abort sites funnel through the same writer, and it's idempotent — an aborting run writes the report once on its way out. A run that died is exactly the run whose report you most want to read, so the report is there for it.

Reading Summary.json

The JSON is the frozen contract: camelCase keys, enum values as their names, indented for reading. Here it is end to end for a small two-tenant run, annotated — the field tables below define every key.

{
  "schemaVersion": "1.0",           // contract version of this report shape
  "tool": "SchemaQuench",
  "toolVersion": "2.4.0.0",
  "run": {
    "product": "Northwind",
    "platform": "MariaDb",           // SqlServer | PostgreSQL | MySQL | MariaDb
    "startedUtc": "2026-07-09T14:03:11.204Z",
    "finishedUtc": "2026-07-09T14:03:47.881Z",
    "durationMs": 36677,            // run wall-clock
    "mode": "Quench",               // Quench | WhatIf | Validate
    "outcome": "Success",           // Success | PartialFailure | Aborted
    "exitCode": 0,
    "resumedFromCheckpoint": false
  },
  "targets": [
    {
      "server": "primary",
      "database": "TenantA",
      "schema": null,               // MariaDB targets carry no schema
      "template": "Tenant",
      "outcome": "Success",         // Success | Failed | Skipped
      "durationMs": 14820,
      "slots": [
        {
          "slot": "ModifiedTables",
          "durationMs": 9120,
          "scriptsRun": 3
        },
        {
          "slot": "ObjectScripts",
          "durationMs": 4110,
          "scriptsRun": 12
        }
      ]
    }
  ],
  "migrationScripts": [
    {
      "path": "MigrationScripts/0007-backfill-region.sql",
      "slot": "MigrationScripts",
      "template": "Tenant",
      "schema": null,
      "server": "primary",
      "database": "TenantA",
      "outcome": "Ran"  // always "Ran" — a listed script is one that ran
    }
  ],
  "timing": {
    "totalMs": 36677,
    "bySlot": [
      { "slot": "ModifiedTables", "totalMs": 18240, "targetCount": 2 }
    ],
    "byDatabase": [
      { "database": "TenantA", "totalMs": 14820 }
    ],
    "bottlenecks": [
      {
        "scope": "[primary].[TenantA]",
        "slot": "ModifiedTables",
        "durationMs": 31210
      }
    ]
  },
  "failures": [],  // one entry per failed scope; empty on a clean run
  "whatIf": null,                   // populated only for a WhatIf-mode run
  "objectChanges": {
    "instrumented": true,
    "created": {
      "tables": 1,
      "indexes": 4,
      "constraints": 2,
      "foreignKeys": 1,
      "procedures": 0,
      "views": 0,
      "functions": 0
    },
    "modified": {
      "tables": 1,
      "columns": 3
    },
    "dropped": {
      "tables": 0,
      "indexes": 1,
      "constraints": 0,
      "foreignKeys": 0
    },
    "scriptsRan": 12,
    "details": [
      {
        "objectType": "table",
        "objectName": "Orders",
        "action": "created"
      },
      {
        "objectType": "column",
        "objectName": "sales.Orders.Region",
        "action": "modified"
      },
      {
        "objectType": "index",
        "objectName": "sales.Orders.IX_Region",
        "action": "dropped"
      },
      {
        "objectType": "procedure",
        "objectName": "Procedures/GetOrders.sql",
        "action": "ran"
      }
    ]
  }
}

Top-level fields

KeyMeaning
schemaVersionContract version of the report shape — currently "1.0".
toolAlways "SchemaQuench".
toolVersionThe CLI version that wrote the report — the same string --version prints.
runRun-level facts: product, platform, timing, mode, outcome.
targetsOne entry per (server, database, schema) target the run touched.
migrationScriptsOne entry per migration script that ran.
timingAggregate timing plus the bottleneck outliers.
failuresOne entry per failed scope — the same content as the failure roll-up log.
whatIfThe would-apply / would-skip / would-deliver plan; null unless the run was WhatIf mode.
objectChangesVerified DDL changes and object-script runs — its own section below.

run

KeyMeaning
productThe product name from Product.json.
platformSqlServer, PostgreSQL, MySQL, or MariaDb.
startedUtc / finishedUtcRun start and end, UTC.
durationMsWall-clock milliseconds for the whole run.
modeQuench (a real deploy), WhatIf (a dry run), or Validate.
outcomeSuccess, PartialFailure (some targets failed, others succeeded), or Aborted.
exitCodeThe process exit code the run returned.
resumedFromCheckpointtrue when the run resumed a prior interrupted deployment.

targets[]

KeyMeaning
server / database / schemaThe target's coordinates; schema is null when the target has no schema.
templateThe template that produced this target.
outcomeSuccess, Failed, or Skipped.
durationMsMilliseconds spent on this target.
slots[]Per-slot timing for this target: slot, durationMs, scriptsRun.

migrationScripts[]

KeyMeaning
pathPackage-relative path of the migration script.
slotThe slot it ran in.
template / schema / server / databaseWhere it ran; schema and database are null when not applicable.
outcomeAlways "Ran" — a script only appears here because it ran.

timing

KeyMeaning
totalMsRun wall-clock, matching run.durationMs.
bySlot[]Per-slot rollup across all targets: slot, totalMs, targetCount.
byDatabase[]Per-database rollup: database, totalMs.
bottlenecks[]Individual slot-on-a-target measurements exceeding BottleneckThresholdMs: scope, slot, durationMs.

failures[]

Empty on a clean run. Each entry mirrors the failure triage roll-up exactly — same content, same backup directory, no new exposure.

KeyMeaning
phaseThe phase the failure occurred in.
scopeKeyThe failed scope — a tenant, a per-server script, or a product-level phase.
errorThe engine's error text for the failure.
contextTail[]The captured tail of log lines leading up to the failure.
artifactPathPath to the resolved-SQL artifact for the failed scope, when one was written.

whatIf

null for a real quench. On a WhatIf-mode run it holds the plan, split three ways — and every entry carries a script path, never a SQL body.

KeyMeaning
wouldApply[]Changes the run would apply: scope, script.
wouldSkip[]Changes it would skip.
wouldDeliver[]Data-delivery scripts it would deliver.

objectChanges — what actually changed

Verified changes versus scripts that ran

Timing tells you where the run spent its seconds; objectChanges tells you what it did to your schema. This is the section a DBA reads after a release: how many tables were created, which columns were modified, what got dropped. But it draws a hard, honest line between changes SchemaSmith verified and scripts it merely ran — and understanding that line is the whole point of the section.

Verified counts. As the four table-quench procedures run DDL, they record each real change to a session-scoped audit that SchemaSmith drains back in-process. Those captured rows are the created, modified, and dropped counts — genuine, observed structural changes to tables, columns, indexes, constraints, and foreign keys. If the count says one table created and three columns modified, that is what happened, read back from the engine.

Scripts that ran. Object scripts — your stored procedures, views, and functions — are a different story. SchemaSmith re-applies them idempotently on every run, so a procedure script executes whether or not its body changed anything. SchemaSmith refuses to guess. It will not tell you a procedure was "created" or "modified" when all it honestly knows is that the script ran. So object scripts never touch the created/modified counts. Instead they contribute to scriptsRan (a count) and to details[] rows carrying "action": "ran".

The count buckets

BucketObject types counted
createdtables, indexes, constraints, foreignKeys, plus procedures / views / functions fields that stay 0 by design (object scripts don't count as created).
modifiedtables, columns.
droppedtables, indexes, constraints, foreignKeys.
scriptsRanTotal object scripts that ran this run.

details[]

Where the counts are the summary, details[] is the itemized list — one row per recorded change or run, each with objectType, objectName, and action. The actions you'll see are created, modified, dropped, and ran.

details[] also carries object types that have no dedicated count bucket. The verified-change audit records more kinds of object than the count fields cover, and those surface here rather than being dropped:

  • fullTextIndex — a full-text index created. MariaDB supports multiple per table.

These appear as details[] rows with their real objectType and action even though no top-level count field aggregates them — the detail is preserved even where the summary doesn't bucket it.

instrumented

ValueMeaning
trueThe run's engine produced a real audit read; the counts and details are populated.
falseThe engine couldn't read the audit (for example, kindling was suppressed), so every count is 0 and details[] is empty.

Note

instrumented: false means unknown, not nothing happened. A run whose audit couldn't be read reports honestly-empty change data rather than pretending zero changes occurred. Read the progress log for what the run actually did in that case.

Cross-platform

The report shape is identical on every supported engine — same keys, same nesting, same enum values. The platform field tells you which engine produced it, and a few details[] object types are engine-specific (the full-text index type above), but the contract is one shape everywhere. A dashboard that parses a SQL Server report parses a MariaDB one unchanged.

Why "ran", not "changed"

Reporting a re-applied procedure as "modified" every single run would be a lie that made every report look busy. An honest report says exactly what it knows: verified structural changes as counts, idempotent re-applies as "ran". When you see scriptsRan: 12, twelve object scripts executed — the report is not claiming twelve objects changed.