| name | database-migration |
| description | Database migration management for Justice Companion using Drizzle ORM: creates migrations, handles rollbacks, validates schema changes, and manages encryption on 11 fields. Use when modifying database schema, adding tables, or troubleshooting migration errors. |
| allowed-tools | Read, Write, Edit, Bash, Grep, mcp__memory__* |
Database Migration Skill
Purpose
Safe database schema management with Drizzle ORM for Justice Companion's encrypted SQLite database.
When Claude Uses This
- User requests database schema changes ("add a column", "create a table")
- Migration errors occur
- User asks "how do I migrate the database?"
- Before modifying
src/db/schema.ts
Critical Constraints
Encryption Fields (11 Total)
These fields MUST use EncryptionService:
users.emailusers.full_namecases.titlecases.descriptionevidence.file_pathevidence.noteschat_conversations.message_contentdocuments.file_pathcontacts.emailcontacts.phone_numbercontacts.address
Rule: If adding columns to these tables, determine if encryption is needed.
Migration Safety
- ✅ Always create backup before migration
- ✅ Test on in-memory database first
- ✅ Use transactions (all-or-nothing)
- ❌ Never delete production data without user confirmation
Workflow
1. Schema Change
// Edit src/db/schema.ts
import { text, integer } from 'drizzle-orm/sqlite-core';
export const cases = sqliteTable('cases', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(), // ENCRYPTED via EncryptionService
// Add new column:
case_number: text('case_number'),
});
2. Generate Migration
pnpm db:generate
# Creates: src/db/migrations/0001_add_case_number.sql
3. Review Migration SQL
-- Check generated SQL for safety
ALTER TABLE cases ADD COLUMN case_number TEXT;
4. Test Migration
# Run in test environment first
pnpm test src/db/database.test.ts
5. Apply Migration
pnpm db:migrate
# Automatic backup created before migration
6. Rollback (If Needed)
pnpm db:migrate:rollback
# Reverts last migration
Common Scenarios
Adding a New Table
// 1. Define schema in src/db/schema.ts
export const legal_documents = sqliteTable('legal_documents', {
id: integer('id').primaryKey({ autoIncrement: true }),
case_id: integer('case_id').references(() => cases.id),
document_type: text('document_type').notNull(),
file_path: text('file_path').notNull(), // ENCRYPT THIS
created_at: integer('created_at', { mode: 'timestamp' }).notNull(),
});
// 2. If file_path needs encryption, update EncryptionService
// 3. Generate migration: pnpm db:generate
// 4. Apply: pnpm db:migrate
Adding Encrypted Column
// 1. Add column to schema
export const contacts = sqliteTable('contacts', {
// ... existing fields
address: text('address'), // NEW - needs encryption
});
// 2. Update EncryptionService to handle contacts.address
// src/services/EncryptionService.ts:
async encryptField(tableName: string, fieldName: string, value: string) {
if (tableName === 'contacts' && fieldName === 'address') {
return this.encrypt(value);
}
// ... existing encryption logic
}
// 3. Generate migration: pnpm db:generate
// 4. Migrate existing data:
// - Read all contacts
// - Encrypt address field
// - Update records
// 5. Apply migration: pnpm db:migrate
Foreign Key Changes
// DANGER: Changing foreign keys requires careful migration
// Step 1: Create new table with correct FK
// Step 2: Copy data from old table
// Step 3: Drop old table
// Step 4: Rename new table
// Example migration SQL:
CREATE TABLE cases_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, -- NEW FK
title TEXT NOT NULL
);
INSERT INTO cases_new SELECT * FROM cases;
DROP TABLE cases;
ALTER TABLE cases_new RENAME TO cases;
Troubleshooting
Migration Failed
# 1. Check migration status
pnpm db:migrate:status
# 2. Rollback failed migration
pnpm db:migrate:rollback
# 3. Fix schema.ts or migration SQL
# 4. Re-run: pnpm db:generate && pnpm db:migrate
Encryption Service Not Working
// Verify EncryptionService handles new field
const service = new EncryptionService(encryptionKey);
// Test encryption
const encrypted = await service.encryptField('contacts', 'address', 'test');
const decrypted = await service.decryptField('contacts', 'address', encrypted);
console.assert(decrypted === 'test', 'Encryption failed');
Foreign Key Violation
-- Check existing data before adding FK
SELECT cases.* FROM cases
LEFT JOIN users ON cases.user_id = users.id
WHERE users.id IS NULL;
-- If rows exist, fix orphaned records first:
DELETE FROM cases WHERE user_id NOT IN (SELECT id FROM users);
Best Practices
Before Migration
- Backup database:
pnpm db:backup - Review generated SQL in
src/db/migrations/ - Test on in-memory database
- Check for data loss scenarios
After Migration
- Verify migration applied:
pnpm db:migrate:status - Test encrypted fields still encrypt/decrypt
- Run full test suite:
pnpm test - Check audit logs for migration events
Encryption Guidelines
- Always encrypt PII (names, emails, addresses, phone numbers)
- Never encrypt foreign keys (breaks referential integrity)
- Never encrypt primary keys (breaks auto-increment)
- Test encryption before migration (avoid data corruption)
Example: Adding Case Priority
Schema Change
// src/db/schema.ts
export const cases = sqliteTable('cases', {
// ... existing fields
priority: text('priority', { enum: ['low', 'medium', 'high', 'urgent'] })
.default('medium'),
});
Generate Migration
pnpm db:generate
# Creates: src/db/migrations/0012_add_case_priority.sql
Review Generated SQL
-- src/db/migrations/0012_add_case_priority.sql
ALTER TABLE cases ADD COLUMN priority TEXT DEFAULT 'medium';
Apply Migration
# Automatic backup created
pnpm db:migrate
# Output:
# ✓ Backup created: justice-companion-backup-20251021-143022.db
# ✓ Migration 0012_add_case_priority.sql applied
Verify
# Check migration status
pnpm db:migrate:status
# Test in code
pnpm test src/repositories/CaseRepository.test.ts
Migration History
Track all migrations in mcp__memory:
// After successful migration
user: "Remember that we added case_priority field with enum values"
// Claude stores in memory for future reference
Emergency Rollback
# If migration breaks production:
1. Stop application
2. pnpm db:migrate:rollback
3. Restore from backup: pnpm db:backup:restore <backup-file>
4. Investigate issue
5. Fix and retry
References
- Schema:
src/db/schema.ts - Migrations:
src/db/migrations/ - Migration Runner:
src/db/migrate.ts - Encryption:
src/services/EncryptionService.ts - Tests:
src/db/database.test.ts
Golden Rule: Test migrations on in-memory database before applying to production. ALWAYS backup first.