From e17ce2be2911b7e5ce789f36dcf63223cf252454 Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 10 Mar 2026 14:31:58 -0500 Subject: [PATCH] fix: Migration 003 - use dynamic column list to handle missing updated_at in old schema --- server/db/migrations.js | 94 ++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 53 deletions(-) diff --git a/server/db/migrations.js b/server/db/migrations.js index 867c050..a8a6676 100644 --- a/server/db/migrations.js +++ b/server/db/migrations.js @@ -30,7 +30,6 @@ class MigrationRunner { const result = this.db.prepare('SELECT version FROM schema_version ORDER BY version DESC LIMIT 1').get(); return result ? result.version : 0; } catch (error) { - // schema_version table doesn't exist, create it this.db.exec(` CREATE TABLE IF NOT EXISTS schema_version ( version INTEGER PRIMARY KEY, @@ -66,7 +65,6 @@ class MigrationRunner { "SELECT sql FROM sqlite_master WHERE type='table' AND name='health_records'" ).get(); if (!row) return false; - // Old constraint lists only the 5 legacy types return row.sql.includes("'test', 'vaccination', 'exam', 'treatment', 'certification'"); } catch (_) { return false; @@ -88,7 +86,6 @@ class MigrationRunner { this.db.exec('BEGIN TRANSACTION'); try { - // Ensure parents table exists this.db.exec(` CREATE TABLE IF NOT EXISTS parents ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -104,14 +101,12 @@ class MigrationRunner { CREATE INDEX IF NOT EXISTS idx_parents_parent ON parents(parent_id); `); - // Backup current dogs table this.db.exec('DROP TABLE IF EXISTS dogs_migration_backup'); this.db.exec('CREATE TABLE dogs_migration_backup AS SELECT * FROM dogs'); const backupCount = this.db.prepare('SELECT COUNT(*) as count FROM dogs_migration_backup').get(); console.log(`[Migration 001] Backed up ${backupCount.count} dogs`); - // Migrate parent relationships to parents table const columns = this.db.prepare("PRAGMA table_info(dogs_migration_backup)").all(); const hasSire = columns.some(col => col.name === 'sire'); const hasDam = columns.some(col => col.name === 'dam'); @@ -133,11 +128,9 @@ class MigrationRunner { console.log(`[Migration 001] Migrated ${damResult.changes} dam relationships`); } - // Drop old dogs table this.db.exec('DROP TABLE dogs'); console.log('[Migration 001] Dropped old dogs table'); - // Create new dogs table with correct schema this.db.exec(` CREATE TABLE dogs ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -161,10 +154,9 @@ class MigrationRunner { `); console.log('[Migration 001] Created new dogs table'); - // Restore data (excluding sire/dam columns) const columnList = ['id', 'name', 'registration_number', 'microchip', 'sex', 'birth_date', 'breed', 'color', 'weight', 'height', 'notes', 'photo_urls', 'is_active', 'created_at', 'updated_at']; if (hasLitterId) { - columnList.splice(11, 0, 'litter_id'); // Insert after notes + columnList.splice(11, 0, 'litter_id'); } const columnsStr = columnList.join(', '); @@ -173,7 +165,6 @@ class MigrationRunner { const restoredCount = this.db.prepare('SELECT COUNT(*) as count FROM dogs').get(); console.log(`[Migration 001] Restored ${restoredCount.count} dogs`); - // Create indexes this.db.exec(` CREATE UNIQUE INDEX IF NOT EXISTS idx_dogs_microchip ON dogs(microchip) WHERE microchip IS NOT NULL; @@ -182,9 +173,7 @@ class MigrationRunner { CREATE INDEX IF NOT EXISTS idx_dogs_registration ON dogs(registration_number); `); - // Clean up backup this.db.exec('DROP TABLE dogs_migration_backup'); - this.db.exec('COMMIT'); console.log('[Migration 001] ✓ Migration complete!'); @@ -219,8 +208,7 @@ class MigrationRunner { } // Migration 3: Remove old restrictive CHECK constraint on health_records.record_type - // The old schema only allowed: 'test','vaccination','exam','treatment','certification' - // The new schema allows any free-form text (ofa_clearance, etc.) + // Uses dynamic column detection so it works regardless of which columns exist in the old table migration003_removeHealthRecordTypeConstraint() { console.log('[Migration 003] Checking health_records.record_type constraint...'); @@ -234,61 +222,65 @@ class MigrationRunner { this.db.exec('BEGIN TRANSACTION'); try { - // Backup existing health records + // Backup existing records this.db.exec('DROP TABLE IF EXISTS health_records_migration_backup'); this.db.exec('CREATE TABLE health_records_migration_backup AS SELECT * FROM health_records'); const backupCount = this.db.prepare('SELECT COUNT(*) as count FROM health_records_migration_backup').get(); console.log(`[Migration 003] Backed up ${backupCount.count} health records`); - // Drop old table (constraint is baked in and cannot be altered) + // Dynamically get the columns that actually exist in the backup + // This handles old DBs that may be missing newer columns like updated_at + const existingCols = this.db.prepare('PRAGMA table_info(health_records_migration_backup)').all(); + const existingColNames = existingCols.map(c => c.name); + console.log(`[Migration 003] Existing columns: ${existingColNames.join(', ')}`); + + // Drop old constrained table this.db.exec('DROP TABLE health_records'); - // Recreate WITHOUT the old CHECK constraint on record_type + // Recreate WITHOUT the old CHECK constraint this.db.exec(` CREATE TABLE health_records ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - dog_id INTEGER NOT NULL, - record_type TEXT NOT NULL, - test_type TEXT, - test_name TEXT, - test_date TEXT NOT NULL, - ofa_result TEXT, - ofa_number TEXT, - performed_by TEXT, - expires_at TEXT, - document_url TEXT, - result TEXT, - vet_name TEXT, - next_due TEXT, - notes TEXT, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')), - FOREIGN KEY (dog_id) REFERENCES dogs(id) + id INTEGER PRIMARY KEY AUTOINCREMENT, + dog_id INTEGER NOT NULL, + record_type TEXT NOT NULL, + test_type TEXT, + test_name TEXT, + test_date TEXT NOT NULL, + ofa_result TEXT, + ofa_number TEXT, + performed_by TEXT, + expires_at TEXT, + document_url TEXT, + result TEXT, + vet_name TEXT, + next_due TEXT, + notes TEXT, + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (dog_id) REFERENCES dogs(id) ON DELETE CASCADE ) `); - // Restore all existing records + // Only restore columns that existed in the backup — new columns get their DEFAULT values + const newCols = ['id', 'dog_id', 'record_type', 'test_type', 'test_name', 'test_date', + 'ofa_result', 'ofa_number', 'performed_by', 'expires_at', + 'document_url', 'result', 'vet_name', 'next_due', 'notes', + 'created_at', 'updated_at']; + const colsToRestore = newCols.filter(c => existingColNames.includes(c)); + const colList = colsToRestore.join(', '); + + console.log(`[Migration 003] Restoring columns: ${colList}`); + this.db.exec(` - INSERT INTO health_records - (id, dog_id, record_type, test_type, test_name, test_date, - ofa_result, ofa_number, performed_by, expires_at, - document_url, result, vet_name, next_due, notes, - created_at, updated_at) - SELECT - id, dog_id, record_type, test_type, test_name, test_date, - ofa_result, ofa_number, performed_by, expires_at, - document_url, result, vet_name, next_due, notes, - created_at, updated_at - FROM health_records_migration_backup + INSERT INTO health_records (${colList}) + SELECT ${colList} FROM health_records_migration_backup `); const restoredCount = this.db.prepare('SELECT COUNT(*) as count FROM health_records').get(); console.log(`[Migration 003] Restored ${restoredCount.count} health records`); - // Clean up backup this.db.exec('DROP TABLE health_records_migration_backup'); - this.db.exec('COMMIT'); console.log('[Migration 003] ✓ health_records constraint removed successfully!'); @@ -372,7 +364,6 @@ class MigrationRunner { const currentVersion = this.getSchemaVersion(); console.log(`Current schema version: ${currentVersion}\n`); - // Run migrations in order if (currentVersion < 1) { this.migration001_removeOldParentColumns(); this.recordMigration(1, 'Migrate sire/dam columns to parents table'); @@ -388,7 +379,6 @@ class MigrationRunner { this.recordMigration(3, 'Remove old record_type CHECK constraint from health_records'); } - // Validate final schema console.log(''); const isValid = this.validateSchema(); @@ -410,7 +400,6 @@ class MigrationRunner { } } -// Function to run migrations function runMigrations(dbPath) { const runner = new MigrationRunner(dbPath); return runner.runMigrations(); @@ -418,7 +407,6 @@ function runMigrations(dbPath) { module.exports = { MigrationRunner, runMigrations }; -// Run migrations if called directly if (require.main === module) { const dbPath = process.env.DB_PATH || path.join(__dirname, '../../data/breedr.db'); runMigrations(dbPath); -- 2.49.1