# 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`: ```prisma model Location { id String @id @default(cuid()) address String // Add new field: province String? // ... } ``` #### 2. Create Migration ```bash 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 ```sql -- migrations/20260213120000_add_province_to_location/migration.sql -- AlterTable ALTER TABLE "locations" ADD COLUMN "province" TEXT; ``` #### 4. Commit Migration ```bash git add prisma/migrations/ git commit -m "Add province field to Location model" ``` ### Production Workflow #### 1. Deploy Migration ```bash 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 ```bash 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) ```prisma model Location { federalDistrict String? // Add nullable field } ``` **Migration:** ```sql ALTER TABLE "locations" ADD COLUMN "federal_district" TEXT; ``` #### Add Field (Required with Default) ```prisma model Location { buildingType BuildingType @default(SINGLE_FAMILY) } ``` **Migration:** ```sql ALTER TABLE "locations" ADD COLUMN "building_type" TEXT NOT NULL DEFAULT 'SINGLE_FAMILY'; ``` #### Add Relation ```prisma model Shift { cutId String? cut Cut? @relation(fields: [cutId], references: [id], onDelete: SetNull) } ``` **Migration:** ```sql 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 ```prisma model Location { geocodeConfidence Int? // Changed from String? to Int? } ``` **Migration (requires data migration):** ```sql -- 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 ```prisma enum BuildingType { SINGLE_FAMILY MULTI_UNIT MIXED_USE COMMERCIAL } ``` **Migration:** ```sql CREATE TYPE "BuildingType" AS ENUM ('SINGLE_FAMILY', 'MULTI_UNIT', 'MIXED_USE', 'COMMERCIAL'); ``` #### Add Index ```prisma model Location { latitude Decimal longitude Decimal @@index([latitude, longitude]) } ``` **Migration:** ```sql 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`: ```typescript export const videos = pgTable('videos', { id: serial('id').primaryKey(), path: text('path').notNull().unique(), // Add new field: description: text('description'), // ... }); ``` #### 2. Push Schema Changes ```bash 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 ```bash npx drizzle-kit studio ``` Opens Drizzle Studio at `https://local.drizzle.studio/` for database inspection. ### Production Workflow **Same as development:** ```bash 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:** ```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 ```bash npx prisma migrate status ``` **Step 2:** Manually revert migration SQL ```sql -- Reverse of migration.sql ALTER TABLE "locations" DROP COLUMN "province"; ``` **Step 3:** Mark migration as rolled back ```sql DELETE FROM "_prisma_migrations" WHERE migration_name = '20260213120000_add_province'; ``` **Step 4:** Remove migration file ```bash rm -rf prisma/migrations/20260213120000_add_province/ ``` **Step 5:** Fix schema Edit `prisma/schema.prisma` to remove `province` field. **Step 6:** Create new migration ```bash 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 ```bash 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:** ```bash npx prisma migrate resolve --applied # Mark as applied # OR npx prisma migrate resolve --rolled-back # 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 ```bash docker compose exec v2-postgres pg_dump -U changemaker_v2 changemaker_v2 > backup.sql ``` ### Production ```bash # 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 ```bash # 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 ```yaml 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](./index.md) — Architecture and models - [Schema Reference](./schema.md) — All model fields - [Seeding](./seeding.md) — Default data - [Prisma Documentation](https://www.prisma.io/docs/orm/prisma-migrate) - [Drizzle Documentation](https://orm.drizzle.team/kit-docs/overview)