Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:25:01 +08:00
commit 45ffb77c40
14 changed files with 3275 additions and 0 deletions

View File

@@ -0,0 +1,461 @@
# 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
```bash
npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit
```
### 2. Create Configuration File
Create `drizzle.config.ts` in your project root:
```typescript
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
```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`:
```typescript
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:
```bash
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`):
```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:
```bash
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:
```bash
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`):
```typescript
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(),
});
```
2. **Generate migration**:
```bash
npm run db:generate
```
Generated SQL (`db/migrations/0001_add_bio.sql`):
```sql
ALTER TABLE "users" ADD COLUMN "bio" text;
```
3. **Apply migration**:
```bash
npm run db:migrate
```
### Scenario 2: Add Index
1. **Update schema** with index:
```typescript
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),
}));
```
2. **Generate and apply**:
```bash
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:**
```typescript
// 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(),
});
```
```bash
npm run db:generate
npm run db:migrate
```
```sql
-- Step 2: Migrate data manually
UPDATE users SET full_name = name WHERE full_name IS NULL;
```
```typescript
// Step 3: Remove old column
export const users = pgTable('users', {
id: serial('id').primaryKey(),
fullName: text('full_name').notNull(),
email: text('email').notNull().unique(),
});
```
```bash
npm run db:generate
npm run db:migrate
```
### Scenario 4: Production Deployment
For production, use a separate migration script:
Create `scripts/migrate.ts`:
```typescript
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`:
```json
{
"scripts": {
"db:migrate:prod": "tsx scripts/migrate.ts"
}
}
```
Run in CI/CD:
```bash
# Before deployment
npm run db:migrate:prod
```
---
## Alternative: Push (Schema Sync)
For **development only**, you can use `db:push` to sync schema without migrations:
```bash
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
```bash
# 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
```bash
# 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
```bash
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:
```typescript
// 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:
```bash
# Delete snapshot
rm -rf db/migrations/meta
# Regenerate
npm run db:generate
```
### Problem: "Relation already exists"
**Solution**: Migration already applied. Check `__drizzle_migrations` table:
```sql
SELECT * FROM __drizzle_migrations;
```
### Problem: "Cannot drop column with data"
**Solution**: Set column nullable first, then drop:
```sql
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
- **Drizzle ORM Docs**: https://orm.drizzle.team/docs/overview
- **Drizzle Kit Docs**: https://orm.drizzle.team/docs/kit-overview
- **Neon + Drizzle Guide**: https://orm.drizzle.team/docs/quick-postgresql/neon
- **Migration Best Practices**: https://orm.drizzle.team/docs/migrations