drizzle-kit generate has been broken since migration 0008 (stale snapshots). The actual workflow is hand-written SQL + manual _journal.json registration. Updated CLAUDE.md and database-migrations.md to reflect reality and prevent future migrations from silently failing to apply.
3.6 KiB
Database Migrations
This project uses drizzle-orm for database schema management with hand-written SQL migrations.
Overview
- Schema definition:
apps/server/db/schema.ts(drizzle-orm table definitions) - Migration output:
apps/server/drizzle/directory (SQL files +meta/_journal.json) - Config:
drizzle.config.ts - Runtime migrator:
apps/server/db/ensure-schema.ts(callsdrizzle-orm/better-sqlite3/migrator)
How It Works
On every server startup, ensureSchema(db) runs all pending migrations from the apps/server/drizzle/ folder. Drizzle tracks applied migrations in a __drizzle_migrations table so only new migrations are applied. This is safe to call repeatedly.
The migrator discovers migrations via apps/server/drizzle/meta/_journal.json — not by scanning the filesystem. A migration SQL file that isn't registered in the journal will never be applied.
Workflow
Making schema changes
Do NOT use drizzle-kit generate — the snapshots in meta/ have been stale since migration 0008 and drizzle-kit generate will produce incorrect interactive prompts. All migrations since 0008 are hand-written.
- Edit
apps/server/db/schema.tswith your table/column changes - Create a new SQL migration file:
apps/server/drizzle/NNNN_descriptive_name.sql- Number it sequentially (check the last migration number)
- Write the SQL (ALTER TABLE, CREATE INDEX, etc.)
- Register it in the journal: edit
apps/server/drizzle/meta/_journal.json- Add a new entry at the end of the
entriesarray:{ "idx": <next_number>, "version": "6", "when": <unix_timestamp_ms>, "tag": "NNNN_descriptive_name", "breakpoints": true } idx: sequential (previous entry's idx + 1)tag: migration filename without.sqlextensionwhen: any timestamp in milliseconds (e.g., previous + 86400000)
- Add a new entry at the end of the
- Commit both the SQL file and the updated
_journal.jsontogether - Run
npm run build && npm linkto pick up the changes
Example
Adding a column to an existing table:
-- apps/server/drizzle/0032_add_comment_threading.sql
ALTER TABLE review_comments ADD COLUMN parent_comment_id TEXT REFERENCES review_comments(id) ON DELETE CASCADE;
CREATE INDEX review_comments_parent_id_idx ON review_comments(parent_comment_id);
// In meta/_journal.json entries array:
{
"idx": 32,
"version": "6",
"when": 1772323200000,
"tag": "0032_add_comment_threading",
"breakpoints": true
}
Applying migrations
Migrations are applied automatically on server startup. No manual step needed.
For tests, the same ensureSchema() function is called on in-memory SQLite databases in apps/server/db/repositories/drizzle/test-helpers.ts.
Rules
- Always hand-write migration SQL. Do not use
drizzle-kit generate(stale snapshots). - Always register in
_journal.json. The migrator reads the journal, not the filesystem. - Commit SQL + journal together. A migration file without a journal entry is invisible to the migrator.
- Never use raw CREATE TABLE statements for schema initialization. The migration system handles this.
- Migration files are immutable. Once committed, never edit them. Make a new migration instead.
- Test with
npm testafter creating migrations to verify they work with in-memory databases. - Keep schema.ts in sync. The schema file is the source of truth for TypeScript types; migrations are the source of truth for database DDL. Both must reflect the same structure.