CtrlK
BlogDocsLog inGet started
Tessl Logo
PluginPublic

testland/migration-operation-taxonomy

Use whenever someone asks if a database migration is safe to run in production: rename or drop a column, add NOT NULL or a default, add an index or foreign key, change a type on a large PostgreSQL or MySQL table. Classifies each statement (additive, locking, breaking, data-loss) by lock and rewrite.

Quality

94%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

engine-quick-reference.mdreferences/

Engine quick reference and worked example

Consolidated lock, rewrite, and severity-divergence tables for the checks in SKILL.md, plus a full worked classification. The classification procedure itself lives in SKILL.md; this file is the lookup companion.

PostgreSQL quick reference

PostgreSQL, current manual (fetched 2026-07-19), version notes called out where behavior changed.

StatementLockRewriteSource
ALTER TABLE (baseline)ACCESS EXCLUSIVE unless noteddepends on clausesql-altertable
ADD COLUMN nullable or constant DEFAULTACCESS EXCLUSIVE, briefno rewrite from PG 11release 11.0, sql-altertable
ADD COLUMN volatile DEFAULT, stored generated, identityACCESS EXCLUSIVEfull rewrite of table and indexessql-altertable
ALTER COLUMN ... TYPEACCESS EXCLUSIVEnormally full rewrite, narrow binary-coercible exceptionsql-altertable
SET NOT NULLACCESS EXCLUSIVEscans whole table unless a valid CHECK proves no NULLsql-altertable
ADD FOREIGN KEYSHARE ROW EXCLUSIVE on both tablesno rewritesql-altertable
VALIDATE CONSTRAINTSHARE UPDATE EXCLUSIVE, plus ROW SHARE on the referenced table for an FKno rewritesql-altertable
CREATE INDEXSHARE, blocks writes not readsn/aexplicit-locking, sql-createindex
CREATE INDEX CONCURRENTLYSHARE UPDATE EXCLUSIVE, two scans, no transaction blockn/aexplicit-locking, sql-createindex
DROP COLUMNACCESS EXCLUSIVE, fastcolumn not physically removed, space reclaimed over timesql-altertable
TRUNCATEACCESS EXCLUSIVE, blocks all concurrent operationsreclaims space immediately, rolls back with the transactionsql-truncate

MySQL 8.0 InnoDB quick reference

MySQL 8.0 InnoDB, all rows from innodb-online-ddl-operations.

StatementIn placeConcurrent DMLRebuilds tableNotes
ADD COLUMNyesyes, except auto-incrementno with INSTANTINSTANT default from 8.0.12; last position only before 8.0.29; INPLACE rebuilds
DROP COLUMNyesyesyesINSTANT default from 8.0.29
Change column data typenonoyes"only supported with ALGORITHM=COPY"
Widen VARCHAR across the 256-byte linenonoyesone length byte below 256 bytes, two at 256 and above
Narrow VARCHARnonoyes"requires a table copy (ALGORITHM=COPY)"
Add secondary indexyesyesno"The table remains available for read and write operations while the index is being created."
Add foreign keyonly with foreign_key_checks disabledyesnootherwise "only the COPY algorithm is supported"
Set column NOT NULLyesyesyesneeds strict SQL mode; "fails if the column contains NULL values"

The three divergences that flip a severity

  1. Index creation. Blocks writes on PostgreSQL without CONCURRENTLY; permits concurrent DML on MySQL 8.0. Category 3 versus category 1.
  2. Column type change. Rewrites on PostgreSQL under ACCESS EXCLUSIVE, and on MySQL forces ALGORITHM=COPY with no concurrent DML. Critical on both, but for different reasons, and the MySQL mitigation (a copy-based online schema change tool) is not the PostgreSQL one.
  3. Adding a foreign key. A metadata-cheap SHARE ROW EXCLUSIVE on PostgreSQL that spreads to a second table, versus a full table copy on a default-configured MySQL. Warning versus Critical.

Worked example

Target: PostgreSQL 16, users 40M rows, orders 180M rows, products 2M rows, sizes taken from a prior data-load migration in the same series.

ALTER TABLE users DROP COLUMN legacy_status;
ALTER TABLE orders ADD COLUMN shipped_at timestamptz NOT NULL;
ALTER TABLE orders ADD COLUMN region text NOT NULL DEFAULT 'unknown';
CREATE INDEX ix_orders_status ON orders (status);
ALTER TABLE products ADD CONSTRAINT fk_category
  FOREIGN KEY (category_id) REFERENCES categories(id);

Classification:

#StatementCategorySeverityBasis
1DROP COLUMN users.legacy_status5 Breaking + 6 Data-lossCriticalColumn becomes invisible to SQL immediately; space not reclaimed, so the fast completion is not evidence of a cheap operation
2ADD COLUMN orders.shipped_at NOT NULL no default7 Unsafe defaultCriticalSET NOT NULL requires no NULL rows exist; verify the add-with-NOT-NULL outcome on the target before merge
3ADD COLUMN orders.region NOT NULL DEFAULT 'unknown'1 AdditiveInfoConstant default, no rewrite from PG 11; would be Critical on PG 10
4CREATE INDEX ix_orders_status ON orders (status)3 LockingCriticalSHARE lock blocks inserts, updates, deletes on a 180M-row table for the whole build
5ADD CONSTRAINT fk_category ... REFERENCES categories(id)4 Lock-escalating + 8 Index-missing FKWarningSHARE ROW EXCLUSIVE on products and on categories; no index on products.category_id

Note how statements 2 and 3 differ by one clause and three severity bands, and how statement 4 would be Info against MySQL 8.0.

SKILL.md

tile.json