* simplify CODEOWNERS * dont .gitignore AGENTS.md * AGENTS.md to document previous CODEOWNERS responsibilities * update from https://developers.mattermost.com/contribute/more-info/server/schema-migration-guide/ * CREATE INDEX CONCURRENTLY now vetted * rewrite and move to README.md * dont limit to 80 chars * rewrite webapp AGENTS.md and add to README.md --------- Co-authored-by: Mattermost Build <build@mattermost.com> |
||
|---|---|---|
| .. | ||
| postgres | ||
| migrations.list | ||
| README.md | ||
Database Migration Rules
Enforce these rules to prevent table locks and performance degradation on large production databases (100M+ posts).
General Rules
- Each migration should do one thing. Don't combine unrelated schema changes.
- Prefer additive changes (new columns, new tables) over destructive ones.
- Schema migrations must ALWAYS be backwards compatible until the last ESR.
- Test migrations against a database with realistic data volume, not just a dev-sized dataset. An
EXPLAIN ANALYZEthat looks fine on 12M posts may behave very differently at 100M posts. - If a shipped migration is broken, do not modify it. Add a new migration to correct.
File Naming and Structure
Migration files follow the convention {sequence_number}_{description}.{up|down}.sql and live in db/migrations/{driver_name}/. After creating migration files, run make migrations-extract to update db/migrations/migrations.list. Merge upstream before submitting a PR to avoid sequence number collisions.
Table Locking
Never write a migration that locks an entire table. Common operations that acquire table-level locks:
ALTER COLUMN(type change) — rewrites the tableCREATE INDEXwithoutCONCURRENTLYADD FOREIGN KEYwithoutNOT VALIDLOCK TABLE
Column Type Changes
As noted above, ALTER COLUMN (type change) rewrites the entire table and blocks concurrent DML: strongly avoid. A real-world example took 8+ hours on a large customer database. When unavoidable, use a multi-release phased approach:
- Release N: add the new column.
- Release N+1: backfill data via background jobs; use triggers for ongoing updates.
- Release N+2 (ESR): start using the new column in code.
- Release N+3 (ESR): drop the old column.
Batch Updates
Never do a full-table UPDATE in a migration. Process data in batches in a job at runtime:
UPDATE foo SET col = value
WHERE id IN (
SELECT id FROM foo WHERE id > :offset ORDER BY id ASC LIMIT 100
);
Store the offset and resume across job runs.
Adding Unique Constraints
Do not add a unique constraint directly. Create the index concurrently first, then attach it:
CREATE UNIQUE INDEX CONCURRENTLY constraint_name ON foo (bar);
ALTER TABLE foo ADD UNIQUE USING INDEX constraint_name;
Note that this cannot be done inside a transaction block, so it must be in a separate migration file. Use the -- morph:nontransactional comment to disable transactions for that migration.
Foreign Key Constraints
Avoid foreign key constraints. Adding a foreign key takes a SHARE ROW EXCLUSIVE lock, limiting concurrent activity to SELECT queries only. If truly needed, consider adding the constraint with NOT VALID to avoid the full-table scan under lock.