| name | drizzle-migrations |
| description | Drizzle Kit workflow for Volatio using idempotent migrations (generate + edit + migrate), never use push. Idempotent patterns prevent partial failure issues, enable safe re-runs, and work cleanly in CI. Use when schema changes, migration errors, or database sync issues occur. |
drizzle-migrations
When to Use
- Making schema changes to
packages/db/src/schema/
- Seeing "relation X does not exist" errors
- Resolving schema-database drift
- Migration fails with "already exists" errors
- Need safe, repeatable migrations for CI/CD
Quick Start
pnpm --filter @volatio/db generate
pnpm --filter @volatio/db migrate
docker exec volatio-postgres-1 psql -U volatio -d volatio_market -c "\d table_name"
CRITICAL: Understanding Drizzle Kit Prompts
Both push and generate will prompt when a diff is ambiguous (rename vs create). These prompts expect a real TTY (raw key events), so piping stdin (echo, yes, printf) won't work.
Is token column in password_reset_tokens table created or renamed from another column?
❯ + token create column
~ token_hash › token rename column
You cannot automate these prompts. The only way to handle them is:
- Run interactively in a terminal (local dev)
- Use custom migrations to make the change explicit (CI-safe)
- Reset the database to eliminate drift
Golden Path: Always Use generate + migrate (Never push)
The recommended workflow is:
pnpm --filter @volatio/db generate
pnpm --filter @volatio/db migrate
Why Not push?
drizzle-kit push directly modifies the database without creating migration files. This:
- Breaks CI/CD: No migration history to commit
- Loses reproducibility: Can't recreate the database state
- Causes prompts: Interactive questions about renames that can't be automated
- Risks data loss: No review step before schema changes
The Better Way: Idempotent Migrations
After running generate, always edit the SQL file to make it idempotent (safe to run multiple times).
Common Idempotency Patterns
CREATE TABLE IF NOT EXISTS "table_name" (...);
DO $$ BEGIN
CREATE TYPE "public"."my_enum" AS ENUM('value1', 'value2');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
DO $$ BEGIN
ALTER TYPE "public"."my_enum" ADD VALUE IF NOT EXISTS 'new_value';
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
ALTER TABLE "table_name" ADD COLUMN IF NOT EXISTS "column_name" text;
DO $$ BEGIN
ALTER TABLE "table_name" ADD CONSTRAINT "constraint_name"
FOREIGN KEY ("col") REFERENCES "other_table"("id");
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
CREATE INDEX IF NOT EXISTS "index_name" ON "table_name" ("column");
ALTER TABLE "table_name" DROP COLUMN IF EXISTS "column_name";
DROP TABLE IF EXISTS "table_name" CASCADE;
Real Example
Generated migration (NOT idempotent):
CREATE TYPE "public"."trade_intent" AS ENUM('open_long', 'close_long');
CREATE TABLE "paper_trades" (
"id" uuid PRIMARY KEY,
"symbol" text NOT NULL
);
ALTER TABLE "paper_trades" ADD COLUMN "intent" "trade_intent";
CREATE INDEX "idx_trades_symbol" ON "paper_trades" ("symbol");
Fixed migration (idempotent):
DO $$ BEGIN
CREATE TYPE "public"."trade_intent" AS ENUM('open_long', 'close_long');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
CREATE TABLE IF NOT EXISTS "paper_trades" (
"id" uuid PRIMARY KEY,
"symbol" text NOT NULL
);
ALTER TABLE "paper_trades" ADD COLUMN IF NOT EXISTS "intent" "trade_intent";
CREATE INDEX IF NOT EXISTS "idx_trades_symbol" ON "paper_trades" ("symbol");
Why Idempotency Matters
- Partial failures: If migration fails halfway, you can re-run it
- Development flexibility: Can run same migration against multiple local DBs
- CI safety: Tests can run migrations without worrying about pre-existing state
- Merge conflicts: Multiple branches can have overlapping schema changes
CI: ONLY use migrate
CI should only run drizzle-kit migrate against idempotent migrations committed to git.
- Start Postgres (may have partial state from cache)
- pnpm --filter @volatio/db migrate
- Run tests
Local Development: Generate + Edit + Migrate
pnpm --filter @volatio/db generate
pnpm --filter @volatio/db migrate
docker compose down -v && docker compose up -d
pnpm --filter @volatio/db migrate
Key Commands
| Command | Purpose | When |
|---|
drizzle-kit push | Push schema directly (interactive) | Local dev ONLY |
drizzle-kit generate | Generate SQL migration files | Before deploy |
drizzle-kit migrate | Apply pending migrations (non-interactive) | CI, deploy |
drizzle-kit check | Verify migration consistency | Before merge |
drizzle-kit generate --custom | Create empty migration for manual SQL | Renames, drift fixes |
CRITICAL: Safe Column Renames
Drizzle Kit treats column renames as DROP + ADD = DATA LOSS.
Wrong Way (Loses Data)
triggeredAt: timestamp('triggered_at'),
detectedAt: timestamp('detected_at'),
Safe Way: Custom Migration
pnpm --filter @volatio/db generate -- --custom --name=rename-triggered-to-detected
ALTER TABLE signals RENAME COLUMN triggered_at TO detected_at;
pnpm --filter @volatio/db migrate
Fixing Schema Drift with Custom Migrations
When your DB is out of sync with your schema (e.g., after failed pushes or manual changes):
Step 1: Create a custom migration
pnpm --filter @volatio/db generate -- --custom --name=fix-schema-drift
Step 2: Write idempotent SQL
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'password_reset_tokens'
AND column_name = 'token_hash'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'password_reset_tokens'
AND column_name = 'token'
) THEN
ALTER TABLE public.password_reset_tokens RENAME COLUMN token_hash TO token;
END IF;
END $$;
ALTER TABLE public.password_reset_tokens
DROP COLUMN IF EXISTS created_at;
Step 3: For tables that need complete reset
DROP TABLE IF EXISTS public.scheduled_release_captures CASCADE;
CREATE TABLE public.scheduled_release_captures (
"id" text PRIMARY KEY NOT NULL,
"symbol" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
Step 4: Apply the migration
pnpm --filter @volatio/db migrate
Once applied, your DB won't be in an ambiguous state anymore, so future runs don't trigger rename prompts.
TimescaleDB Hypertables
Some tables are hypertables with special constraints:
docker exec volatio-postgres-1 psql -U volatio -d volatio_market -c \
"SELECT * FROM timescaledb_information.hypertables;"
Limitations:
- Cannot rename columns on hypertables easily
- Cannot change time column after creation
- Some ALTER TABLE operations restricted
pnpm --filter @volatio/db setup:hypertables
Verify Schema-Database Sync
Before migrations:
docker exec volatio-postgres-1 psql -U volatio -d volatio_market -c "\d table_name"
cat packages/db/src/schema/table-name.ts
Troubleshooting
"Is X created or renamed?" Prompt in CI
This breaks CI. You cannot automate the answer.
Solution: Write a custom migration instead:
pnpm --filter @volatio/db generate -- --custom --name=fix-ambiguous-column
pnpm --filter @volatio/db migrate
"Column X does not exist"
docker compose down -v && docker compose up -d
pnpm --filter @volatio/db migrate
pnpm --filter @volatio/db generate -- --custom --name=add-missing-column
Migration Conflicts After Branch Merge
pnpm --filter @volatio/db drizzle-kit check
rm -rf packages/db/drizzle/
pnpm --filter @volatio/db generate -- --name=consolidated
Don'ts
- Don't use
push in CI - prompts require TTY, breaks pipelines
- Don't use
generate in CI - can also prompt for ambiguous changes
- Don't pipe stdin to
push - echo, yes, printf won't work
- Don't rename columns without custom migration SQL
- Don't skip reviewing generated SQL - catch data loss
No Rollback Support
Drizzle Kit has NO drizzle-kit down command. To undo:
- Write a forward migration that reverses changes
- Or restore from database backup
References