// "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."
| 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__*"] |
Safe database schema management with Drizzle ORM for Justice Companion's encrypted SQLite database.
src/db/schema.tsThese fields MUST use EncryptionService:
users.emailusers.full_namecases.titlecases.descriptionevidence.file_pathevidence.noteschat_conversations.message_contentdocuments.file_pathcontacts.emailcontacts.phone_numbercontacts.addressRule: If adding columns to these tables, determine if encryption is needed.
// 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'),
});
pnpm db:generate
# Creates: src/db/migrations/0001_add_case_number.sql
-- Check generated SQL for safety
ALTER TABLE cases ADD COLUMN case_number TEXT;
# Run in test environment first
pnpm test src/db/database.test.ts
pnpm db:migrate
# Automatic backup created before migration
pnpm db:migrate:rollback
# Reverts last migration
// 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
// 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
// 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;
# 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
// 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');
-- 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);
pnpm db:backupsrc/db/migrations/pnpm db:migrate:statuspnpm test// src/db/schema.ts
export const cases = sqliteTable('cases', {
// ... existing fields
priority: text('priority', { enum: ['low', 'medium', 'high', 'urgent'] })
.default('medium'),
});
pnpm db:generate
# Creates: src/db/migrations/0012_add_case_priority.sql
-- src/db/migrations/0012_add_case_priority.sql
ALTER TABLE cases ADD COLUMN priority TEXT DEFAULT 'medium';
# Automatic backup created
pnpm db:migrate
# Output:
# ✓ Backup created: justice-companion-backup-20251021-143022.db
# ✓ Migration 0012_add_case_priority.sql applied
# Check migration status
pnpm db:migrate:status
# Test in code
pnpm test src/repositories/CaseRepository.test.ts
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
# 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
src/db/schema.tssrc/db/migrations/src/db/migrate.tssrc/services/EncryptionService.tssrc/db/database.test.tsGolden Rule: Test migrations on in-memory database before applying to production. ALWAYS backup first.