Product.json defines your schema package. Template.json targets databases within it. Together they control what gets deployed, where, and in what order.
By the SchemaSmith Team · Last reviewed
The Product.json file sits at the root of the schema package and is the top-level configuration — the starting point for every deployment.
A schema package contains two types of JSON configuration files that govern how SchemaQuench discovers, validates, and deploys your database schema:
Product.json sits at the package root. It names
the product, defines the order in which templates are processed, declares
package-wide script tokens, and optionally runs product-level scripts before
and after all template work.
Template.json lives inside each
Templates/<TemplateName>/ subfolder. It controls everything
for one logical group of databases: which databases to target, what script
folders to process, and template-specific token overrides.
Templates come in two flavors, and both work through the same execution pipeline.
A regular template fans out across databases:
DatabaseIdentificationScript returns one row per database, and
SchemaQuench runs the full template against each returned database. A
schema template fans out across schemas inside a single database:
SchemaIdentificationScript returns one row per schema, and SchemaQuench
runs the full template against each returned schema with the active schema name
available as {{SchemaName}} everywhere it's needed. One declaration,
many iterations, one quench.
An iteration is one matched database for a regular template, or one matched schema inside a database for a schema template. See Schema Templates below for the full mode-switch mechanics, and the Multi-Tenant Deployments page for both patterns end to end.
Platform matches the running toolValidationScript against the target serverTemplateOrder, loading each Template.json in sequence<package-root>/
Product.json
Templates/
<TemplateName>/
Template.json
Tables/
Before Scripts/
Schemas/
Functions/
Views/
Procedures/
Triggers/
Table Data/
After Scripts/
...
The following fields are shared across all platforms.
| Property | Type | Default | Required | Description |
|---|---|---|---|---|
Name |
string | — | Yes | Product name. Automatically added as a {{ProductName}} script token. Used for migration script tracking and version stamping. |
Platform |
string | — | Yes | Target platform. Valid values: "SqlServer", "PostgreSQL", "MySQL". Determines which platform adapter handles deployment, extraction, and the default folder set. |
ValidationScript |
string | — | Yes | SQL expression evaluated before quench begins. Must return a truthy value or the quench aborts. Supports token replacement. |
TemplateOrder |
string[] | [] |
No | Ordered list of template directory names. Templates are quenched in this order. |
ScriptTokens |
object | {} |
No | Key-value pairs for {{TokenName}} replacement in scripts and SQL properties. See Script token mechanics. |
BaselineValidationScript |
string | — | No | SQL expression evaluated after server validation but before template processing. |
VersionStampScript |
string | — | No | SQL executed once after all templates complete successfully. Typically records the release version on the server. |
DropUnknownIndexes |
bool | false |
No | When true, the table quench drops indexes on managed tables that aren't defined in the table JSON. |
MinimumVersion |
string | — | No | Minimum target server version floor. Optional; omit to deploy against any version. SchemaQuench detects the version of every resolved target before any deployment work begins — if any target is below the floor, the entire run aborts with no side effects, and the diagnostic names each below-floor target with its detected version. SQL Server accepts a major version number (16) or a release year (2022, 2019, 2017, 2016); PostgreSQL accepts a major number (15, 16, 17); MySQL accepts major.minor (8.0, 8.4). An unparseable value is a configuration error that aborts at startup; if a target's version cannot be determined, that is also a hard error. See Validation and Version Stamps. |
CheckConstraintStyle |
string | "ColumnLevel" |
No | Controls how SchemaTongs writes check constraints during extraction: "ColumnLevel" (inline CheckExpression on the column) or "TableLevel" (named constraints in the CheckConstraints array). |
ScriptFolders |
array | [] |
No | Optional product-level folder definitions. Used to add custom folder paths or assign secondary-server filtering. See Custom Script Folders. |
BranchNameFile |
string | "{{repo_path}}/.git/HEAD" |
No | Path to the file SchemaSmith reads to derive the {{BranchName}} automatic token. Default points at Git's HEAD. Use any VCS that exposes the current branch as a single-line file; the only requirement is that the file exists and contains the branch identifier somewhere on its first line. |
BeforeBranchNameMask |
string | "ref: refs/heads/" |
No | Prefix to strip from the line read out of BranchNameFile. Default matches Git's ref: refs/heads/<branch> format. Set to "" for VCSs whose branch file already contains the bare branch name. |
AfterBranchNameMask |
string | "" |
No | Suffix to strip after the prefix is removed. Default empty. Set when your VCS appends extra text after the branch name. |
Extensions |
any | null |
No | Reserved. Product.json does not currently use Extensions for custom properties. |
SchemaQuench's drop-control flags are product-tier settings too — declare any of them
in Product.json to govern the whole product. All seven below are bool, optional, and default
to true. They resolve across an environment → product → template
cascade (all but DropTablesRemovedFromProduct add a per-table tier) where an explicit false at a
higher tier is sticky for all lower tiers. DropUnknownIndexes (in the table
above) participates in the same cascade with a default of false. See
Drop Control for full cascade behavior.
| Flag | What it gates |
|---|---|
DropTablesRemovedFromProduct |
Tables that exist in the target, aren't defined in any table JSON in the package, and were previously managed by this product. |
DropColumnsRemovedFromProduct |
Column-drop-by-absence: columns present in the database but absent from the table JSON. |
DropForeignKeysRemovedFromProduct |
Foreign-key-drop-by-absence. Only by-absence removal is gated — a modified foreign key (same name, changed definition) is still dropped and recreated. |
DropCheckConstraintsRemovedFromProduct |
Table-level check-constraint-drop-by-absence. Column-level CheckExpression reconciliation is not governed by this flag. |
DropExcludeConstraintsRemovedFromProduct |
EXCLUDE-constraint-drop-by-absence. PostgreSQL only — no effect on SQL Server or MySQL. |
DropStatisticsRemovedFromProduct |
Statistics-drop-by-absence for user-created statistics objects; auto-created statistics are never touched. SQL Server and PostgreSQL. |
DropIndexesRemovedFromProduct |
Product-owned indexes removed from the table JSON — distinct from DropUnknownIndexes, which targets out-of-band indexes never managed by SchemaSmith. Primary keys are never dropped. |
{
"Name": "MyProduct",
"Platform": "SqlServer",
"ValidationScript": "SELECT CAST(CASE WHEN EXISTS(SELECT * FROM master.sys.databases WHERE [Name] = '{{AppDb}}') THEN 1 ELSE 0 END AS BIT)",
"TemplateOrder": ["Shared", "AppDatabase"],
"ScriptTokens": {
"AppDb": "MyApp_Production",
"ReleaseVersion": "3.2.1"
},
"VersionStampScript": "UPDATE dbo.SchemaVersion SET Version = '{{ReleaseVersion}}'"
}
{
"Name": "MyProduct",
"Platform": "PostgreSQL",
"ValidationScript": "SELECT EXISTS(SELECT * FROM pg_database WHERE datname = '{{AppDb}}')",
"TemplateOrder": ["Shared", "AppDatabase"],
"ScriptTokens": {
"AppDb": "myapp_production",
"ReleaseVersion": "3.2.1"
},
"VersionStampScript": "UPDATE schema_version SET version = '{{ReleaseVersion}}'"
}
{
"Name": "MyProduct",
"Platform": "MySQL",
"ValidationScript": "SELECT EXISTS(SELECT * FROM information_schema.schemata WHERE SCHEMA_NAME = '{{AppDb}}')",
"TemplateOrder": ["AppDatabase", "AuditDatabase"],
"ScriptTokens": {
"AppDb": "my_app",
"ReleaseVersion": "3.2.1"
},
"VersionStampScript": "INSERT INTO `{{AppDb}}`.`schema_version` (`version`, `applied_at`) VALUES ('{{ReleaseVersion}}', NOW())"
}
| Property | Type | Default | Required | Description |
|---|---|---|---|---|
Name |
string | — | Yes | Template name. Must match the containing directory name. Automatically added as a {{TemplateName}} script token. |
DatabaseIdentificationScript |
string | — | Yes | SQL query that returns one or more database names. SchemaQuench reads the first column of each row. Supports token replacement. |
IdentificationDatabase |
string | — | No | Re-targets which database the DatabaseIdentificationScript runs against. Empty (the default) uses the platform init database. Point it at a control-plane registry database to enumerate a roster from a registry table. Token-resolvable — see roster from a control-plane registry. |
VersionStampScript |
string | — | No | SQL executed per database after that database's quench completes successfully. |
UpdateFillFactor |
bool | true |
No | When true, the table quench updates index fill factors to match the JSON definitions. OR'd with table-level and index-level UpdateFillFactor settings. |
IndexOnlyTableQuenches |
bool | false |
No | When true, the table quench only manages indexes, statistics, XML/full-text indexes. Skips table creation, column changes, and foreign key management. Tables that don't exist are silently skipped. |
BaselineValidationScript |
string | — | No | SQL validation executed per database before quenching that database. |
RequireAtLeastOneTarget |
bool | true |
No | When true, deployment fails if discovery returns no targets — zero matching databases for a regular template, or zero matching (database, schema) pairs for a schema template. Catches misconfigured identification scripts that silently skip an entire template. Replaces the prior Required field (renamed in v2.1). |
SkipIfReadOnly |
bool | false |
No | When true, databases that are read-only are silently skipped instead of failing the quench. Enables Availability Group secondary handling on SQL Server and replica handling on other platforms. |
ScriptFolders |
array | [] |
No | Optional list of TemplateFolder definitions. When empty, the platform's default folder set is used. When non-empty, this array fully replaces the defaults — so include every folder you want active. See Custom Script Folders. |
ScriptTokens |
object | {} |
No | Key-value pairs that override matching product-level tokens for this template. Template tokens take precedence over product tokens with the same key. |
SchemaIdentificationScript |
string | — | No | SQL Server / PostgreSQL: query returning one column, N rows; each row is a schema name to iterate over — presence activates schema-template mode (see Schema Templates). MySQL: has no in-database schema axis (a schema is a database), so schema templates don't apply; the field is instead accepted as a deprecated backward-compat alias for DatabaseIdentificationScript — on load its value migrates into that field (only when it is empty) and a deprecation warning advises renaming. Use DatabaseIdentificationScript directly on MySQL. |
CreateSchemaIfMissing |
bool | false |
No | Schema templates only. When true, the engine creates any discovered schema that doesn't yet exist before running that iteration. See Schema Templates. |
AllowParallel |
bool | true |
No | Schema templates only. When false, iterations of this template run serially even when the global thread pool has capacity. See Schema Templates. |
ContinueOnSchemaFailure |
bool | true |
No | Schema templates only. When false, the first failing iteration aborts all subsequent iterations for this template. See Schema Templates. |
ContinueOnDatabaseFailure |
bool | true |
No | Regular templates only. When false, the first failing database iteration aborts all subsequent database iterations for this template. Ignored on schema templates — failure isolation there is governed by ContinueOnSchemaFailure. |
{
"Name": "AppDatabase",
"DatabaseIdentificationScript": "SELECT [name] FROM master.sys.databases WHERE [name] = '{{AppDb}}'",
"VersionStampScript": "EXEC dbo.RecordDeployment '{{ReleaseVersion}}'",
"RequireAtLeastOneTarget": true
}
{
"Name": "TenantWorkspace",
"DatabaseIdentificationScript": "SELECT [name] FROM master.sys.databases WHERE [name] = '{{TenantCRMDb}}'",
"SchemaIdentificationScript": "SELECT [Name] FROM dbo.Tenants WHERE [Status] = N'Active' ORDER BY [Name]",
"RequireAtLeastOneTarget": false
}
The presence of SchemaIdentificationScript makes this a schema template — the full template runs once per returned schema. See Schema Templates below.
{
"Name": "AppDatabase",
"DatabaseIdentificationScript": "SELECT datname FROM pg_database WHERE datname = '{{AppDb}}'",
"VersionStampScript": "INSERT INTO deploy_log (version, deployed_at) VALUES ('{{ReleaseVersion}}', NOW())",
"RequireAtLeastOneTarget": true
}
{
"Name": "TenantWorkspace",
"DatabaseIdentificationScript": "SELECT datname FROM pg_database WHERE datname = '{{TenantCRMDb}}'",
"SchemaIdentificationScript": "SELECT name FROM public.tenants WHERE status = 'Active' ORDER BY name",
"RequireAtLeastOneTarget": false
}
The presence of SchemaIdentificationScript makes this a schema template — the full template runs once per returned schema. See Schema Templates below.
{
"Name": "AppDatabase",
"DatabaseIdentificationScript": "SELECT SCHEMA_NAME FROM information_schema.schemata WHERE SCHEMA_NAME = '{{AppDb}}'",
"VersionStampScript": "UPDATE `{{AppDb}}`.`schema_version` SET `last_applied` = NOW() WHERE `template` = 'AppDatabase'",
"RequireAtLeastOneTarget": true
}
Schema templates don't apply on MySQL — a schema is a database, so MySQL fans out across tenants with DatabaseIdentificationScript instead (database-per-tenant). See Schema Templates below.
Schema templates fan a single declarative template out across multiple schemas inside one
database. You write the template once — tables, procedures, views, migration scripts —
and SchemaQuench runs it once per schema returned by the SchemaIdentificationScript query,
injecting the active schema name as the {{SchemaName}} token at every step. The most common
use is multi-tenant SaaS where each tenant owns their own schema, but any pattern that needs
the same object shape replicated across schemas works the same way. For a full narrative
walkthrough, see Multi-Tenant Deployments.
Schema templates are supported on SQL Server and PostgreSQL only. MySQL uses a database-per-tenant model instead — there is no sub-database schema namespace to fan out across. On MySQL the SchemaIdentificationScript field is still accepted, but only as a deprecated backward-compat alias for DatabaseIdentificationScript (MySQL conflates schema and database): its value migrates into DatabaseIdentificationScript when that field is empty, a deprecation warning advises renaming, and no schema fan-out occurs. New MySQL packages should use DatabaseIdentificationScript directly.
SchemaIdentificationScript is the mode switch. When this field is present and non-empty on a Template.json, the template becomes a schema template. The value is a SQL query that returns one column and any number of rows; each row is a schema name, and SchemaQuench runs the full template once per returned row.
The query runs against each target database identified by DatabaseIdentificationScript. If
both are present, the engine computes the full cross-product: every (database, schema) pair
runs as an independent iteration. Token replacement applies to the query body before
execution, so you can reference script tokens or <*Query*> tokens in the
discovery query itself.
The active schema name is available to every part of the iteration as {{SchemaName}} —
in table Name and Schema fields, in procedure and view SQL bodies, in migration script
filenames, in VersionStampScript, and in user-defined script tokens. See
Script token mechanics for
availability rules and resolution timing.
A small set of platform-built-in schemas can't be used as iteration targets. If your discovery query returns one of these names, the engine fails the iteration with an error naming the offending schema and pointing you at the "shared content lives in a regular template" remediation. The reserved sets are:
dbo, sys, INFORMATION_SCHEMA, guest, plus the fixed database roles that double as schemas (db_owner, db_accessadmin, db_securityadmin, db_ddladmin, db_backupoperator, db_datareader, db_datawriter, db_denydatareader, db_denydatawriter).public, pg_catalog, pg_toast, information_schema, plus any schema matching the pg_temp_* or pg_toast_temp_* wildcards (Postgres uses these for session-scoped temp objects).Shared content (lookup tables, audit logs, dimension data) belongs in a regular template that runs once per database, not in a schema-template iteration. The reserved-name guard is the engine's way of catching a discovery query that accidentally returns public or dbo instead of a real tenant schema.
When CreateSchemaIfMissing is false (the default), an iteration whose schema
doesn't exist fails immediately with a clear error. This is the safer behavior: a typo in
your discovery query should not silently create schemas in production. When true, the engine
emits CREATE SCHEMA for any schema returned by the discovery query that does not
yet exist on the target database.
Set CreateSchemaIfMissing to true when you're running a fully automated onboarding pipeline and the deployment user is trusted to create schemas. Many teams prefer to create schemas via an explicit stored procedure (OnboardTenant) in the Shared template and leave this false.
CreateSchemaIfMissing: true requires the deployment user to have CREATE SCHEMA
permission on SQL Server or CREATE on the database on PostgreSQL. The default false is
intentional fail-fast: if the discovery query returns an unexpected schema name, you want an
error, not a new schema.
When AllowParallel is true (the default), schema iterations can run in parallel
up to the MaxThreads limit alongside iterations from other templates and
databases. Parallel execution is safe: each iteration touches its own schema namespace and
converges independently.
Set AllowParallel: false to force this template's iterations to run one at a
time — useful to cap concurrent load on a resource-constrained target, or when the
template's own migration scripts perform DDL that can't run concurrently. Parallel iteration
is otherwise the production-realistic default; both TenantCRM demos ship
AllowParallel: true.
When ContinueOnSchemaFailure is true (the default), a single schema iteration's
failure does not abort the others — remaining iterations continue and the product run
exits non-zero after all iterations have completed or failed. This matches the database-level
isolation behavior that most teams already rely on. When false, the first failing schema
iteration stops the dispatcher: no new iterations start, in-flight iterations drain, and
subsequent templates in TemplateOrder do not run.
Set ContinueOnSchemaFailure to false for deployments where any single-tenant failure is a hard stop — for example, a CI environment where partial deployment is worse than no deployment.
This is the TenantWorkspace/Template.json from the SQL Server TenantCRM demo — a real schema template with all four fields declared:
{
"Name": "TenantWorkspace",
"DatabaseIdentificationScript": "SELECT [name] FROM master.sys.databases WHERE [name] = '{{TenantCRMDb}}'",
"SchemaIdentificationScript": "SELECT [Name] FROM dbo.Tenants WHERE [Status] = N'Active' ORDER BY [Name]",
"RequireAtLeastOneTarget": false,
"CreateSchemaIfMissing": false,
"AllowParallel": true,
"ContinueOnSchemaFailure": true,
"VersionStampScript": "PRINT 'TenantCRM TenantWorkspace [{{SchemaName}}] {{ReleaseVersion}}'",
"ScriptFolders": [
{ "FolderPath": "Before Scripts", "QuenchSlot": "Before" },
{ "FolderPath": "Functions", "QuenchSlot": "Objects", "ObjectType": "Functions" },
{ "FolderPath": "Views", "QuenchSlot": "Objects", "ObjectType": "Views" },
{ "FolderPath": "Procedures", "QuenchSlot": "Objects", "ObjectType": "Procedures" },
{ "FolderPath": "Triggers", "QuenchSlot": "Objects", "ObjectType": "Triggers" }
]
}
RequireAtLeastOneTarget: false here handles a fresh installation where no tenants
have been onboarded yet — the schema template finds zero rows, treats it as a no-op, and
the product run succeeds so the Initialize and Shared templates still complete. The PostgreSQL
TenantCRM demo is identical in structure and ships AllowParallel: true, so tenants
deploy concurrently.
By default, every template uses the platform's standard folder layout (see Default Folders below). When you need to add a folder that isn't in the defaults, rename a folder, or change which slot a folder runs in, you declare your own ScriptFolders array on Template.json.
The array is an explicit replacement, not a merge. The moment you provide ScriptFolders with at least one entry, the defaults are skipped entirely — so you should include every folder you want loaded.
| Property | Type | Required | Description |
|---|---|---|---|
FolderPath |
string | Yes | Relative path under the template directory. Forward or back slashes both work. |
QuenchSlot |
string | Yes | Which execution slot the folder runs in. See Quench Slot Reference below for valid values. |
ObjectType |
string | No | When the folder contains programmable objects (functions, views, procedures, triggers, etc.), tag it with the corresponding object type so SchemaQuench can route it through the dependency-retry loop correctly. |
ShouldApplyExpression |
string | No | Optional SQL predicate evaluated against the target at deploy time (tokens resolved first): true deploys the folder, false skips it (logged), blank always deploys. See Conditional application. |
{
"Name": "Reporting",
"DatabaseIdentificationScript": "SELECT [Name] FROM master.sys.databases WHERE [Name] = '{{ReportDB}}'",
"ScriptFolders": [
{ "FolderPath": "Before Scripts", "QuenchSlot": "Before" },
{ "FolderPath": "Schemas", "QuenchSlot": "Objects", "ObjectType": "Schemas" },
{ "FolderPath": "Functions", "QuenchSlot": "Objects", "ObjectType": "Functions" },
{ "FolderPath": "Views", "QuenchSlot": "Objects", "ObjectType": "Views" },
{ "FolderPath": "Procedures", "QuenchSlot": "Objects", "ObjectType": "Procedures" },
{ "FolderPath": "BetweenTablesAndKeys", "QuenchSlot": "BetweenTablesAndKeys" },
{ "FolderPath": "AfterTables", "QuenchSlot": "AfterTablesScripts" },
{ "FolderPath": "Triggers", "QuenchSlot": "AfterTablesObjects", "ObjectType": "Triggers" },
{ "FolderPath": "Table Data", "QuenchSlot": "TableData" },
{ "FolderPath": "After Scripts", "QuenchSlot": "After" }
]
}
In this example, the team kept the standard folders but added two extra slots (BetweenTablesAndKeys, AfterTablesScripts) that aren't part of the default set. They can write migration scripts that run after the table structure exists but before foreign keys, or after the table structure but before triggers, without writing any extra glue.
Custom script folders are how you make the schema package fit your deployment lifecycle, not the other way around. Need a folder called Permissions that runs in the After slot? Done. Need to split your large Procedures directory into Procedures/Public and Procedures/Internal for code review? Done. Need an entirely new slot for your team's idempotent post-deploy data fixes? Drop a folder, point it at After, and you're done.
Product.json can also declare custom folders via its ScriptFolders array (the property name is the same as Template.json's). The shape is similar but uses ProductQuenchSlot (Before or After) and supports a ServerToQuench setting that controls whether the folder runs on the primary, secondaries, or both. ServerToQuench is a SQL Server-only feature; see Platform Differences below. Product folders also accept a ShouldApplyExpression, evaluated per server against the admin connection — see Conditional application.
When a package grows beyond a handful of objects, flat script folders become hard to navigate.
SchemaQuench discovers scripts recursively — all .sql files in a folder and
every subfolder underneath it, sorted alphabetically by full path — so you can group
related objects without changing their deployment behavior.
Procedures/
Reporting/
dbo.GetMonthlyRevenue.sql
dbo.GetQuarterlyReport.sql
Core/
dbo.ProcessOrder.sql
dbo.ValidateCustomer.sql
The alphabetical-by-full-path sort determines execution order, so Core/ scripts
run before Reporting/ scripts. Name your folders and files to make the order
legible — a prefix convention (01-Core/, 02-Reporting/) makes
intent obvious at a glance.
Table and view JSON follows the same rule. Files in Tables/Analytics/Orders.json
and Tables/Transactional/Customers.json are both discovered and loaded; the
subfolder is purely organizational.
Layout round-trips. When you re-extract a table, SchemaTongs looks up the object's existing file path in its index and writes back to that same location. Your subfolder organization survives the extraction cycle intact.
If the same filename appears in more than one subfolder — for example,
Reporting/dbo.GetReport.sql and Archive/dbo.GetReport.sql —
SchemaTongs logs a warning and writes to the base folder instead of an ambiguous location.
Keep object filenames unique across subfolders to avoid this.
TemplateQuenchSlot controls when in the deployment lifecycle a folder's scripts run.
| Slot | Behavior |
|---|---|
Before |
One-time migration scripts that run after initial object creation and new table creation, but before table modifications. Use for data preparation that must happen before columns are altered or dropped. Sequential, tracked. |
Objects |
Database objects that may have cross-dependencies (schemas, types, catalogs, functions, views, procedures). The retry loop resolves creation order automatically. |
BetweenTablesAndKeys |
Migration scripts that need the table structure to exist but must run before foreign key constraints are enforced. Typical use: populating a new NOT NULL column before FKs block the data load. Sequential, tracked. |
AfterTablesScripts |
Migration scripts that depend on the final table and key structure but must run before triggers are deployed. Sequential, tracked. |
AfterTablesObjects |
Triggers, DDL triggers, rules, and views that depend on the completed table structure. Dependency retry loop. |
TableData |
Data population scripts (MERGE statements, INSERT/UPDATE seeds). Run after triggers are deployed but before foreign key constraints are applied. Dependency retry loop. |
After |
Final migration scripts. Run after all database objects and data are deployed. Sequential, tracked. |
ProductQuenchSlot has only two values:
| Slot | Behavior |
|---|---|
Before |
Product-level scripts that run before any template processing begins. Sequential, untracked (run every deployment). |
After |
Product-level scripts that run after all templates complete. Sequential, untracked. |
Sequential, untracked — Product-level scripts run in alphabetical order on every deployment. They aren't recorded in any tracking table. Write these scripts to be idempotent.
Sequential, tracked — Template-level migration scripts run in alphabetical order. Each script's completion is recorded in the SchemaSmith.CompletedMigrationScripts table and won't run again on subsequent quenches. Scripts with [ALWAYS] in the filename run every time regardless of tracking.
Dependency retry loop — All scripts in the slot are attempted. Scripts that fail due to unresolved dependencies are retried on the next iteration. The loop continues until all scripts succeed or no progress is made on an iteration.
When Template.json does not declare its own ScriptFolders, SchemaSmith fills in a platform-specific default set. Each platform's defaults reflect the object types and lifecycle stages that platform actually supports.
| Folder | Quench Slot | Object Type |
|---|---|---|
Before Scripts/ | Before | — |
Schemas/ | Objects | Schemas |
DataTypes/ | Objects | DataTypes |
FullTextCatalogs/ | Objects | FullTextCatalogs |
FullTextStopLists/ | Objects | FullTextStopLists |
XMLSchemaCollections/ | Objects | XMLSchemaCollections |
Functions/ | Objects | Functions |
Views/ | Objects | Views |
Procedures/ | Objects | Procedures |
Triggers/ | AfterTablesObjects | Triggers |
DDLTriggers/ | AfterTablesObjects | DDLTriggers |
Table Data/ | TableData | — |
After Scripts/ | After | — |
13 default folders
| Folder | Quench Slot | Object Type |
|---|---|---|
Before Scripts/ | Before | — |
Schemas/ | Objects | Schemas |
Domain Types/ | Objects | DomainTypes |
Enum Types/ | Objects | EnumTypes |
Composite Types/ | Objects | CompositeTypes |
Functions/ | Objects | Functions |
Trigger Functions/ | Objects | TriggerFunctions |
Window Functions/ | Objects | WindowFunctions |
Aggregates/ | Objects | Aggregates |
Procedures/ | Objects | Procedures |
Sequences/ | Objects | Sequences |
Rules/ | AfterTablesObjects | Rules |
Triggers/ | AfterTablesObjects | Triggers |
Views/ | AfterTablesObjects | Views |
Table Data/ | TableData | — |
After Scripts/ | After | — |
16 default folders
| Folder | Quench Slot | Object Type |
|---|---|---|
Before Scripts/ | Before | — |
Events/ | Objects | Events |
Functions/ | Objects | Functions |
Procedures/ | Objects | Procedures |
Triggers/ | AfterTablesObjects | Triggers |
Views/ | AfterTablesObjects | Views |
Table Data/ | TableData | — |
After Scripts/ | After | — |
8 default folders
If your existing package has MigrationScripts/Before/, MigrationScripts/After/, or a TableData/ folder (no space) on disk, SchemaSmith will use them in place of Before Scripts/, After Scripts/, and Table Data/ respectively. This keeps older packages working without a folder rename.
Tables are always loaded from Tables/ regardless of platform. SQL Server adds Indexed Views/; PostgreSQL adds Materialized Views/. These are not script folders — they hold structured JSON object definitions, not .sql files.
The Product.json and Template.json shapes are identical across every platform. The table below summarises the engine-specific variations a schema package carries on top of that shared shape.
| Difference | SQL Server | PostgreSQL | MySQL |
|---|---|---|---|
| Table wrapper type | SqlServerTable |
PostgreSqlTable |
MySqlTable |
| Default table schema | dbo |
public |
— (database name = schema) |
| Database identification catalog | master.sys.databases |
pg_database |
information_schema.schemata |
| Schema templates | Supported | Supported | Not applicable — MySQL is database-per-tenant; the schema-identification field is accepted only as a deprecated alias (see Schema Templates) |
| Default folder count | 13 | 16 | 8 |
| Platform-specific object types | DDL Triggers, XML Schema Collections, Full-Text Catalogs / Stop Lists, Indexed Views | Domain Types, Enum Types, Composite Types, Trigger Functions, Window Functions, Aggregates, Sequences, Rules, Materialized Views | Events |
ServerToQuench (Availability Group / replica routing) |
Supported | Not applicable | Not applicable |
| Reference-data merge idiom | MERGE statement |
MERGE statement (PostgreSQL 15+) |
INSERT ... ON DUPLICATE KEY UPDATE |
The ServerToQuench routing field on product folders, and the SecondaryServers settings that back it, exist only for SQL Server Availability Groups. PostgreSQL and MySQL deployments connect to a single target server.
For the per-platform table-level fields (indexes, constraints, computed columns, full-text, materialized views), see Defining tables. For the reference-data merge configuration itself, see Data delivery.
TemplateOrder controls both which templates are included and
the sequence in which they execute. Templates are processed strictly sequentially:
SchemaQuench completes all databases for template N before moving to template N+1.
Order matters when:
Before scripts must complete before any template work begins
A template name in TemplateOrder with no matching folder on disk
causes SchemaQuench to abort. A template folder that exists but is not listed
in TemplateOrder is silently ignored.
Every SchemaQuench run has a pre-flight window before a single table is touched. That window exists to answer one question: is this the right server, in the right state, for this package?
ValidationScript is a required Product-level property. It runs first, against the server's
admin connection (the platform's init database: master, postgres,
or information_schema). If it returns a falsy value, the deployment aborts
before any quench begins. This is your identity check: am I on the database server I think
I am? Common patterns: verify an expected database exists on the server, confirm a linked
server or infrastructure dependency is in place, or gate on server version or edition to
prevent running an incompatible package. Token replacement applies —
{{MainDB}}, {{ReleaseVersion}}, any token you define in
ScriptTokens — so the validation script isn't hardcoded to one environment.
Validation scripts form a gate chain. Each must pass before the next phase begins:
BaselineValidationScript answers "is this environment at the state this package expects?" The canonical use is anti-rollback protection. Pair it with VersionStampScript: the stamp records a version identifier after a successful deployment; the baseline checks for that identifier before the next deployment. If someone accidentally runs an older package against an already-upgraded environment, the baseline check aborts before any harm is done.
{
"VersionStampScript": "UPDATE dbo.DeploymentInfo SET Version = '{{ReleaseVersion}}'",
"BaselineValidationScript": "SELECT CAST(CASE WHEN EXISTS(
SELECT 1 FROM dbo.DeploymentInfo WHERE Version = '{{PreviousVersion}}'
) THEN 1 ELSE 0 END AS BIT)"
}
MinimumVersion is the declarative companion to the validation scripts — set it in Product.json and SchemaQuench enforces a hard engine version floor before running anything. Use it for the unconditional floor ("this package requires PostgreSQL 15 or later"); write ValidationScript logic for conditional gates or checks that need to read server state. See the Product.json fields table for accepted version formats and abort behavior.
Define your first Product.json and template structure, then orchestrate which templates deploy and in what order.
Start the product lab