SchemaSmith Concept

Conditional Application

Declare which table components apply where. ShouldApplyExpression evaluates per target database; one table file carries every environment variant with no branching pipeline.

By the SchemaSmith Team · Last reviewed

Environment labels scoping table components at deployment time

ShouldApplyExpression is the universal opt-in/opt-out switch on every component that supports it.

Overview

Dev uses synthetic data and a lean index set. Staging mirrors production's footprint. Production carries the full reporting stack and the regulated columns. Without a declarative answer, those differences turn into per-environment file copies, branching pipelines, or hand-maintained deploy scripts — the kind of drift that's fine until a column gets forgotten. ShouldApplyExpression is that declarative answer: a SQL fragment attached to a component that decides, at deployment time, whether the component should apply to the current target.

Where you can use it

The same property is available on:

  • Tables
  • Columns
  • Indexes
  • Foreign keys
  • Check constraints
  • Indexed views
  • Materialized views
  • Statistics
  • Exclude constraints
  • Full-text indexes
  • XML indexes

Token resolution and evaluation

The expression is a SQL fragment that returns a single scalar value. When SchemaQuench evaluates it before deploying that component, it considers the result false if it's 0, the literal string false, or empty/null. Anything else means apply the component normally.

On a table component you write it as a bare boolean predicate — the example below is simply '{{Table.Environment}}' = 'Production'. A script folder gate is written as a SELECT that returns a boolean; see Folder-level gates below.

Tokens inside the expression — {{Table.Environment}} in the example below — are substituted through the Script Tokens system before the expression runs. See Script token mechanics for the token sources, substitution rules, and case-insensitive matching behavior.

Example

A reporting index that should only materialize in production, scoped by an Extensions-driven Custom Property:

{
  "Name": "[Orders]",
  "Extensions": { "Environment": "Production" },
  "Indexes": [
    {
      "Name": "[IX_Orders_Reporting]",
      "IndexColumns": "[ReportingDate]",
      "ShouldApplyExpression": "'{{Table.Environment}}' = 'Production'"
    }
  ]
}

The Orders table declares a table-level Custom Property Extensions.Environment set to Production. The IX_Orders_Reporting index's ShouldApplyExpression reads that property through {{Table.Environment}} at deployment. The expression is true on every target where the property resolves to Production, so the index applies there and is skipped everywhere else — one index definition, one table file, no per-environment forks.

Three levels of gating

ShouldApplyExpression works at more than one level, and it helps to keep them straight:

  • Within a table. Columns, indexes, foreign keys, check constraints, statistics, and the other components inside a table file can each declare conditional variants — same name, mutually exclusive expressions — and the matching one is chosen per target. These variant sets are fully preserved when SchemaTongs re-extracts the table.
  • The whole table. A table-level ShouldApplyExpression gates the entire table present-or-absent on a given target, and that gating round-trips through extraction too.
  • A whole folder. A product- or template-level script folder can carry a ShouldApplyExpression, so an entire folder of scripts deploys or is skipped per target — a MariaDB/ variant gated on @@version, a Jobs/ folder skipped on Azure SQL, or TableData/TestData/ kept out of production. Its tokens are resolved before evaluation, like every other gate. See Folder-level gates below.

Recommendation: to vary a table's structure by target, prefer component-level variants inside a single table file (or give the structurally different tables distinct names). Two separate same-named whole-table variant files will deploy correctly, but SchemaTongs normalizes a table to one file per name on extraction, so a multi-file same-named-table layout isn't reproduced when you re-extract.

Folder-level gates

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 as 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 is run 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. It can read SERVERPROPERTY / @@version, call an environment-type function your team already has, query a control table, or reference resolved tokens (including {{SchemaName}} on schema templates).

This turns "different folders for different flavors of a target" into a declarative property instead of pipeline branching: a MariaDB/ folder gated on a @@version check beside a MySQL/ folder gated on the negation, a Jobs/ folder skipped on Azure SQL, or a TableData/TestData/ folder kept out of production by your environment predicate.

Note

A product-folder expression runs against the server's admin connection (the platform's init database — master / postgres / information_schema), because product-level scripts are server-scoped. Use server-scoped predicates there (server properties, version, edition). A template-folder expression runs against the actual target database (and schema for schema templates), so it can also query target-database state. Product folders are evaluated per server; template folders are evaluated per database — and per schema for schema templates.

Important

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.

Naming your variants

When a component carries several same-named variants gated by mutually exclusive expressions, give each one a VariantName. The expression says when a variant applies; the name says why it exists — and that name is how a human (or a downstream tool) tells two complex, SQL-gated variants apart at a glance.

"Indexes": [
  {
    "Name": "[IX_Orders_Region]",
    "VariantName": "Modern engines",
    "IndexColumns": "[Region]",
    "FilterExpression": "[Region] IS NOT NULL",
    "ShouldApplyExpression": "SERVERPROPERTY('ProductMajorVersion') >= 16"
  },
  {
    "Name": "[IX_Orders_Region]",
    "VariantName": "Legacy engines",
    "IndexColumns": "[Region]",
    "ShouldApplyExpression": "SERVERPROPERTY('ProductMajorVersion') < 16"
  }
]

Both variants share a name and target the same column, but only one matches any given server. When the matching variant deploys, its name rides along in the log:

  Creating index dbo.Orders.IX_Orders_Region (variant: Modern engines)

The same (variant: ...) suffix appears in WhatIf output, so a dry run tells you exactly which variant would be applied to each target — before you commit to it. VariantName is metadata only: it has no effect on what gets deployed, it just makes the deployment legible. It's an optional label, up to 128 characters, and it round-trips through SchemaTongs re-extraction alongside the rest of the variant set.

Full-text indexes on SQL Server take variants one step further: declare an array of full-text variants on a table — each with its own catalog and ShouldApplyExpression — and each target deploys only the variant that matches it. See the table definition reference for the array field shape.

Runtime sentinel skip

Sometimes the skip decision can't be expressed as a static expression in the package at all — it depends on something only the target server can answer at the moment the script runs. Is this database on a replica? Did a prior script's data migration land correctly? Is this a tenant that hasn't opted into a feature yet? ShouldApplyExpression covers those cases when the answer is a SQL query. But if the logic is inside the script itself — reading row counts, calling a stored procedure, checking a role membership, branching on a version+edition combination — you need the script to decide at runtime.

Raise the sentinel error and SchemaQuench treats the script as an intentional skip, not a failure:

-- SQL Server
IF SERVERPROPERTY('EngineEdition') NOT IN (5, 8)
    RAISERROR('SCHEMASMITH: SHOULD NOT APPLY', 16, 1);

SchemaQuench recognizes the exact message SCHEMASMITH: SHOULD NOT APPLY (trimmed, case-insensitive, matched as the entire message). It logs the skip and moves on — the deployment succeeds. Any other error still surfaces as a real failure.

The raise form on each platform

PlatformRaise form
SQL ServerRAISERROR('SCHEMASMITH: SHOULD NOT APPLY', 16, 1)
PostgreSQLRAISE EXCEPTION 'SCHEMASMITH: SHOULD NOT APPLY'
MySQLSIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'SCHEMASMITH: SHOULD NOT APPLY'

Warning

SQL Server severity matters. RAISERROR at severity ≤ 10 is an informational message, not an error — SchemaQuench never sees it and the script continues executing. Use severity ≥ 11 (16 is the conventional choice) so the raise is an abort-level error that SchemaQuench can catch.

Batch semantics

Any batch may carry the sentinel — not just the top of the script. Earlier batches that already ran are committed (the engine does not wrap the script in a transaction). The sentinel stops the rest of the script; the work those earlier batches did is preserved. "Do real setup in early batches, then decide later batches shouldn't apply" is fully supported — you own the partial-work semantics.

Run-once tracking

A migration script that raises the sentinel is recorded in CompletedMigrationScripts just like a script that ran normally — it will not be retried on the next deployment. The skip decision is per-database, so a database with different state re-evaluates independently.

Which script surfaces honor it

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

Think of the sentinel as the ShouldApplyExpression for logic that can only run inside the script. Use ShouldApplyExpression when a single SQL expression makes the call; use the sentinel when the script needs to inspect, branch, or call procedures before it can decide.

Custom Properties as drivers

A ShouldApplyExpression evaluates against metadata your team defines. Custom Properties are the mechanism: an open Extensions object on every schema object where you attach environment labels, data-classification tags, ownership markers, retention policies, or any other team-defined metadata. Those values become {{TokenName}} substitutions inside ShouldApplyExpression and every other expression field SchemaQuench evaluates.

Combined with Custom Properties and the rest of the Script Tokens feature surface, ShouldApplyExpression lets you express deployment-time decisions declaratively without ever writing a separate per-environment script file. See Custom Properties for the Extensions carrier shape, token promotion rules (bare names at table scope, Table. prefix from child components), and nested-object flattening.

Hands-on lab

Write a ShouldApplyExpression to gate an index, column, or table to a specific environment, without per-environment file copies.

Try conditional application