1155 lines
23 KiB
Markdown

# Database Migrations Guide
Complete guide to managing database schema changes in Changemaker Lite V2 using Prisma Migrate and Drizzle Kit.
## Overview
Changemaker Lite V2 uses **two ORMs** for different parts of the application:
- **Prisma** (Main API) - Full-featured ORM with migration tracking
- **Drizzle** (Media API) - Lightweight ORM with schema push (no migrations)
This guide covers both workflows.
## Prisma Migrations (Main API)
### Migration Workflow Overview
```
1. Edit schema.prisma
2. Create migration (npx prisma migrate dev)
3. Review generated SQL
4. Test migration locally
5. Commit migration files
6. Deploy to production (npx prisma migrate deploy)
```
### Understanding Prisma Migrate
**Prisma Migrate:**
- Tracks schema changes as SQL migration files
- Stores migration history in `_prisma_migrations` table
- Ensures schema consistency across environments
- Supports rollback via version control
**Migration Files:**
- Located in `api/prisma/migrations/`
- Named with timestamp: `20260213123456_description/`
- Contains `migration.sql` (SQL commands)
**Migration States:**
- **Pending:** Not yet applied
- **Applied:** Successfully executed
- **Failed:** Execution error (requires manual fix)
### Creating Migrations
#### Step 1: Edit Prisma Schema
Edit `api/prisma/schema.prisma`:
```prisma
// Before
model User {
id Int @id @default(autoincrement())
email String @unique
password String
role Role @default(USER)
createdAt DateTime @default(now())
}
// After (add name field)
model User {
id Int @id @default(autoincrement())
email String @unique
password String
name String? // New field (nullable)
role Role @default(USER)
createdAt DateTime @default(now())
}
```
#### Step 2: Validate Schema
```bash
cd api
npx prisma validate
```
**Expected output:**
```
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
The schema is valid ✔
```
**If errors:**
```
Error validating model "User": Field "foo" references unknown model "Bar"
```
Fix errors before proceeding.
#### Step 3: Create Migration
```bash
cd api
npx prisma migrate dev --name add_user_name
```
**What happens:**
1. Prisma detects schema changes
2. Generates SQL migration file
3. Prompts for migration name (or uses `--name` argument)
4. Applies migration to development database
5. Regenerates Prisma Client
**Expected output:**
```
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "changemaker_v2_db"
Applying migration `20260213123456_add_user_name`
Running seed command `tsx prisma/seed.ts` ...
✔ Generated Prisma Client to ./node_modules/@prisma/client
```
**Migration file created:**
```
api/prisma/migrations/
└── 20260213123456_add_user_name/
└── migration.sql
```
#### Step 4: Review Generated SQL
```bash
cd api
cat prisma/migrations/20260213123456_add_user_name/migration.sql
```
**Example SQL:**
```sql
-- AlterTable
ALTER TABLE "users" ADD COLUMN "name" TEXT;
```
**Verify SQL is correct:**
- Check table names match expectations
- Ensure data types are correct
- Look for unexpected `DROP` commands
#### Step 5: Test Migration
Migration already applied to development DB. Verify:
```bash
# Check schema with Prisma Studio
cd api
npx prisma studio
```
Or query directly:
```bash
# PostgreSQL shell
docker compose exec v2-postgres psql -U changemaker_v2 -d changemaker_v2_db
# Describe users table
changemaker_v2_db=# \d users;
```
**Expected output:**
```
Column | Type | Nullable | Default
----------+---------+----------+---------
id | integer | not null | nextval(...)
email | text | not null |
password | text | not null |
name | text | | <-- New field
role | text | not null | 'USER'
created_at| timestamp| not null | now()
```
#### Step 6: Commit Migration
```bash
git add prisma/migrations/20260213123456_add_user_name/
git add prisma/schema.prisma
git commit -m "feat(db): add name field to User model"
```
**Always commit:**
- Migration directory (`prisma/migrations/*/`)
- Updated `schema.prisma`
### Applying Migrations (Production)
#### In Production Environment
```bash
cd api
npx prisma migrate deploy
```
**What it does:**
- Checks `_prisma_migrations` table for applied migrations
- Applies only **pending** migrations
- Does NOT create new migrations
- Safe for production
**Expected output:**
```
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "changemaker_v2_prod_db"
2 migrations found in prisma/migrations
Applying migration `20260213123456_add_user_name`
Applying migration `20260214000000_add_user_avatar`
All migrations have been successfully applied.
```
#### In Docker
```bash
# Apply migrations in Docker container
docker compose exec api npx prisma migrate deploy
# Or during container startup (Dockerfile)
CMD npx prisma migrate deploy && npm start
```
#### CI/CD Deployment
```yaml
# GitHub Actions example
- name: Run migrations
run: |
cd api
npx prisma migrate deploy
```
### Migration Best Practices
#### 1. Incremental Changes
Make small, focused migrations:
**Good:**
```bash
# Separate migrations
npx prisma migrate dev --name add_user_name
npx prisma migrate dev --name add_user_avatar
npx prisma migrate dev --name add_user_bio
```
**Bad:**
```bash
# One huge migration
npx prisma migrate dev --name update_user_model
# (adds 10 fields, 3 relations, 5 indexes)
```
#### 2. Descriptive Names
Use clear migration names:
**Good:**
```bash
npx prisma migrate dev --name add_user_name
npx prisma migrate dev --name make_email_unique
npx prisma migrate dev --name create_posts_table
npx prisma migrate dev --name add_user_posts_relation
```
**Bad:**
```bash
npx prisma migrate dev --name update
npx prisma migrate dev --name fix
npx prisma migrate dev --name changes
```
#### 3. Review SQL Before Committing
Always review generated SQL:
```bash
cat prisma/migrations/*/migration.sql
```
**Watch for:**
- Unexpected `DROP TABLE` or `DROP COLUMN`
- Missing `NOT NULL` constraints
- Incorrect data types
- Missing indexes on foreign keys
#### 4. Backup Before Migration (Production)
```bash
# Backup database before deploy
docker compose exec v2-postgres pg_dump -U changemaker_v2 changemaker_v2_db > backup-$(date +%Y%m%d).sql
# Apply migration
npx prisma migrate deploy
# If migration fails, restore:
cat backup-20260213.sql | docker compose exec -T v2-postgres psql -U changemaker_v2 changemaker_v2_db
```
#### 5. Test on Staging First
Never deploy migrations directly to production:
```
1. Create migration in development
2. Test locally
3. Commit to version control
4. Deploy to staging environment
5. Test on staging
6. Deploy to production
```
### Common Migration Scenarios
#### Add New Field
```prisma
// schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
name String? // New nullable field
}
```
```bash
npx prisma migrate dev --name add_user_name
```
**Generated SQL:**
```sql
ALTER TABLE "users" ADD COLUMN "name" TEXT;
```
#### Add Required Field (with Default)
```prisma
model User {
id Int @id @default(autoincrement())
email String @unique
createdAt DateTime @default(now()) // New required field with default
}
```
```bash
npx prisma migrate dev --name add_created_at
```
**Generated SQL:**
```sql
ALTER TABLE "users" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
```
#### Add New Table
```prisma
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
}
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
}
```
```bash
npx prisma migrate dev --name create_posts_table
```
**Generated SQL:**
```sql
CREATE TABLE "posts" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT,
"published" BOOLEAN NOT NULL DEFAULT false,
"author_id" INTEGER NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "posts_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "posts_author_id_idx" ON "posts"("author_id");
ALTER TABLE "posts" ADD CONSTRAINT "posts_author_id_fkey"
FOREIGN KEY ("author_id") REFERENCES "users"("id")
ON DELETE RESTRICT ON UPDATE CASCADE;
```
#### Add Relation
```prisma
model Campaign {
id Int @id @default(autoincrement())
title String
createdByUserId Int // New foreign key
createdBy User @relation(fields: [createdByUserId], references: [id])
}
model User {
id Int @id @default(autoincrement())
email String @unique
campaigns Campaign[]
}
```
```bash
npx prisma migrate dev --name add_campaign_user_relation
```
**Generated SQL:**
```sql
ALTER TABLE "campaigns" ADD COLUMN "created_by_user_id" INTEGER NOT NULL;
CREATE INDEX "campaigns_created_by_user_id_idx" ON "campaigns"("created_by_user_id");
ALTER TABLE "campaigns" ADD CONSTRAINT "campaigns_created_by_user_id_fkey"
FOREIGN KEY ("created_by_user_id") REFERENCES "users"("id")
ON DELETE RESTRICT ON UPDATE CASCADE;
```
#### Change Field Type
```prisma
// Before
model User {
age Int
}
// After
model User {
age String // Changed from Int to String
}
```
```bash
npx prisma migrate dev --name change_user_age_to_string
```
**Generated SQL:**
```sql
ALTER TABLE "users" ALTER COLUMN "age" SET DATA TYPE TEXT;
```
**Warning:** This may fail if data cannot be cast. Consider data migration first.
#### Add Unique Constraint
```prisma
model User {
email String @unique // Add unique constraint
}
```
```bash
npx prisma migrate dev --name make_email_unique
```
**Generated SQL:**
```sql
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
```
#### Add Index
```prisma
model User {
email String
@@index([email]) // Add index
}
```
```bash
npx prisma migrate dev --name add_email_index
```
**Generated SQL:**
```sql
CREATE INDEX "users_email_idx" ON "users"("email");
```
### Migration History and Status
#### Check Migration Status
```bash
cd api
npx prisma migrate status
```
**Expected output:**
```
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "changemaker_v2_db"
Database schema is up to date!
Following migrations have been applied:
20260101000000_init
20260105000000_add_campaigns
20260110000000_add_locations
20260213123456_add_user_name
```
#### View Migration History
```bash
# List migration files
ls -la api/prisma/migrations/
# View specific migration
cat api/prisma/migrations/20260213123456_add_user_name/migration.sql
```
#### Check Database Migration Table
```bash
docker compose exec v2-postgres psql -U changemaker_v2 -d changemaker_v2_db -c "SELECT * FROM _prisma_migrations;"
```
**Output:**
```
id | checksum | finished_at | migration_name | logs
---+----------+-------------+----------------+-----
1 | abc123 | 2026-01-01 | 20260101000000_init | NULL
2 | def456 | 2026-01-05 | 20260105000000_add_campaigns | NULL
```
### Rollback Strategies
Prisma Migrate does **NOT** have automatic rollback. Use these strategies:
#### 1. Version Control Rollback
```bash
# Revert schema changes
git revert <commit-hash>
# Create new migration to undo changes
npx prisma migrate dev --name revert_user_name
# This creates a new migration that undoes the previous one
```
#### 2. Manual Rollback Migration
Create a new migration to reverse changes:
```prisma
// If you added a field, remove it
model User {
id Int @id @default(autoincrement())
email String @unique
// name String? // Remove this
}
```
```bash
npx prisma migrate dev --name remove_user_name
```
**Generated SQL:**
```sql
ALTER TABLE "users" DROP COLUMN "name";
```
#### 3. Database Restore (Last Resort)
```bash
# Restore from backup
cat backup-20260213.sql | docker compose exec -T v2-postgres psql -U changemaker_v2 changemaker_v2_db
# Mark migrations as rolled back
docker compose exec v2-postgres psql -U changemaker_v2 -d changemaker_v2_db -c "
DELETE FROM _prisma_migrations
WHERE migration_name = '20260213123456_add_user_name';
"
```
#### 4. Reset Development Database
**WARNING: Deletes all data!**
```bash
cd api
npx prisma migrate reset
```
This:
1. Drops all tables
2. Re-applies all migrations from scratch
3. Runs seed script
### Handling Migration Conflicts
#### Schema Drift
**Problem:** Database schema doesn't match Prisma schema.
**Symptoms:**
```
Error: Database schema is not in sync with the migration history
```
**Solution:**
```bash
# Check what's different
npx prisma migrate diff \
--from-schema-datamodel prisma/schema.prisma \
--to-schema-datasource prisma/schema.prisma
# Create migration to fix drift
npx prisma migrate dev --name fix_schema_drift
```
#### Failed Migration
**Problem:** Migration fails during apply.
**Symptoms:**
```
Error: Migration failed with error:
ALTER TABLE "users" ADD COLUMN "age" INTEGER NOT NULL;
ERROR: column "age" contains null values
```
**Solution:**
```bash
# 1. Mark migration as rolled back
npx prisma migrate resolve --rolled-back 20260213123456_add_user_age
# 2. Fix migration SQL manually
vi prisma/migrations/20260213123456_add_user_age/migration.sql
# Change to:
ALTER TABLE "users" ADD COLUMN "age" INTEGER; -- Make nullable first
UPDATE "users" SET "age" = 0 WHERE "age" IS NULL; -- Set default
ALTER TABLE "users" ALTER COLUMN "age" SET NOT NULL; -- Then make required
# 3. Apply migration again
npx prisma migrate deploy
```
#### Conflicting Migrations (Team Environment)
**Problem:** Two developers create migrations simultaneously.
**Solution:**
```bash
# 1. Pull latest changes
git pull origin v2
# 2. Prisma detects conflict
npx prisma migrate dev
# 3. Resolve by creating merge migration
# Prisma will prompt you to create a migration that includes both changes
```
### Data Migrations
Prisma Migrate handles **schema** changes, not **data** changes. For data transformations:
#### Option 1: Custom SQL in Migration
Edit generated migration file:
```sql
-- Add column (Prisma-generated)
ALTER TABLE "users" ADD COLUMN "full_name" TEXT;
-- Populate from existing data (manual addition)
UPDATE "users" SET "full_name" = "first_name" || ' ' || "last_name";
-- Remove old columns (Prisma-generated)
ALTER TABLE "users" DROP COLUMN "first_name";
ALTER TABLE "users" DROP COLUMN "last_name";
```
#### Option 2: Separate Data Migration Script
```typescript
// api/prisma/data-migrations/20260213-populate-full-name.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
const users = await prisma.user.findMany();
for (const user of users) {
await prisma.user.update({
where: { id: user.id },
data: {
fullName: `${user.firstName} ${user.lastName}`
}
});
}
console.log(`Updated ${users.length} users`);
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());
```
Run after migration:
```bash
npx tsx prisma/data-migrations/20260213-populate-full-name.ts
```
## Drizzle Push (Media API)
### Drizzle Overview
**Drizzle Kit Push:**
- Syncs schema directly to database
- No migration files generated
- Fast iteration for prototyping
- Used only for Media API tables
**Schema Location:**
- `api/src/modules/media/db/schema.ts`
**When to Use:**
- Rapid prototyping
- Development only
- Media API tables (videos, jobs, reactions)
**When NOT to Use:**
- Production deployments
- Main API tables (use Prisma)
- When migration history is needed
### Drizzle Push Workflow
#### Step 1: Edit Schema
Edit `api/src/modules/media/db/schema.ts`:
```typescript
// Before
export const videos = pgTable('videos', {
id: serial('id').primaryKey(),
filename: text('filename').notNull(),
title: text('title'),
duration: integer('duration'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// After (add description field)
export const videos = pgTable('videos', {
id: serial('id').primaryKey(),
filename: text('filename').notNull(),
title: text('title'),
description: text('description'), // New field
duration: integer('duration'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
```
#### Step 2: Push Schema
```bash
cd api
npm run drizzle:push
```
Or directly:
```bash
cd api
npx drizzle-kit push
```
**What happens:**
1. Drizzle compares schema to database
2. Generates SQL for changes
3. Applies changes immediately
4. No migration files created
**Expected output:**
```
Reading config from drizzle.config.ts
Using 'pg' driver for database querying
Pulling schema from database...
[✓] Schema pulled successfully
Comparing schemas...
[!] Changes detected:
- ALTER TABLE "videos" ADD COLUMN "description" TEXT;
Do you want to execute these changes? [y/N]: y
Applying changes...
[✓] Schema pushed successfully
```
#### Step 3: Verify Changes
```bash
# Check with Drizzle Studio
cd api
npx drizzle-kit studio
```
Or query directly:
```bash
docker compose exec v2-postgres psql -U changemaker_v2 -d changemaker_v2_db -c "\d videos"
```
### Drizzle Best Practices
#### 1. Development Only
Use Drizzle Push only in development:
**Good:**
```bash
# Development
npm run drizzle:push
```
**Bad:**
```bash
# Production (use Prisma migrate for production schema changes)
npm run drizzle:push
```
#### 2. Backup Before Push
Always backup before pushing schema:
```bash
# Backup database
docker compose exec v2-postgres pg_dump -U changemaker_v2 changemaker_v2_db > backup.sql
# Push schema
npm run drizzle:push
# If something breaks, restore:
cat backup.sql | docker compose exec -T v2-postgres psql -U changemaker_v2 changemaker_v2_db
```
#### 3. Test Changes Locally
Never push untested schema changes:
```bash
# 1. Edit schema
vi src/modules/media/db/schema.ts
# 2. Push to dev database
npm run drizzle:push
# 3. Test with Drizzle Studio
npm run drizzle:studio
# 4. Test API endpoints
curl http://localhost:4100/api/media/videos
```
### Drizzle vs Prisma
| Feature | Prisma Migrate | Drizzle Push |
|---------|----------------|--------------|
| Migration files | ✅ Yes | ❌ No |
| Migration history | ✅ Tracked | ❌ Not tracked |
| Rollback | ✅ Via version control | ❌ Manual only |
| Production use | ✅ Recommended | ⚠️ Not recommended |
| Prototyping | ⚠️ Slower | ✅ Faster |
| Use case | Main API tables | Media API tables |
## Seeding After Migration
### Running Seed Script
After migrations, seed database:
```bash
cd api
npx prisma db seed
```
**What it does:**
- Runs `prisma/seed.ts`
- Creates admin user
- Creates default settings
- Creates sample blocks
**Expected output:**
```
Running seed command `tsx prisma/seed.ts` ...
Seeding database...
Created default settings
Created admin user: admin@example.com
Created 10 sample blocks
Seed completed successfully
```
### Custom Seed Data
Edit `api/prisma/seed.ts`:
```typescript
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// Create admin user
await prisma.user.upsert({
where: { email: 'admin@example.com' },
update: {},
create: {
email: 'admin@example.com',
password: await hashPassword('Admin123!'),
role: 'SUPER_ADMIN',
name: 'Admin User'
}
});
// Create sample campaign
await prisma.campaign.create({
data: {
title: 'Sample Campaign',
description: 'This is a sample campaign',
active: true,
createdByUserId: 1
}
});
console.log('Seed completed');
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());
```
## CI/CD Integration
### GitHub Actions Example
```yaml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
working-directory: ./api
run: npm ci
- name: Run migrations
working-directory: ./api
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: npx prisma migrate deploy
- name: Seed database
working-directory: ./api
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: npx prisma db seed
```
### Docker Deployment
```dockerfile
# api/Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
# Generate Prisma Client
RUN npx prisma generate
# Run migrations on startup
CMD npx prisma migrate deploy && npm start
```
## Troubleshooting
### Migration Fails with "Column Already Exists"
**Problem:**
```
Error: column "name" of relation "users" already exists
```
**Solution:**
```bash
# Mark migration as applied
npx prisma migrate resolve --applied 20260213123456_add_user_name
# Or drop column manually and re-run
docker compose exec v2-postgres psql -U changemaker_v2 -d changemaker_v2_db -c "ALTER TABLE users DROP COLUMN name;"
npx prisma migrate deploy
```
### Migration Fails with "Relation Does Not Exist"
**Problem:**
```
Error: relation "posts" does not exist
```
**Solution:**
```bash
# Check migration history
npx prisma migrate status
# Apply missing migrations
npx prisma migrate deploy
# Or reset (development only)
npx prisma migrate reset
```
### Schema Out of Sync
**Problem:**
```
Error: Database schema is not in sync
```
**Solution:**
```bash
# Generate migration to fix drift
npx prisma migrate dev --name fix_drift
# Or in production, create explicit migration
npx prisma migrate diff \
--from-schema-datamodel prisma/schema.prisma \
--to-schema-datasource prisma/schema.prisma \
--script > fix-drift.sql
# Review fix-drift.sql and apply manually
```
### Drizzle Push Fails
**Problem:**
```
Error: Could not push schema
```
**Solution:**
```bash
# Check Drizzle config
cat api/drizzle.config.ts
# Verify DATABASE_URL
echo $DATABASE_URL
# Test database connection
docker compose exec v2-postgres psql -U changemaker_v2 -d changemaker_v2_db -c "SELECT 1"
# Clear Drizzle cache and retry
rm -rf api/.drizzle
npm run drizzle:push
```
## Related Documentation
- **Setup:** [Local Development Setup](local-setup.md)
- **Commands:** [NPM Commands Reference](npm-commands.md)
- **Docker:** [Docker Workflow](docker-workflow.md)
- **Database:** [Database Schema](../architecture/database-schema.md)
- **Deployment:** [Production Deployment](../deployment/production.md)
## Summary
You now know:
- ✅ How Prisma Migrate tracks schema changes
- ✅ How to create and apply migrations
- ✅ Common migration scenarios (add field, table, relation)
- ✅ Migration best practices
- ✅ How to handle migration conflicts
- ✅ How to perform data migrations
- ✅ How Drizzle Push works for Media API
- ✅ When to use Prisma vs Drizzle
- ✅ How to seed database after migrations
- ✅ How to integrate migrations in CI/CD
**Quick Reference:**
```bash
# Prisma: Create migration
npx prisma migrate dev --name description
# Prisma: Apply migrations (production)
npx prisma migrate deploy
# Prisma: Check status
npx prisma migrate status
# Drizzle: Push schema (dev only)
npx drizzle-kit push
# Seed database
npx prisma db seed
# Reset (dev only, DELETES DATA)
npx prisma migrate reset
```