Here is the failure.
You clean up a schema:
ALTER TABLE users DROP COLUMN legacy_status;
The migration applies. New code is fine. Mid-deploy, an old app instance (or a Sidekiq worker that hasn't restarted yet) still runs a query that selects legacy_status. Rails may also have a cached schema that still lists the column. Requests start failing with "column does not exist" until every process has the new code and a fresh schema cache.
This is not a theoretical edge case. GitLab's "Avoiding downtime in migrations" guide opens the "Dropping columns" section with the same point: running processes expect those columns to exist, ActiveRecord caches the table schema at boot, and you need a multi-release sequence: ignore the column in code first, drop it in a later release, then remove the ignore rule after that.
Postgres itself makes the DDL look cheap. Per the ALTER TABLE docs, DROP COLUMN does not rewrite the table immediately; it marks the column invisible. The lock is still real, and the application failure mode is worse than the lock: code that still references the column breaks even when the statement finishes quickly.
The pattern that works
Change the schema in steps. Do not combine them into one locking "cleanup" migration.
-
Ignore it (release M): remove every code reference, including model validations, and deploy code that ignores the column. GitLab's
ignore_columnis one form of this. - Wait: make sure every instance, worker, and job is running that code. No process still reads or validates the column.
- Drop it (release M+1): only then run the drop. Remove the ignore rule in the release after that.
ALTER TABLE users DROP COLUMN legacy_status;
Same idea when you are adding shape instead of removing it: add the new column nullable, backfill in batches, then tighten constraints. Do not add NOT NULL and drop the old column in the same deploy as the code change that needs them.
When destructive SQL does show up in a migration file, read it twice: first work out exactly what gets dropped, then confirm the target names and that no app path still references them before you apply.
What reviewers should ask
- Does any still-running binary select, update, or validate this column?
- Are there views, triggers, or reporting queries that still name it?
- Is this drop in the same release as the code that stopped using it? If yes, split the release.
If an AI agent writes your migrations, give it the same checks as a standing rule before it ever suggests applying a DROP COLUMN: confirm the column name, verify no app references, and split the change across releases instead of one combined cleanup.
A free Cursor hook that steers the agent's shell commands away from running prisma migrate reset and drizzle-kit push --force against a non-local database is here: https://site-export-rescue.pages.dev/cursor-prisma-drizzle-guard/?utm_source=devto&utm_medium=article&utm_campaign=dont-drop-column-while-old-code-still-reads-it. It doesn't touch your SQL files or block the CLI for humans; pair it with rules that make the agent flag DROP COLUMN before the migration lands.
Drafted with AI assistance from GitLab's avoiding-downtime migration guide, the PostgreSQL ALTER TABLE documentation, and our own migration review rules, and checked against them.
Top comments (0)