Drizzle — Migrations¶
OWNER: urszula, maxim ALSO_USED_BY: boris, floris, hugo, jasper, arjan LAST_VERIFIED: 2026-03-26 GE_STACK_VERSION: drizzle-kit 0.31.x
Overview¶
Drizzle Kit generates SQL migration files from schema changes. GE uses
the generate + migrate workflow for production. The push command
is for local development ONLY. Agents must understand both flows and
know when each is appropriate.
Migration Workflow¶
Development (local iteration)¶
CHECK: Agent is iterating on schema design locally.
IF: Schema is not yet committed or reviewed.
THEN: Use drizzle-kit push for fast iteration.
IF: Schema is finalized and ready for review.
THEN: Switch to generate workflow.
Production (GE standard)¶
# Step 1: Generate migration SQL from schema diff
npx drizzle-kit generate
# Step 2: Review the generated SQL file
cat drizzle/migrations/NNNN_*.sql
# Step 3: Apply migration
npx drizzle-kit migrate
CHECK: Agent is deploying schema changes to staging or production.
IF: Using drizzle-kit push.
THEN: STOP. Push is forbidden outside local dev. Use generate + migrate.
Migration File Conventions¶
Naming¶
Drizzle Kit auto-numbers migrations: 0000_*.sql, 0001_*.sql, etc.
GE renames migration files to be descriptive:
0000_add_offline_status.sql
0001_abnormal_the_stranger.sql # drizzle-kit auto-name (acceptable)
0002_knowledge_and_dag_tables.sql # GE descriptive name (preferred)
0003_commercialization_core.sql
0006_security_database_fixes.sql
0007_team_routing.sql
CHECK: Agent has generated a new migration.
IF: The auto-generated name is meaningless (e.g., 0009_dizzy_falcon.sql).
THEN: Rename to describe the change: 0009_add_incident_tables.sql.
THEN: Update the corresponding entry in drizzle/migrations/meta/_journal.json.
Migration Structure¶
Every GE migration follows this structure:
-- Migration: {descriptive title}
-- Date: {YYYY-MM-DD}
-- Author: {agent name} ({agent role})
-- Source: {what triggered this — ticket, audit, discussion}
--
-- Implements:
-- - {change 1}
-- - {change 2}
--
-- Rollback: See {NNNN}_*_rollback.sql
-- ============================================================================
-- PART 1: {section title}
-- ============================================================================
{SQL statements}
-- ============================================================================
-- PART 2: {section title}
-- ============================================================================
{SQL statements}
-- ============================================================================
-- VERIFICATION QUERIES
-- ============================================================================
{Queries to verify the migration worked}
CHECK: Agent is writing a migration. IF: The migration has no header comment. THEN: Add one. Migrations are reviewed by Boris and must be traceable.
Rollback Strategy¶
Rollback Files¶
For every non-trivial migration, create a companion rollback file:
-- Rollback: 0006_security_database_fixes
-- WARNING: This drops security constraints. Use only in emergencies.
ALTER TABLE audit_log DISABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS audit_append_only ON audit_log;
DROP POLICY IF EXISTS audit_read ON audit_log;
-- ...
CHECK: Agent is writing a migration that alters existing tables. IF: The migration adds constraints, modifies columns, or drops objects. THEN: Write a rollback file. Additive-only migrations (new tables) do not require rollback files.
Rollback Execution¶
ANTI_PATTERN: Automating rollback in CI/CD pipelines. FIX: Rollbacks are manual, human-approved operations. Boris or the on-call engineer executes them after assessing impact.
Safe Migration Patterns¶
Adding a Column¶
-- SAFE: Adding a nullable column with a default
ALTER TABLE agents ADD COLUMN IF NOT EXISTS provider text DEFAULT 'claude';
CHECK: Agent is adding a column. IF: The column is NOT NULL without a default. THEN: STOP. This locks the table and rewrites all rows. THEN: Instead, add as nullable first, backfill, then add NOT NULL constraint.
Adding NOT NULL to Existing Column (3-step)¶
-- Step 1: Add default value
ALTER TABLE agents ALTER COLUMN provider SET DEFAULT 'claude';
-- Step 2: Backfill existing rows (in batches for large tables)
UPDATE agents SET provider = 'claude' WHERE provider IS NULL;
-- Step 3: Add NOT NULL constraint
ALTER TABLE agents ALTER COLUMN provider SET NOT NULL;
Adding an Index¶
-- SAFE: Concurrent index creation (does not lock table)
CREATE INDEX CONCURRENTLY IF NOT EXISTS agents_provider_idx ON agents (provider);
CHECK: Agent is adding an index to a production table.
IF: The table has more than 10,000 rows.
THEN: Use CREATE INDEX CONCURRENTLY. Standard CREATE INDEX locks
the table for the entire build duration.
ANTI_PATTERN: CREATE INDEX agents_provider_idx ON agents (provider); on production.
FIX: CREATE INDEX CONCURRENTLY ... — non-blocking.
Adding an Enum Value¶
-- PostgreSQL 10+: Adding enum values is safe
ALTER TYPE task_status ADD VALUE IF NOT EXISTS 'paused';
ANTI_PATTERN: Removing or renaming enum values. FIX: Enum values in PostgreSQL are append-only. To "remove" a value, add the new value, migrate data, then leave the old value unused.
Renaming a Column¶
-- Step 1: Add new column
ALTER TABLE agents ADD COLUMN display_name text;
-- Step 2: Backfill
UPDATE agents SET display_name = name;
-- Step 3: Update application code to use new column
-- Step 4: Drop old column (NEXT migration, after deployment)
ALTER TABLE agents DROP COLUMN name;
CHECK: Agent is renaming a column.
IF: Using ALTER TABLE ... RENAME COLUMN.
THEN: STOP. This breaks running application instances.
THEN: Use the add-backfill-switch-drop pattern across two migrations.
Zero-Downtime Migration Rules¶
GE deploys with rolling updates. During deployment, BOTH the old and new application versions run simultaneously. Migrations MUST be backward-compatible with the previous application version.
Safe operations (single migration): - Add a new table - Add a nullable column - Add a column with a default - Add an index (CONCURRENTLY) - Add an enum value
Unsafe operations (require multi-step migration): - Add NOT NULL constraint to existing column - Rename a column - Remove a column - Change a column type - Drop a table
CHECK: Agent is writing a migration. IF: The migration contains any "unsafe" operation. THEN: Split into multiple migrations deployed across multiple releases.
GE Migration Workflow¶
- Agent modifies
drizzle/schema/{domain}.ts. - Agent runs
npx drizzle-kit generate. - Agent reviews the generated SQL — verifies it matches intent.
- Agent renames migration file to be descriptive.
- Agent writes rollback file if migration alters existing objects.
- Agent adds header comments with author, date, and source.
- Agent adds verification queries at the bottom.
- Boris reviews the migration SQL before merge.
- Deployment applies migration via
drizzle-kit migratein init container.
Drizzle Kit Configuration¶
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './drizzle/schema.ts',
out: './drizzle/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL || 'postgresql://localhost:5432/admin_ui',
},
});
CHECK: Agent is modifying drizzle.config.ts.
IF: Changing schema or out paths.
THEN: Verify all existing migrations still resolve.
IF: Changing dialect.
THEN: STOP. GE uses PostgreSQL exclusively. Dialect is always postgresql.
Troubleshooting¶
Migration out of sync¶
# Check migration status
npx drizzle-kit check
# If schema and migrations diverge, generate a new migration
npx drizzle-kit generate
Drizzle Kit metadata corruption¶
The drizzle/migrations/meta/ directory contains _journal.json and
snapshot files. If corrupted:
ANTI_PATTERN: Manually editing _journal.json without understanding the format.
FIX: Let drizzle-kit manage its metadata. Only edit if renaming a migration file.
Cross-References¶
READ_ALSO: wiki/docs/stack/drizzle/index.md READ_ALSO: wiki/docs/stack/drizzle/schema-design.md READ_ALSO: wiki/docs/stack/drizzle/pitfalls.md READ_ALSO: wiki/docs/stack/postgresql/indexing.md READ_ALSO: wiki/docs/stack/postgresql/security.md