Files
gh-jezweb-claude-skills-ski…/templates/drizzle-migrations-workflow.md
2025-11-30 08:25:01 +08:00

9.8 KiB

Drizzle Migrations Workflow for Neon Postgres

This guide shows the complete workflow for managing database migrations with Drizzle ORM and Neon Postgres.

Initial Setup

1. Install Dependencies

npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit

2. Create Configuration File

Create drizzle.config.ts in your project root:

import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  schema: './db/schema.ts',
  out: './db/migrations',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!
  }
});

3. Add Scripts to package.json

{
  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:studio": "drizzle-kit studio",
    "db:push": "drizzle-kit push"
  }
}

Schema Definition

Create your schema in db/schema.ts:

import { pgTable, serial, text, timestamp, boolean, integer } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  avatarUrl: text('avatar_url'),
  createdAt: timestamp('created_at').defaultNow(),
});

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  userId: integer('user_id').notNull().references(() => users.id),
  title: text('title').notNull(),
  content: text('content'),
  published: boolean('published').default(false),
  createdAt: timestamp('created_at').defaultNow(),
});

export const comments = pgTable('comments', {
  id: serial('id').primaryKey(),
  postId: integer('post_id').notNull().references(() => posts.id, { onDelete: 'cascade' }),
  userId: integer('user_id').notNull().references(() => users.id),
  content: text('content').notNull(),
  createdAt: timestamp('created_at').defaultNow(),
});

Migration Workflow

Step 1: Generate Migration

After creating or modifying your schema, generate a migration:

npm run db:generate

This creates a new migration file in db/migrations/ directory:

  • db/migrations/0000_initial.sql - First migration
  • db/migrations/0001_add_published_column.sql - Second migration
  • etc.

What happens:

  • Drizzle compares your schema with the database
  • Generates SQL migration file with CREATE/ALTER/DROP statements
  • Creates snapshot in db/migrations/meta/ for future comparisons

Step 2: Review Migration

ALWAYS review generated SQL before applying!

Example generated migration (db/migrations/0000_initial.sql):

CREATE TABLE IF NOT EXISTS "users" (
    "id" serial PRIMARY KEY NOT NULL,
    "name" text NOT NULL,
    "email" text NOT NULL UNIQUE,
    "avatar_url" text,
    "created_at" timestamp DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS "posts" (
    "id" serial PRIMARY KEY NOT NULL,
    "user_id" integer NOT NULL,
    "title" text NOT NULL,
    "content" text,
    "published" boolean DEFAULT false,
    "created_at" timestamp DEFAULT NOW(),
    FOREIGN KEY ("user_id") REFERENCES "users"("id")
);

CREATE TABLE IF NOT EXISTS "comments" (
    "id" serial PRIMARY KEY NOT NULL,
    "post_id" integer NOT NULL,
    "user_id" integer NOT NULL,
    "content" text NOT NULL,
    "created_at" timestamp DEFAULT NOW(),
    FOREIGN KEY ("post_id") REFERENCES "posts"("id") ON DELETE CASCADE,
    FOREIGN KEY ("user_id") REFERENCES "users"("id")
);

Step 3: Apply Migration

Apply the migration to your database:

npm run db:migrate

What happens:

  • Drizzle connects to your Neon database
  • Runs all pending migrations in order
  • Records applied migrations in __drizzle_migrations table

Output:

✅ Applying migration: 0000_initial.sql
✅ Migration applied successfully

Step 4: Verify Migration

Use Drizzle Studio to verify your schema:

npm run db:studio

Opens a web UI at https://local.drizzle.studio where you can:

  • View tables and relationships
  • Run queries
  • Edit data

Common Scenarios

Scenario 1: Add New Column

  1. Update schema (db/schema.ts):
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  avatarUrl: text('avatar_url'),
  bio: text('bio'),  // ← New column
  createdAt: timestamp('created_at').defaultNow(),
});
  1. Generate migration:
npm run db:generate

Generated SQL (db/migrations/0001_add_bio.sql):

ALTER TABLE "users" ADD COLUMN "bio" text;
  1. Apply migration:
npm run db:migrate

Scenario 2: Add Index

  1. Update schema with index:
export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  userId: integer('user_id').notNull().references(() => users.id),
  title: text('title').notNull(),
  content: text('content'),
  published: boolean('published').default(false),
  createdAt: timestamp('created_at').defaultNow(),
}, (table) => ({
  userIdIdx: index('user_id_idx').on(table.userId),
  createdAtIdx: index('created_at_idx').on(table.createdAt),
}));
  1. Generate and apply:
npm run db:generate
npm run db:migrate

Scenario 3: Rename Column

⚠️ WARNING: Drizzle treats renames as DROP + ADD (data loss!)

Safe approach:

  1. Add new column
  2. Migrate data (manually)
  3. Drop old column

Example:

// Step 1: Add new column
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),  // Old
  fullName: text('full_name').notNull(),  // New
  email: text('email').notNull().unique(),
});
npm run db:generate
npm run db:migrate
-- Step 2: Migrate data manually
UPDATE users SET full_name = name WHERE full_name IS NULL;
// Step 3: Remove old column
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  fullName: text('full_name').notNull(),
  email: text('email').notNull().unique(),
});
npm run db:generate
npm run db:migrate

Scenario 4: Production Deployment

For production, use a separate migration script:

Create scripts/migrate.ts:

import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
import { migrate } from 'drizzle-orm/neon-http/migrator';

async function runMigrations() {
  const sql = neon(process.env.DATABASE_URL!);
  const db = drizzle(sql);

  console.log('Running migrations...');
  await migrate(db, { migrationsFolder: './db/migrations' });
  console.log('Migrations completed!');

  process.exit(0);
}

runMigrations().catch((err) => {
  console.error('Migration failed:', err);
  process.exit(1);
});

Add to package.json:

{
  "scripts": {
    "db:migrate:prod": "tsx scripts/migrate.ts"
  }
}

Run in CI/CD:

# Before deployment
npm run db:migrate:prod

Alternative: Push (Schema Sync)

For development only, you can use db:push to sync schema without migrations:

npm run db:push

What it does:

  • Compares schema with database
  • Applies changes directly
  • No migration files created

When to use:

  • Local development (rapid iteration)
  • Prototyping

When NOT to use:

  • Production
  • Shared databases
  • Need migration history

Best Practices

1. Always Review Generated SQL

# Generate migration
npm run db:generate

# Review file before applying
cat db/migrations/0001_*.sql

# Apply only if safe
npm run db:migrate

2. Test Migrations on Development Database First

# Create Neon branch for testing
neonctl branches create --name test-migration --parent main

# Get branch connection string
export DATABASE_URL=$(neonctl connection-string test-migration)

# Test migration
npm run db:migrate

# If successful, apply to main
neonctl branches delete test-migration
export DATABASE_URL=$(neonctl connection-string main)
npm run db:migrate

3. Commit Migration Files to Git

git add db/migrations/
git add db/schema.ts
git commit -m "feat: add bio column to users table"

4. Use Transactions for Multi-Step Migrations

Drizzle migrations run in transactions by default. If any step fails, all changes roll back.

5. Handle Data Migrations Separately

For complex data transformations, create custom migration scripts:

// db/migrations/custom/0001_migrate_user_data.ts
import { neon } from '@neondatabase/serverless';

const sql = neon(process.env.DATABASE_URL!);

// Custom data migration
await sql`UPDATE users SET status = 'active' WHERE status IS NULL`;

Troubleshooting

Problem: "No schema changes detected"

Solution: Drizzle uses snapshots. If out of sync:

# Delete snapshot
rm -rf db/migrations/meta

# Regenerate
npm run db:generate

Problem: "Relation already exists"

Solution: Migration already applied. Check __drizzle_migrations table:

SELECT * FROM __drizzle_migrations;

Problem: "Cannot drop column with data"

Solution: Set column nullable first, then drop:

ALTER TABLE users ALTER COLUMN old_column DROP NOT NULL;
ALTER TABLE users DROP COLUMN old_column;

Complete Checklist

  • drizzle.config.ts configured
  • Schema defined in db/schema.ts
  • Scripts added to package.json
  • DATABASE_URL environment variable set
  • Initial migration generated (npm run db:generate)
  • Migration reviewed
  • Migration applied (npm run db:migrate)
  • Schema verified in Drizzle Studio
  • Migration files committed to git

Resources