SchemaSmith Concept

Drop Control

SchemaQuench removes objects when they leave your product definition. Drop control offers two approaches: configuration flags per object type, and PreventDrop guards for a single table or a whole environment.

By the SchemaSmith Team · Last reviewed

A cascade of configuration tiers deciding whether a database object removed from a product definition is dropped or preserved

Multiple settings in SchemaSmith share the same story: start conservative, align in stages, enforce once you're ready.

Overview

When you remove a table, column, foreign key, constraint, statistics object, or index from your product definition, SchemaQuench has to decide what to do with the copy that's still in the database. The drop-control flags are that decision. Each one governs a single object type's removal by absence — the case where an object exists in the target but no longer appears anywhere in the schema package.

Seven of the eight flags default to true, so the database stays in sync as objects leave the product over time. The eighth, DropUnknownIndexes, defaults to false. Every flag resolves through the same tiered cascade, and a false set anywhere in that cascade is a hard guardrail that no lower tier can override.

The flags are not the whole picture. They gate the by-absence drop pass while a table's definition is still present — so a table you delete from the package has nothing left to carry a false. PreventDrop covers that case from two directions: a sticky per-table guard persisted in the database, and an environment-wide setting that suppresses every by-absence drop for a whole run.

The eight flags

Each flag targets one object type and gates only removal by absence. Seven default to true; DropUnknownIndexes is the lone default-false flag, because it removes indexes SchemaSmith never created.

Flag Governs removal-by-absence of… Default Engines
DropTablesRemovedFromProduct Tables the product previously managed, absent from every table JSON true All
DropColumnsRemovedFromProduct Columns absent from the table JSON true All
DropForeignKeysRemovedFromProduct Foreign keys absent from the table JSON true All
DropCheckConstraintsRemovedFromProduct Table-level CHECK constraints absent from the JSON true All
DropExcludeConstraintsRemovedFromProduct EXCLUDE constraints absent from the JSON true PostgreSQL
DropStatisticsRemovedFromProduct User-created statistics absent from the JSON true SQL Server, PostgreSQL
DropIndexesRemovedFromProduct Product-owned indexes absent from the JSON true All
DropUnknownIndexes Out-of-band indexes SchemaSmith never managed, on managed tables false All

Only removal by absence is gated

An object whose definition merely changed — a foreign key with a new referenced column, a check constraint with a new expression, an index with different columns — is always dropped and recreated so the new definition takes effect, regardless of these flags. And some objects are never in scope at all: a primary key is never dropped by a drop-control flag, and auto-created statistics are left alone — only the named objects your product defines can be dropped by absence.

The four-tier cascade

Every drop-control flag resolves the same way: SchemaQuench composes an effective value from a stack of tiers, evaluated from the broadest scope to the narrowest.

The tiers

  • Environment — the flag in SchemaQuench.settings.json, or the SmithySettings_<FlagName> environment variable. Controls every product deployed in that environment.
  • Product — the flag in Product.json. Controls a single product wherever it deploys.
  • Template — the flag in Template.json. Controls a single template within a product.
  • Table — the flag in a table's .json file. Protects that one table's objects. Six of the eight flags reach this tier; DropTablesRemovedFromProduct and DropUnknownIndexes have no table tier and stop at the template level.
// SchemaQuench.settings.json — suppress column drops environment-wide
{ "DropColumnsRemovedFromProduct": false }

Explicit false is sticky

A false set at any tier locks the effective value to false for all lower tiers. A true at a lower tier overrides an inherited true but can never override an ancestor's false. Absent (not set) inherits from the tier above. This makes a higher-tier false a hard guardrail: an environment that sets false suppresses the drop regardless of what any product or template declares.

Per-table tightening

The table tier can only tighten. A table can set its own false to protect its objects even when higher tiers permit the drop — but it cannot set true to re-enable a drop a higher tier has suppressed.

// Tables/dbo.AuditLog.json — protect this table's own columns
{ "Name": "AuditLog", "DropColumnsRemovedFromProduct": false }

PreventDrop — protection that outlives the definition

The Drop…RemovedFromProduct flags help while a table is still in the package, but they share a blind spot: they gate the by-absence drop pass, so they only “see” a table whose definition is still present. The moment you delete a table's .json, there is nothing left to carry a false — and 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.

PreventDrop is a per-table boolean set in the table's .json, 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 SQL Server it is a PreventDrop extended property stamped on the table; on PostgreSQL, MySQL, and 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 simply deleting its JSON, since that is exactly the case the stickiness defends against. Two ways to remove protection:

  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. Now remove the table from the package on a later deployment and it drops normally.
  2. Drop via migration script. Write a migration script that drops the table explicitly. Migration scripts run outside the drop-by-absence pass, so they are not gated by PreventDrop at all.

Tip

Reach for the refresh-then-remove path when you want the removal to flow through the normal declarative pipeline; reach for the migration script when you want the drop recorded as an explicit, reviewable step in the package.

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 you name one at a time. Sometimes you want the opposite default: an entire environment where the deployment tool is simply not allowed to remove anything by omission — production, a shared staging fleet, any target where an accidental drop is unacceptable. The environment-level PreventDrop setting is that blanket guardrail.

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 and exclude constraints, statistics, 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. It is 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 — so you get a precise list of what was not removed (objectType + objectName) without the run failing. Read the manifest to see whether a package genuinely intends those removals; if it does, deploy that package to an unprotected environment, or clear protection deliberately.

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 and putting it back, modifying a constraint, recreating a computed column whose expression changed — reconciles exactly as it always does. Those drops are part of applying your declared schema, not removing something you left out, so protected mode never blocks them.

How the three layers relate

Three layers, narrowest-winning intent:

  • Drop…RemovedFromProduct cascade — per-object-type, four-tier (environment → product → template → table). Fine-grained: “should this kind of by-absence drop run here?”
  • Per-table PreventDrop — a sticky, persisted guard on one named table, surviving its removal from the package.
  • Environment PreventDrop — a whole-run blanket: “for this deployment, don't remove anything by absence.” The simplest possible answer when the rule is “this environment never drops.”

They compose. The environment switch is the outermost guarantee; the cascade and per-table guards still apply beneath it for environments that aren't fully locked down. Behavior is identical on every supported engine — the persistence mechanism differs per engine, but the contract is the same everywhere.

Turning enforcement on safely

DropUnknownIndexes defaults to false for good reason: most teams adopting SchemaSmith inherit environments with years of index drift. Turn it on after capturing every needed index in your repository — not before. The same staged approach applies to any enforcement flag you're switching on, DropTablesRemovedFromProduct included.

  1. Extract and inventory. Use SchemaTongs to capture your current schema, then review what comes out — you may find objects in production that don't exist in dev, duplicates, or contradictions.
  2. Build up the repository in stages. Add objects to your schema package in controlled stages, maybe one schema area at a time. Deploy each stage, verify performance, confirm the deployment window is acceptable.
  3. Keep enforcement off during alignment. Leave DropUnknownIndexes and DropTablesRemovedFromProduct off until you're confident the repository represents the complete desired state — otherwise you risk dropping objects you haven't captured yet.
  4. Turn on enforcement to prevent drift from returning. Once alignment is verified across environments, enable the flags. Now SchemaSmith stops drift the moment you turn your back.

Why big-bang alignment fails

Bringing everything into alignment in a single deployment is one of the most common mistakes teams make: hours-long deployment windows when every index gets rebuilt, unexpected performance degradation when indexes are dropped that weren't captured yet, or accidental data loss when a table is dropped that still had dependencies. Add what's safe, deploy, verify, repeat.

Two rollback-friendly postures

Turning a drop off is one way to protect data. Keeping the drop on but recoverable is another.

Never drop. Set DropTablesRemovedFromProduct: false in production. A table that leaves the product is left in place; SchemaQuench touches nothing. The trade-off is accumulation — the database collects orphaned tables until you retire them by hand.

Drop but recoverable. Keep auto-drops on and install the recyclebin hooks. A removed table moves into a recyclebin instead of being destroyed, auto-restores if it returns to the product, and is purged after a retention window.

See Recyclebin for the hooks contract, registry, and cleanup scheduling. The recyclebin protects tables only — a dropped column is never caught.

Drop control in patch packages

A patch package is a subset of a full product, so its drops need special handling. When SchemaQuench deploys a subset against a target that already has the full product installed, it would normally read every object absent from the patch as "should be dropped."

SchemaShears prevents that: it stamps the drop-suppression flags to false in the emitted patch's Product.json, so objects outside the patch are preserved rather than reconciled away. As of v2.2.0, all seven stamped flags are enforced.

Need a patch to drop something on purpose? The --AllowDrops switch re-enables specific categories. See SchemaShears for the manifest workflow and the full category list.

Platform notes

Two flags are engine-specific; the rest apply to every supported engine.

Flag SQL Server PostgreSQL MySQL MariaDB
DropExcludeConstraintsRemovedFromProduct
DropStatisticsRemovedFromProduct
All other drop-control flags

EXCLUDE constraints are a PostgreSQL feature, so DropExcludeConstraintsRemovedFromProduct is accepted but has no effect on any other engine. MySQL and MariaDB have no separate statistics objects, so DropStatisticsRemovedFromProduct does not apply there. And on MySQL and MariaDB, DropForeignKeysRemovedFromProduct now governs foreign-key cleanup on its own — that cleanup previously required enabling DropUnknownIndexes, matching how SQL Server and PostgreSQL have always behaved.