Every table in SchemaSmith is a JSON file inside your schema package's Tables folder, declaring the desired state of that table. SchemaQuench computes the DDL to get there.
By the SchemaSmith Team · Last reviewed
Every table definition file declares exactly one table.
In SchemaSmith, your database schema lives as declarative JSON files, not as a collection of ALTER scripts or numbered migrations. Every table is one JSON file. That file describes the table as it should be: its name, schema namespace, columns, indexes, constraints, foreign keys, check expressions, and — depending on your platform — platform-specific features like XML indexes, full-text indexes, or statistics. There's no hand-written DDL, no rollback files, no version numbers. You shape the JSON; SchemaQuench reads it and computes the minimal set of DDL statements needed to transform your live database into the declared state.
The process is straightforward. You create a schema package — a directory tree with your JSON table definitions — and pass it to SchemaQuench. SchemaQuench diffs your live database against the JSON declarations, generates the necessary CREATE or ALTER statements, and executes them in order. On SQL Server, you declare columns with INT IDENTITY(1, 1) syntax; on PostgreSQL, you use INTEGER GENERATED ALWAYS AS IDENTITY; on MySQL, INT AUTO_INCREMENT. The JSON structure is identical across all three platforms — only the data types, quoting styles, and platform-native features change. You build the same mental model for schema definition across SQL Server, PostgreSQL, and MySQL; the file format doesn't shift, only the dialect inside it does.
Tables aren't defined in isolation. The JSON arrays in each table file capture the full picture: Columns (with nullable flags, defaults, and check expressions), Indexes (with clustering and uniqueness directives), CheckConstraints (table-level business rules), and ForeignKeys (referential integrity). On platforms that support them, you also declare XmlIndexes, FullTextIndex, and Statistics (SQL Server); ExcludeConstraints, sequences, composite types, and enums (PostgreSQL); or Engine and Collation settings (MySQL). SchemaQuench reads these arrays, aligns them against the live database, and generates the diff. Run the same package against dev, staging, and production and you get the same predictable deployment every time.
Two cross-cutting features get their own homes: DataDelivery declares how a table's reference rows land in each target database, and conditional application uses ShouldApplyExpression to scope tables, columns, indexes, and constraints to specific environments or server versions. Both appear as properties on the shared table JSON structure below.
All three platforms use the same JSON table structure, but the contents are platform-specific. Data types differ: SQL Server's INT IDENTITY, PostgreSQL's GENERATED AS IDENTITY, MySQL's AUTO_INCREMENT. Quoting differs: SQL Server uses [brackets], PostgreSQL uses bare names or "double quotes", MySQL uses `backticks` or bare names. A SQL Server dbo.Customers.json is not a PostgreSQL public.customers.json — the file structure rhymes, but the contents are platform-specific. Each platform gets its own schema package.
Every engine brings capabilities that go beyond basic columns and indexes. SQL Server has temporal tables and change data capture. PostgreSQL has exclusion constraints and row-level security. MySQL has storage-engine tuning and auto-increment seeding. All of them live in the same table JSON you've been writing — no new tools, no separate configuration layer. SchemaSmith models each platform's table as a wrapper object (SqlServerTable, PostgreSqlTable, MySqlTable), and a schema package ships one file per table, written for exactly one platform.
The engine-specific fields below extend the shared properties. Pick your platform:
SqlServerTable properties, in addition to the shared set:
| Property | Type | Default | Description |
|---|---|---|---|
Schema |
string | "dbo" |
Database schema. Bracket notation in extracted files (e.g., "[Production]"). |
CompressionType |
string | "NONE" |
Table data compression: "NONE", "ROW", or "PAGE". |
IsTemporal |
bool | false |
Manages the table as system-versioned temporal. See the note below. |
XmlIndexes |
array | [] |
XML index definitions (primary + secondary VALUE / PATH / PROPERTY). |
Statistics |
array | [] |
Custom statistics definitions. |
FullTextIndex |
object or array | null |
Full-text index — a single definition, or an array of conditional variants. See Full-text indexes. |
UpdateFillFactor |
bool | false |
When true, index fill factors on this table are updated to match the JSON definitions during quench. |
EnableCDC |
bool | false |
When true, the table is enabled for Change Data Capture. SchemaSmith sequences the enable/disable safely around column changes so CDC and schema evolution don't conflict. |
Temporal tables. Set IsTemporal to true and SchemaSmith adds the system-time period columns (ValidFrom, ValidTo), the PERIOD FOR SYSTEM_TIME declaration, and SYSTEM_VERSIONING = ON pointing at a <Name>_Hist history table, and protects the period columns from drop detection. Toggle it back to false and SchemaSmith emits SET (SYSTEM_VERSIONING = OFF) — clean in both directions. The history table itself (<Name>_Hist in the same schema) is not created for you: declare it as a sibling table JSON, or create it in a Before script before the temporal table is quenched.
PostgreSqlTable properties, in addition to the shared set:
| Property | Type | Default | Description |
|---|---|---|---|
Schema |
string | "public" |
Database schema. Double-quote notation in extracted files (e.g., "\"sales\""). |
Statistics |
array | [] |
Extended statistics definitions. |
ExcludeConstraints |
array | [] |
Exclusion constraint definitions. See Exclude constraints below. |
RowLevelSecurity |
bool | false |
Enables row-level security on the table. |
ForceRowLevelSecurity |
bool | false |
When true, row-level security is enforced even for the table owner. |
AccessMethod |
string | null |
Table storage access method (e.g., "heap"). Distinct from the per-index AccessMethod covered under Indexes. |
PersistenceType |
string | null |
Persistence override (e.g., "UNLOGGED", "TEMPORARY"). |
UpdateFillFactor |
bool | false |
Enables fill-factor reconciliation for this table. |
FillFactor |
short (0–100) | 0 |
Table fill factor. 0 means use the server default. |
Row-level security. SchemaSmith manages the table-level RowLevelSecurity and ForceRowLevelSecurity flags. The policies themselves are defined in your scripts — SchemaSmith does not manage individual row policies. Materialized views aren't tables: they live as their own JSON files in a Materialized Views/ folder alongside your tables, where SchemaSmith manages their create/drop lifecycle and their own indexes.
MySqlTable properties, in addition to the shared set:
| Property | Type | Default | Description |
|---|---|---|---|
Engine |
string | "InnoDB" |
Storage engine. |
RowFormat |
string | null |
Row format: "DYNAMIC", "COMPACT", "COMPRESSED", or "REDUNDANT". |
CharacterSet |
string | null |
Default character set for the table. |
Collation |
string | null |
Default collation for the table. |
Comment |
string | null |
Table comment. |
AutoIncrementValue |
ulong | null |
Initial auto-increment seed. Applied with set-if-higher semantics. See the note below. |
FullTextIndexes |
array | [] |
Full-text index definitions (MySQL supports multiple per table). See Full-text indexes. |
Auto-increment seed. AutoIncrementValue is applied at quench time using set-if-higher semantics: the seed is only raised, never lowered. MySQL clamps a below-current value to max+1, so SchemaSmith skips the statement in that case to avoid phantom DDL on every quench.
Unique indexes enforce equality: no two rows can have the same value. Exclusion constraints enforce an operator relationship: no two rows can satisfy a given operator pair. The canonical use case is non-overlapping reservation periods — a GiST index with the && (overlap) operator guarantees that no two reservations for the same room span the same time. Declare these in the ExcludeConstraints array on the table.
| Property | Type | Description |
|---|---|---|
Name |
string | Constraint name. |
AccessMethod |
string | Index access method backing the constraint (e.g., "gist"). |
ExcludeColumns |
array | One or more { "Column", "Operator" } pairs. |
FilterExpression |
string | Optional WHERE clause. |
ShouldApplyExpression |
string | Conditional inclusion. See Conditional Application. |
VariantName |
string | Optional label for a conditional variant. Max 128 characters. |
Deferrable |
bool | Whether the constraint is deferrable. |
InitiallyDeferred |
bool | Whether the constraint defers by default. |
A non-overlapping reservation constraint:
{
"Name": "no_overlapping_reservations",
"AccessMethod": "gist",
"ExcludeColumns": [
{ "Column": "room_id", "Operator": "=" },
{ "Column": "reserved_period", "Operator": "&&" }
]
}
A table's JSON can carry any of the object-level drop flags — DropColumnsRemovedFromProduct, DropForeignKeysRemovedFromProduct, DropCheckConstraintsRemovedFromProduct, DropIndexesRemovedFromProduct — set to false to shield that table's own columns, foreign keys, check constraints, or indexes from by-absence removal, even when the environment, product, or template tier permits drops. The table tier can only tighten: a false is a hard guardrail. It can never set true to re-enable a drop that a higher tier has suppressed. (DropTablesRemovedFromProduct has no table-level equivalent — a removed table has no JSON left to carry the flag.) See Drop Control for the full four-tier cascade.
Each wrapper appears in worked examples on the platform's daily workflows page: SQL Server, PostgreSQL, MySQL.
Every entry in the Columns array defines one column. The shared shape is small; platform-specific column subclasses add fields where the engine genuinely differs.
| Property | Type | Default | Description |
|---|---|---|---|
Name |
string | Column name. | |
DataType |
string | Platform-appropriate data type with precision/scale/length. SQL Server: NVARCHAR(50), INT IDENTITY(1,1). PostgreSQL: VARCHAR(50), INTEGER GENERATED ALWAYS AS IDENTITY. MySQL: VARCHAR(50), INT AUTO_INCREMENT. |
|
Nullable |
bool | false |
Whether the column allows NULL. |
Default |
string | Default constraint expression — e.g., getdate() (SQL Server), now() (PostgreSQL), CURRENT_TIMESTAMP (MySQL). |
|
ShouldApplyExpression |
string | Conditional inclusion. See Conditional Application. | |
VariantName |
string | Optional label for a conditional variant. Appears in deployment log messages when the variant applies. Max 128 characters. | |
OldName |
string | "" |
Previous column name for rename detection. Clear after the rename has deployed everywhere. |
Extensions |
object | null |
Custom metadata for this column. See Custom Properties. |
When a database uses user-defined types (CREATE TYPE / CREATE DOMAIN), the DataType value is the type name. The type must be created in the appropriate types script folder (DataTypes/ on SQL Server; Domain Types/, Enum Types/, or Composite Types/ on PostgreSQL) before the table quench runs.
A generated column derives its value from an expression over other columns in the same row — the engine keeps it current on every insert and update, with no triggers and no application-layer logic. SQL Server uses ComputedExpression; PostgreSQL and MySQL use GenerationExpression. Whether that value is stored on disk or recomputed on every read is a per-platform choice — see each tab below.
Declare the expression with ComputedExpression; set Persisted to store the result on disk.
{
"Name": "FullName",
"ComputedExpression": "[FirstName] + ' ' + [LastName]",
"Persisted": true
}
Declare it with GenerationExpression and "Generated": "ALWAYS". It's STORED by default; set "Virtual": true and PostgreSQL recomputes it on every read (VIRTUAL) instead.
{
"Name": "full_name",
"DataType": "TEXT",
"Generated": "ALWAYS",
"GenerationExpression": "first_name || ' ' || last_name",
"Nullable": false
}
A MySQL generated column must be defined after every column it references. SchemaSmith resolves the creation order automatically with a topological sort (and circular-dependency detection) — declare the expression and the tool handles where the column lands. Use "Generated": "STORED" or "VIRTUAL" to control persistence.
{
"Name": "full_name",
"DataType": "VARCHAR(255)",
"GenerationExpression": "CONCAT(first_name, ' ', last_name)",
"Nullable": false
}
Always Encrypted lets SQL Server store sensitive column data in encrypted form that the server itself cannot read — only authorized clients holding the Column Master Key can decrypt. It is a SQL Server feature with no PostgreSQL or MySQL equivalent. SchemaTongs extracts encrypted columns and captures all three encryption properties; SchemaQuench declares them with the exact ENCRYPTED WITH (…) syntax SQL Server requires — extract once, deploy everywhere the CMK is distributed, no hand-written DDL. These three properties appear on SQL Server column definitions only and are ignored on other platforms.
| Property | Type | Default | Description |
|---|---|---|---|
EncryptionType |
string | "NONE" |
"DETERMINISTIC" (repeatable ciphertext, supports equality comparisons) or "RANDOMIZED" (non-repeatable ciphertext, stronger protection). "NONE" or absent means no encryption. |
EncryptionKey |
string | "" |
Column Encryption Key name, bracket-wrapped (e.g., "[MyCEK]"). Must match a CEK already installed in the target database. |
EncryptionAlgorithm |
string | "" |
Encryption algorithm name. The only currently supported value is "AEAD_AES_256_CBC_HMAC_SHA_256". |
When EncryptionType is not "NONE", SchemaQuench emits this clause into the column DDL:
ENCRYPTED WITH (
COLUMN_ENCRYPTION_KEY = [EncryptionKey],
ENCRYPTION_TYPE = [EncryptionType],
ALGORITHM = '[EncryptionAlgorithm]'
)
{
"Name": "[SSN]",
"DataType": "NVARCHAR(11)",
"Nullable": false,
"Collation": "Latin1_General_BIN2",
"EncryptionType": "DETERMINISTIC",
"EncryptionKey": "[CustomerCEK]",
"EncryptionAlgorithm": "AEAD_AES_256_CBC_HMAC_SHA_256"
}
Changing encryption on a populated column is not supported in-quench. If you change EncryptionType, EncryptionKey, or EncryptionAlgorithm on a column that already has data — or add encryption to a column that was previously plaintext — SchemaQuench raises a hard error before any DDL runs, names the column, and leaves it untouched. This is a fundamental SQL Server constraint, not a tooling limitation: a standard server holds no Column Master Key and cannot re-encrypt data server-side. The guard fires in both live and WhatIf mode, so the constraint shows up in previews. The workaround is a full-table rebuild — a Before script that creates the new table with the target encrypted schema and copies rows over a Column Encryption Setting=Enabled connection (the driver handles decrypt/re-encrypt client-side), then an After script that recreates foreign keys, constraints, and extended properties. Run it in a maintenance window.
Encrypted character columns require a BIN2 collation (e.g., Latin1_General_BIN2). SQL Server enforces this — a non-binary collation on an encrypted column fails at DDL execution time.
Each platform adds engine-specific column fields — collation, data masking, generated-column syntax, character sets. Worked examples live on the SQL Server, PostgreSQL, and MySQL daily workflows pages.
Every entry in the Indexes array defines an index or key constraint on the table. The shared shape covers the common cases; per-platform index types extend it.
| Property | Type | Default | Description |
|---|---|---|---|
Name |
string | Index or constraint name. | |
PrimaryKey |
bool | false |
true for a primary key constraint. |
Unique |
bool | false |
true for a unique index. |
UniqueConstraint |
bool | false |
true for a UNIQUE constraint (as opposed to a unique index). |
IndexColumns |
string | Comma-separated column names with optional sort direction. | |
IncludeColumns |
string | Comma-separated INCLUDE / covering columns where supported. | |
FilterExpression |
string | Filtered / partial index WHERE clause. | |
ShouldApplyExpression |
string | Conditional inclusion. See Conditional Application. | |
VariantName |
string | Optional label for a conditional variant. Appears in deployment log messages when the variant applies. Max 128 characters. | |
Extensions |
object | null |
Custom metadata. See Custom Properties. |
SQL Server quoting; clustered primary key.
{
"Name": "[PK_Product_ProductID]",
"PrimaryKey": true,
"Unique": true,
"Clustered": true,
"IndexColumns": "[ProductID]"
}
PostgreSQL filtered / partial index.
{
"Name": "ux_customer_email_active",
"Unique": true,
"IndexColumns": "email",
"FilterExpression": "is_active = true"
}
SQL Server covering index with INCLUDE columns.
{
"Name": "[IX_Customer_Name]",
"IndexColumns": "[LastName] ASC, [FirstName] ASC",
"IncludeColumns": "[Email], [Phone]"
}
Beyond the shared shape, each engine adds its own index types and modifiers.
Extras: Clustered, ColumnStore, CompressionType (NONE / ROW / PAGE), FillFactor, UpdateFillFactor. Add "ColumnStore": true to build a columnstore index instead of the default B-tree — the right shape for analytic scans over millions of rows — and pair it with "CompressionType": "COLUMNSTORE_ARCHIVE" for maximum compression on cold data.
{
"Name": "[CCI_Sales]",
"ColumnStore": true,
"CompressionType": "COLUMNSTORE_ARCHIVE"
}
Extras: AccessMethod, Tablespace, and IncludeColumns (PostgreSQL 11+ covering indexes). Set AccessMethod to pick the index method — gin for JSONB and arrays, gist for ranges and geometry, brin for append-only time-series — and SchemaSmith emits the matching USING clause.
{
"Name": "ix_docs_tags",
"IndexColumns": "tags",
"AccessMethod": "gin"
}
Extras: IndexType (BTREE / HASH) and a visible/invisible flag. Set "IndexType": "SPATIAL" to index a geometry column — SchemaSmith emits CREATE SPATIAL INDEX, so you can pair it with MySQL's spatial functions for proximity and bounding-box queries.
{
"Name": "sp_geom",
"IndexColumns": "location",
"IndexType": "SPATIAL"
}
Natural-language search over text columns needs a full-text index — a catalog-backed structure the engine manages separately from its B-tree indexes. It's declared as a property on the table, not as an entry in the Indexes array.
SQL Server allows one full-text index per table — but that one index can cover multiple text columns. Declare it as a single FullTextIndex object, or as an array of conditional variants when different targets need different definitions (a different catalog per region, say). At deploy time each variant's ShouldApplyExpression runs against the target and the matching variant deploys.
| Property | Type | Description |
|---|---|---|
FullTextCatalog |
string | Name of the full-text catalog. |
KeyIndex |
string | Name of the unique index used as the full-text key. |
ChangeTracking |
string | "OFF", "MANUAL", or "AUTO". |
StopList |
string | Name of a full-text stop list. |
Columns |
string | Column specification for the full-text index. |
ShouldApplyExpression |
string | Boolean expression evaluated on the target; the index (or variant) applies only when true. |
VariantName |
string | Optional label for a conditional variant. Max 128 characters. |
Variant rules:
ShouldApplyExpression, and those expressions must be mutually exclusive on any given target. Two variants matching the same target fails the deployment with a clear error."FullTextIndex": [
{
"FullTextCatalog": "[FTC_East]",
"KeyIndex": "[PK_Docs]",
"Columns": "[Title],[Body]",
"ShouldApplyExpression": "'{{Region}}' = 'East'"
},
{
"FullTextCatalog": "[FTC_West]",
"KeyIndex": "[PK_Docs]",
"Columns": "[Title],[Body]",
"ShouldApplyExpression": "'{{Region}}' = 'West'"
}
]
MySQL allows multiple full-text indexes per table, so the property is FullTextIndexes (an array). Each index can specify a custom parser — "Parser": "ngram" for CJK and other non-space-delimited languages, for example. SchemaSmith creates each one with CREATE FULLTEXT INDEX … WITH PARSER when a parser is specified.
| Property | Type | Description |
|---|---|---|
Name |
string | Index name. |
Columns |
string | Comma-separated column names. |
Parser |
string | Optional parser name (e.g., "ngram"). |
Comment |
string | Index comment. |
ShouldApplyExpression |
string | Conditional inclusion. |
VariantName |
string | Optional label for a conditional variant. Max 128 characters. |
"FullTextIndexes": [
{
"Name": "ft_body",
"Columns": "title, body",
"Parser": "ngram"
}
]
Each platform extends the shared index properties with engine-native features — clustered and columnstore indexes, access methods, index types, visibility flags. Worked examples live on the SQL Server, PostgreSQL, and MySQL daily workflows pages.
Referential integrity lives in the table JSON alongside columns and indexes, not in separate migration scripts. The ForeignKeys array captures each relationship: the local columns, the related table they reference, and the optional cascade actions for deletes and updates. SchemaQuench reads the array, diffs it against the live database, and adds, drops, or recreates constraints to match.
| Property | Type | Default | Description |
|---|---|---|---|
Name |
string | Constraint name. | |
Columns |
string | Comma-separated local column names. | |
RelatedTableSchema |
string | platform default | Schema of the referenced table (dbo on SQL Server, public on PostgreSQL, omitted on MySQL). |
RelatedTable |
string | Referenced table name. | |
RelatedColumns |
string | Comma-separated referenced column names. | |
DeleteAction |
string | null |
"NO ACTION", "CASCADE", "SET NULL", "SET DEFAULT", or "RESTRICT" where supported. |
UpdateAction |
string | null |
Same values as DeleteAction. |
ShouldApplyExpression |
string | Conditional inclusion. See Conditional Application. | |
VariantName |
string | Optional label for a conditional variant. Appears in deployment log messages when the variant applies. Max 128 characters. | |
Extensions |
object | null |
Custom metadata. See Custom Properties. |
For composite foreign keys, list all columns in both Columns and RelatedColumns in matching order.
Table-level check constraints sit in the CheckConstraints array. Used when CheckConstraintStyle is "TableLevel" in Product.json, or when a check constraint spans multiple columns.
| Property | Type | Default | Description |
|---|---|---|---|
Name |
string | Constraint name. | |
Expression |
string | Boolean SQL expression. | |
ShouldApplyExpression |
string | Conditional inclusion. See Conditional Application. | |
VariantName |
string | Optional label for a conditional variant. Appears in deployment log messages when the variant applies. Max 128 characters. | |
Extensions |
object | null |
Custom metadata. See Custom Properties. |
When CheckConstraintStyle is "ColumnLevel" (the default), single-column check constraints are written as CheckExpression on the column instead. Multi-column constraints always use the CheckConstraints array. The column-level CheckExpression shorthand works identically on SQL Server, PostgreSQL, and MySQL.
Each platform reference covers the complete table JSON format, the DDL SchemaQuench emits, and the platform-native concepts it handles for that engine.
Walk through adding tables with INT IDENTITY, modifying columns, writing CREATE OR ALTER procedures, extracting drift with SchemaTongs, and bootstrapping new databases with the Initialize template.
Walk through adding tables with GENERATED AS IDENTITY, modifying columns, writing CREATE OR REPLACE functions, extracting drift with SchemaTongs, and bootstrapping new databases with the Initialize template.
Walk through adding tables with AUTO_INCREMENT, modifying columns, writing procedures with DELIMITER //, extracting drift with SchemaTongs, and bootstrapping new databases with the Initialize template.
Write your first table as a JSON file with columns, indexes, and a primary key, then deploy it as a package.
Start the first package