474 lines
11 KiB
Markdown
474 lines
11 KiB
Markdown
# 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 <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
|
|
```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)
|