Schema

Sometimes the right answer is no

Before altering a column, SchemaStack counts the rows, classifies the operation against the engine you actually run, estimates how long it will take and names what it will lock — and while it runs, the API says 503 rather than hanging.

SchemaStack team22 Aug 2026Verified working · 22 Aug 2026Docs

Changing a column type in a spreadsheet is instant. Changing one in a 2-million-row Postgres table takes four minutes and blocks every read for the duration.

Most tools in this category paper over that distinction, because the honest version is unpleasant: your click will take four minutes and your application will be down. So they run the ALTER and hope you weren't busy.

We'd rather tell you.

The changetotal: TEXT → NUMERICRows counted2.4 millionClassifiedfull table rewriteEstimated≈ 4 minPostgreSQL · blocks reads and writesAccessExclusive for the whole rewritepick your hour — nothing schedules this for youMySQL / InnoDB · blocks writes onlyreads continue; the same change costs less hereone blocking migration per table, everWhile it runs, data endpoints answer 503 with Retry-After — a client that can wait, rather than a request that hangs on a lock.
The dry run classifies the change against the engine you actually run, estimates duration from the real row count, and names what it will lock — before anything is altered.

The dry run

Before anything is altered, the change is put through an analyzer that answers four questions in order.

How many rows? Estimated from the table itself, because a type change on 800 rows and the same change on 2.4 million are different events and only one of them needs a plan.

What operation is this, on the engine you actually run? PostgreSQL and MySQL/InnoDB are classified separately, and non-InnoDB MySQL separately again, because they genuinely differ. Some examples that come straight out of the analyzer:

ChangePostgreSQLMySQL / InnoDB
Change a column typefull table rewrite, AccessExclusive — blocks reads and writestable rebuild, blocks writes only
Add NOT NULLfull table scan to validate, AccessExclusivein-place, concurrent writes allowed
Drop NOT NULLinstant metadata changein-place
Shrink a VARCHARrewrite if the length actually shrinksrebuild
Add a column with a defaultinstant on PG 11+, rewrite before thatinstant

How long? Duration is estimated from the row count against separate per-row rates for a rewrite, a full scan and an index build — because those three cost differently and lumping them together produces an estimate you'd be right to ignore.

So what will it lock? The verdict names it: blocks reads and writes, blocks writes only, or neither.

While it runs

A migration that blocks is going to block. The question is what your API does about it.

It answers 503 with a Retry-After header, and a body saying which table is migrating. Not a hung request holding a connection while it waits on a lock, and not a timeout that looks like an outage. A client that understands Retry-After backs off and comes back; one that doesn't at least gets an error it can log properly.

Two rules keep this bounded. Only one blocking migration per table may start — a second is refused rather than queued behind the first, so you can't accumulate a pile-up you didn't intend. And a migration cannot hold a table indefinitely, regardless of how badly its duration was estimated; there's a ceiling, and passing it ends the migration rather than extending the outage.

Why an estimate is still worth having

An estimate that's wrong by a factor of two is still the difference between "do this now" and "do this at 3am". The failure mode we're avoiding isn't imprecision — it's surprise: finding out that a column type change was a four-minute outage by watching it happen.

What it doesn't do (yet)

  • Nothing schedules it for you. The dry run tells you this will block reads for four minutes, and then choosing the hour is entirely on you. No maintenance window, no queue, no "apply at".
  • The blast radius of a cascading foreign key isn't computed. The guard checks the table you're changing, not the tables a cascade would reach. A change that fans out is estimated as though it doesn't.
  • Estimates come from row counts and fixed per-row rates, not from your hardware, your disk, your current load or your Postgres version's specific behaviour. A busy database will be slower than the number says.
  • 503 protects data endpoints. It doesn't hold back another tool connecting to your database directly — this is your database, and we don't own the only door to it.
  • No progress on a running migration. You know it started and you know when it ends.
  • No rollback of a completed migration. Reversing a type change is another migration, with its own dry run and its own cost.
  • The one-blocking-migration-per-table rule is per table, not per database. Several tables can migrate at once, and the combined load isn't estimated.

The schema migrations guide covers the mechanics, and what happens when the schema moves without us is the other half of this story — changes from outside, rather than changes you asked for.

Verified 22 Aug 2026: 52 tests — the impact analyzer's PostgreSQL, MySQL/InnoDB, non-InnoDB and aggregation cases (27), the migration guard interceptor including its active-blocking-migration behaviour (16), and end-to-end column migrations against a live database covering type changes, renames, constraint changes and multi-change batches (9).