zanith

Preflight checks

A pass that runs ahead of the migration, probing live data for the conditions that would make the DDL fail. Returns a list of PreflightFinding objects with sample offending rows — actionable enough to fix the data and re-run.

Why it exists

ALTER TABLE orders ADD CONSTRAINT … UNIQUE (sku) succeeds or fails on a single condition: are there duplicate sku values? The database tells you with an error. Preflight asks the same question first, with a probe query that returns up to sampleSize example duplicates — so you fix the data instead of staring at could not create unique index.

What it probes

Op kindProbeBlocks when
addUniqueSELECT cols, COUNT(*) … GROUP BY cols HAVING COUNT(*) > 1Duplicates exist on the new unique columns.
addForeignKeySELECT … WHERE child_col NOT IN (SELECT pk FROM parent)Orphan rows reference a parent row that doesn't exist.
alterColumnNullable (to NOT NULL)SELECT … WHERE col IS NULLNULL rows would violate the new constraint.

Other op kinds aren't probed — there's nothing in the live data that could make addColumn fail, for instance. The check returns an empty array for those.

The signature

TS
import { preflightCheck, describeFindings } from 'zanith';
 
const findings = await preflightCheck(adapter, plan.ops, {
sampleSize: 5, // default
skip: false, // default — set true when bootstrapping an empty DB
});
 
const blockers = findings.filter((f) => f.blocking);
if (blockers.length > 0) {
console.error(describeFindings(blockers));
process.exit(1);
}

preflightChecktakes the plan's ops and an adapter, runs each relevant probe in turn, and resolves to PreflightFinding[]. describeFindings formats the blocking findings as a single human-readable string — useful for CI output.

PreflightFinding shape

TS
interface PreflightFinding {
op: MigrationOp;
ok: boolean; // false when blocking
check: string; // short description of what was checked
blocking: boolean; // true → migration would fail
sample?: unknown[]; // up to sampleSize offending values
offendingCount?: number; // total count
message?: string; // free-form, surfaced in error reports
}

Where preflight fits in the lifecycle

StageWhat runs
generateCompare schemas → emit plan.
planRisk-score every op.
verifyApply on a shadow database — see [Shadow-DB verify](/migrate/verify).
**preflightCheck**Probe live data on the real database before any DDL.
upApply the plan — only proceeds if preflight had no blockers.

Preflight and shadow verify are complementary — shadow verify catches schema-shape errors; preflight catches data-shape errors. Both run on the production database (or a disposable copy of it) before up commits anything.

When to skip

skip: trueis for bootstrapping a fresh database where there's no data to probe. The CLI sets it automatically when it detects an empty schema; you usually don't need to set it by hand.