11 KiB
Migration Workflow
Overview
Changemaker Lite V2 uses a dual ORM architecture with separate migration workflows:
- Prisma Migrate — Main API (Express, 30 models)
- Drizzle Kit — Media API (Fastify, 3 models)
Both ORMs share the same PostgreSQL database but maintain independent migration histories.
Prisma Migration Workflow
Development Workflow
1. Modify Schema
Edit api/prisma/schema.prisma:
model Location {
id String @id @default(cuid())
address String
// Add new field:
province String?
// ...
}
2. Create Migration
cd api
npx prisma migrate dev --name add_province_to_location
This command:
- Generates SQL migration file in
prisma/migrations/ - Applies migration to database
- Regenerates Prisma Client
- Updates
_prisma_migrationstable
Output:
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "changemaker_v2", schema "public"
Applying migration `20260213120000_add_province_to_location`
The following migration(s) have been created and applied from new schema changes:
migrations/
└─ 20260213120000_add_province_to_location/
└─ migration.sql
Your database is now in sync with your schema.
3. Review Migration SQL
-- migrations/20260213120000_add_province_to_location/migration.sql
-- AlterTable
ALTER TABLE "locations" ADD COLUMN "province" TEXT;
4. Commit Migration
git add prisma/migrations/
git commit -m "Add province field to Location model"
Production Workflow
1. Deploy Migration
docker compose exec api npx prisma migrate deploy
This command:
- Applies pending migrations from
prisma/migrations/ - Does NOT create new migrations
- Does NOT prompt for confirmations
- Safe for production/CI pipelines
2. Verify Migration Status
docker compose exec api npx prisma migrate status
Output:
1 migration found in prisma/migrations
Following migration have been applied:
20260213120000_add_province_to_location
Database schema is up to date!
Common Migration Scenarios
Add Field (Nullable)
model Location {
federalDistrict String? // Add nullable field
}
Migration:
ALTER TABLE "locations" ADD COLUMN "federal_district" TEXT;
Add Field (Required with Default)
model Location {
buildingType BuildingType @default(SINGLE_FAMILY)
}
Migration:
ALTER TABLE "locations" ADD COLUMN "building_type" TEXT NOT NULL DEFAULT 'SINGLE_FAMILY';
Add Relation
model Shift {
cutId String?
cut Cut? @relation(fields: [cutId], references: [id], onDelete: SetNull)
}
Migration:
ALTER TABLE "shifts" ADD COLUMN "cut_id" TEXT;
CREATE INDEX "shifts_cut_id_idx" ON "shifts"("cut_id");
ALTER TABLE "shifts" ADD CONSTRAINT "shifts_cut_id_fkey" FOREIGN KEY ("cut_id") REFERENCES "cuts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
Change Field Type
model Location {
geocodeConfidence Int? // Changed from String? to Int?
}
Migration (requires data migration):
-- Step 1: Add new column
ALTER TABLE "locations" ADD COLUMN "geocode_confidence_new" INTEGER;
-- Step 2: Migrate data (custom logic)
UPDATE "locations" SET "geocode_confidence_new" = CAST("geocode_confidence" AS INTEGER)
WHERE "geocode_confidence" ~ '^[0-9]+$';
-- Step 3: Drop old column
ALTER TABLE "locations" DROP COLUMN "geocode_confidence";
-- Step 4: Rename new column
ALTER TABLE "locations" RENAME COLUMN "geocode_confidence_new" TO "geocode_confidence";
Add Enum
enum BuildingType {
SINGLE_FAMILY
MULTI_UNIT
MIXED_USE
COMMERCIAL
}
Migration:
CREATE TYPE "BuildingType" AS ENUM ('SINGLE_FAMILY', 'MULTI_UNIT', 'MIXED_USE', 'COMMERCIAL');
Add Index
model Location {
latitude Decimal
longitude Decimal
@@index([latitude, longitude])
}
Migration:
CREATE INDEX "locations_latitude_longitude_idx" ON "locations"("latitude", "longitude");
Migration Commands Reference
| Command | Description | Environment |
|---|---|---|
npx prisma migrate dev |
Create + apply migration | Development |
npx prisma migrate deploy |
Apply pending migrations | Production/CI |
npx prisma migrate status |
Check migration status | All |
npx prisma migrate reset |
Reset DB + apply all migrations | Development only |
npx prisma db push |
Push schema without migrations | Prototyping only |
npx prisma studio |
Open Prisma Studio (DB GUI) | Development |
Safe Migration Practices
✅ DO
- Always review generated SQL before committing
- Test migrations on dev database first
- Back up production database before deploying migrations
- Use nullable fields for new columns on existing tables
- Use
@default()for new required fields - Commit migration files to version control
❌ DON'T
- Use
prisma db pushin production (skips migrations) - Use
prisma migrate resetin production (deletes data) - Manually edit migration files after applying
- Delete old migration files (breaks history)
- Change field names without data migration plan
Drizzle Migration Workflow
Development Workflow
1. Modify Schema
Edit api/src/modules/media/db/schema.ts:
export const videos = pgTable('videos', {
id: serial('id').primaryKey(),
path: text('path').notNull().unique(),
// Add new field:
description: text('description'),
// ...
});
2. Push Schema Changes
cd api
npx drizzle-kit push
This command:
- Generates SQL diff from schema
- Applies changes directly to database
- Does NOT create migration files (Drizzle push mode)
- Updates database schema immediately
Output:
Reading config file '/home/bunker-admin/changemaker.lite/api/drizzle.config.ts'
Pulling schema from database...✓
Applying changes...
[✓] Applying: ALTER TABLE "videos" ADD COLUMN "description" text;
Schema applied successfully!
3. Verify Schema
npx drizzle-kit studio
Opens Drizzle Studio at https://local.drizzle.studio/ for database inspection.
Production Workflow
Same as development:
docker compose exec media-api npx drizzle-kit push
Drizzle vs Prisma Migrate
| Feature | Prisma Migrate | Drizzle Kit Push |
|---|---|---|
| Migration files | ✓ Generated | ✗ Not generated |
| Migration history | ✓ Tracked in _prisma_migrations |
✗ No history table |
| Rollback support | ✓ Via migration files | ✗ Manual only |
| Production safety | ✓ Explicit deploy step | ⚠️ Direct push |
| Best for | Main API (schema stability) | Media API (rapid iteration) |
Why Drizzle for Media API?
- Smaller schema (3 tables vs 30)
- Faster iteration during development
- Simpler deployment (no migration history to manage)
- Media API is newer (less risk of breaking changes)
Drizzle Commands Reference
| Command | Description |
|---|---|
npx drizzle-kit push |
Push schema changes to DB |
npx drizzle-kit studio |
Open Drizzle Studio |
npx drizzle-kit generate |
Generate migrations (not used) |
Migration File Structure
Prisma Migrations
api/prisma/migrations/
├── 20260211120000_initial/
│ └── migration.sql
├── 20260211125000_add_refresh_tokens/
│ └── migration.sql
├── 20260212100000_add_canvass_system/
│ └── migration.sql
└── migration_lock.toml
File naming: YYYYMMDDHHMMSS_description/migration.sql
migration_lock.toml:
# Please do not edit this file manually
provider = "postgresql"
Drizzle Schema (No Migrations)
api/src/modules/media/db/
├── schema.ts # Source of truth
└── drizzle.config.ts # Drizzle config
Rollback Strategies
Prisma Rollback (Manual)
Scenario: Migration 20260213120000_add_province caused issues.
Step 1: Identify last good migration
npx prisma migrate status
Step 2: Manually revert migration SQL
-- Reverse of migration.sql
ALTER TABLE "locations" DROP COLUMN "province";
Step 3: Mark migration as rolled back
DELETE FROM "_prisma_migrations" WHERE migration_name = '20260213120000_add_province';
Step 4: Remove migration file
rm -rf prisma/migrations/20260213120000_add_province/
Step 5: Fix schema
Edit prisma/schema.prisma to remove province field.
Step 6: Create new migration
npx prisma migrate dev --name remove_province_from_location
Drizzle Rollback (Manual)
Step 1: Revert schema changes in schema.ts
Step 2: Push reverted schema
npx drizzle-kit push
Step 3: If data loss occurred, restore from backup
Common Migration Errors
Error: "Migration failed to apply cleanly"
Cause: Database state doesn't match expected state Solution:
npx prisma migrate resolve --applied <migration-name> # Mark as applied
# OR
npx prisma migrate resolve --rolled-back <migration-name> # Mark as rolled back
Error: "Unique constraint violation"
Cause: Trying to add unique constraint on column with duplicate values Solution:
- Clean up duplicate data first
- Run migration
Error: "Column cannot be NOT NULL"
Cause: Trying to add required field to table with existing rows
Solution: Use @default() or make field nullable
Error: "Foreign key constraint failed"
Cause: Referencing non-existent records Solution: Ensure related records exist before adding FK
Database Backup Before Migration
Development
docker compose exec v2-postgres pg_dump -U changemaker_v2 changemaker_v2 > backup.sql
Production
# Via docker-compose
docker compose exec v2-postgres pg_dump -U changemaker_v2 changemaker_v2 | gzip > backup_$(date +%Y%m%d_%H%M%S).sql.gz
# Via backup script
./scripts/backup.sh
Restore from Backup
# Stop API services
docker compose stop api media-api
# Restore database
docker compose exec -T v2-postgres psql -U changemaker_v2 changemaker_v2 < backup.sql
# Restart services
docker compose up -d api media-api
CI/CD Integration
GitHub Actions Example
name: Deploy V2
on:
push:
branches: [main]
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
- name: Install dependencies
run: cd api && npm ci
- name: Run Prisma migrations
run: cd api && npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Run Drizzle push
run: cd api && npx drizzle-kit push
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Related Documentation
- Database Overview — Architecture and models
- Schema Reference — All model fields
- Seeding — Default data
- Prisma Documentation
- Drizzle Documentation