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_migrations table

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 push in production (skips migrations)
  • Use prisma migrate reset in 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:

  1. Clean up duplicate data first
  2. 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 }}