Column names in code must exist in the schema
On this page
Reject code that references a column name in an UPPER_SNAKE_CASE_COLS array when that column doesn’t exist in any committed schema.sql or migrations/*.sql. Catches the wrong-migration footgun before the migration runs - independent of which apply script was or wasn’t executed.
What this catches#
Typical failure shape: db/content.js declares POST_LIST_COLS = ['id', 'title', 'country', 'alternates', 'noindex'], but schema.sql and migrations/*.sql only declare id and title - the migrations that add the other columns haven’t been applied. Every /api/content?type=posts request 500s after deploy.
The check:
- Walk
**/schema.sqland**/migrations/*.sql- extract every column declared byCREATE TABLEbody orALTER TABLE … ADD COLUMN - Walk
**/db/**/*.{js,ts},**/database/**/*.{js,ts},**/models/**/*.{js,ts}- findconst FOO_COLS = [...]/const FOO_COLUMNS = [...]/const FOO_FIELDS = [...]declarations - For every string literal in such an array, verify the name exists in the declared-columns union
- BLOCK on any mismatch
The directory scope is narrow on purpose - limited to db/ / database/ / models/ so unrelated UPPER_SNAKE_COLS constants in business logic don’t accidentally trigger the gate.
Fix#
Three options:
1. Add the missing column (the case where the migration is missing):
-- migrations/2026-05-add-country-column.sql
ALTER TABLE posts ADD COLUMN country TEXT;
ALTER TABLE posts ADD COLUMN alternates TEXT;
ALTER TABLE posts ADD COLUMN noindex INTEGER;
…and run the wrapper. Both migration-script-explicit-env and migration-verification-step will then govern the wrapper.
2. Remove the reference from code (the case where the column was never going to ship):
// db/content.js
const POST_LIST_COLS = ['id', 'title']; // dropped country/alternates/noindex
3. Suppress per-declaration (computed field / alias / external mapping):
// appframes:disable-next-line database/schema-vs-code-drift
const POST_VIRTUAL_COLS = ['id', 'computed_url', 'derived_status'];
Suppression scope#
The frame supports three levels:
- File-level:
// appframes:disable database/schema-vs-code-driftanywhere in the file - Line-level: marker on the line immediately above the const declaration
- Whitelist entry: standard
.appframes/_canonical/whitelist.toml(useframe = "database/schema-vs-code-drift")
What it does NOT do#
- No type checking. It only verifies “this column name is declared somewhere.” Type / nullability / default-value mismatches need a real type system.
- No dialect awareness. Postgres / SQLite / MySQL all express ALTER differently; the regex set is broad enough to catch most shapes but not exhaustive.
- No live DB connection. Can’t tell whether your deployed schema has the column - only whether committed schema does. Pair this with
migration-verification-stepfor the deploy-time confidence.
Generalizes to#
Any code-vs-schema convention where columns are named in source:
- Drizzle ORM / Prisma - they generate types from schema, so they self-protect; this frame doesn’t add value there
- Raw SQL builders (knex, kysely, query-as-string) - exactly the population this targets
- Python:
COLUMNS = [...]in DAO files - Go:
var PostListCols = []string{...}(extend the regex to match Go consts)
Adding more languages requires extending codeColumnArrayRegex + codeApplicableFileForSchemaDrift.
WARN lets the push through and records the finding. Turn frames on per repo on the dashboard's Policy page - see choosing what the gate checks.
Source on GitHub Live demo How it works Questions: contact@nimblegate.com