Merge pull request 'fix: Migration 003 - dynamic column restore to handle missing updated_at' (#42) from fix/migration-003-dynamic-columns into master
Reviewed-on: #42
This commit was merged in pull request #42.
This commit is contained in:
@@ -30,7 +30,6 @@ class MigrationRunner {
|
|||||||
const result = this.db.prepare('SELECT version FROM schema_version ORDER BY version DESC LIMIT 1').get();
|
const result = this.db.prepare('SELECT version FROM schema_version ORDER BY version DESC LIMIT 1').get();
|
||||||
return result ? result.version : 0;
|
return result ? result.version : 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// schema_version table doesn't exist, create it
|
|
||||||
this.db.exec(`
|
this.db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS schema_version (
|
CREATE TABLE IF NOT EXISTS schema_version (
|
||||||
version INTEGER PRIMARY KEY,
|
version INTEGER PRIMARY KEY,
|
||||||
@@ -66,7 +65,6 @@ class MigrationRunner {
|
|||||||
"SELECT sql FROM sqlite_master WHERE type='table' AND name='health_records'"
|
"SELECT sql FROM sqlite_master WHERE type='table' AND name='health_records'"
|
||||||
).get();
|
).get();
|
||||||
if (!row) return false;
|
if (!row) return false;
|
||||||
// Old constraint lists only the 5 legacy types
|
|
||||||
return row.sql.includes("'test', 'vaccination', 'exam', 'treatment', 'certification'");
|
return row.sql.includes("'test', 'vaccination', 'exam', 'treatment', 'certification'");
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return false;
|
return false;
|
||||||
@@ -88,7 +86,6 @@ class MigrationRunner {
|
|||||||
this.db.exec('BEGIN TRANSACTION');
|
this.db.exec('BEGIN TRANSACTION');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure parents table exists
|
|
||||||
this.db.exec(`
|
this.db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS parents (
|
CREATE TABLE IF NOT EXISTS parents (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -104,14 +101,12 @@ class MigrationRunner {
|
|||||||
CREATE INDEX IF NOT EXISTS idx_parents_parent ON parents(parent_id);
|
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('DROP TABLE IF EXISTS dogs_migration_backup');
|
||||||
this.db.exec('CREATE TABLE dogs_migration_backup AS SELECT * FROM dogs');
|
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();
|
const backupCount = this.db.prepare('SELECT COUNT(*) as count FROM dogs_migration_backup').get();
|
||||||
console.log(`[Migration 001] Backed up ${backupCount.count} dogs`);
|
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 columns = this.db.prepare("PRAGMA table_info(dogs_migration_backup)").all();
|
||||||
const hasSire = columns.some(col => col.name === 'sire');
|
const hasSire = columns.some(col => col.name === 'sire');
|
||||||
const hasDam = columns.some(col => col.name === 'dam');
|
const hasDam = columns.some(col => col.name === 'dam');
|
||||||
@@ -133,11 +128,9 @@ class MigrationRunner {
|
|||||||
console.log(`[Migration 001] Migrated ${damResult.changes} dam relationships`);
|
console.log(`[Migration 001] Migrated ${damResult.changes} dam relationships`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop old dogs table
|
|
||||||
this.db.exec('DROP TABLE dogs');
|
this.db.exec('DROP TABLE dogs');
|
||||||
console.log('[Migration 001] Dropped old dogs table');
|
console.log('[Migration 001] Dropped old dogs table');
|
||||||
|
|
||||||
// Create new dogs table with correct schema
|
|
||||||
this.db.exec(`
|
this.db.exec(`
|
||||||
CREATE TABLE dogs (
|
CREATE TABLE dogs (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -161,10 +154,9 @@ class MigrationRunner {
|
|||||||
`);
|
`);
|
||||||
console.log('[Migration 001] Created new dogs table');
|
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'];
|
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) {
|
if (hasLitterId) {
|
||||||
columnList.splice(11, 0, 'litter_id'); // Insert after notes
|
columnList.splice(11, 0, 'litter_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnsStr = columnList.join(', ');
|
const columnsStr = columnList.join(', ');
|
||||||
@@ -173,7 +165,6 @@ class MigrationRunner {
|
|||||||
const restoredCount = this.db.prepare('SELECT COUNT(*) as count FROM dogs').get();
|
const restoredCount = this.db.prepare('SELECT COUNT(*) as count FROM dogs').get();
|
||||||
console.log(`[Migration 001] Restored ${restoredCount.count} dogs`);
|
console.log(`[Migration 001] Restored ${restoredCount.count} dogs`);
|
||||||
|
|
||||||
// Create indexes
|
|
||||||
this.db.exec(`
|
this.db.exec(`
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_dogs_microchip
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_dogs_microchip
|
||||||
ON dogs(microchip) WHERE microchip IS NOT NULL;
|
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);
|
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('DROP TABLE dogs_migration_backup');
|
||||||
|
|
||||||
this.db.exec('COMMIT');
|
this.db.exec('COMMIT');
|
||||||
console.log('[Migration 001] ✓ Migration complete!');
|
console.log('[Migration 001] ✓ Migration complete!');
|
||||||
|
|
||||||
@@ -219,8 +208,7 @@ class MigrationRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Migration 3: Remove old restrictive CHECK constraint on health_records.record_type
|
// Migration 3: Remove old restrictive CHECK constraint on health_records.record_type
|
||||||
// The old schema only allowed: 'test','vaccination','exam','treatment','certification'
|
// Uses dynamic column detection so it works regardless of which columns exist in the old table
|
||||||
// The new schema allows any free-form text (ofa_clearance, etc.)
|
|
||||||
migration003_removeHealthRecordTypeConstraint() {
|
migration003_removeHealthRecordTypeConstraint() {
|
||||||
console.log('[Migration 003] Checking health_records.record_type constraint...');
|
console.log('[Migration 003] Checking health_records.record_type constraint...');
|
||||||
|
|
||||||
@@ -234,17 +222,23 @@ class MigrationRunner {
|
|||||||
this.db.exec('BEGIN TRANSACTION');
|
this.db.exec('BEGIN TRANSACTION');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Backup existing health records
|
// Backup existing records
|
||||||
this.db.exec('DROP TABLE IF EXISTS health_records_migration_backup');
|
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');
|
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();
|
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`);
|
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');
|
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(`
|
this.db.exec(`
|
||||||
CREATE TABLE health_records (
|
CREATE TABLE health_records (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -264,31 +258,29 @@ class MigrationRunner {
|
|||||||
notes TEXT,
|
notes TEXT,
|
||||||
created_at TEXT DEFAULT (datetime('now')),
|
created_at TEXT DEFAULT (datetime('now')),
|
||||||
updated_at TEXT DEFAULT (datetime('now')),
|
updated_at TEXT DEFAULT (datetime('now')),
|
||||||
FOREIGN KEY (dog_id) REFERENCES dogs(id)
|
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(`
|
this.db.exec(`
|
||||||
INSERT INTO health_records
|
INSERT INTO health_records (${colList})
|
||||||
(id, dog_id, record_type, test_type, test_name, test_date,
|
SELECT ${colList} FROM health_records_migration_backup
|
||||||
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
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const restoredCount = this.db.prepare('SELECT COUNT(*) as count FROM health_records').get();
|
const restoredCount = this.db.prepare('SELECT COUNT(*) as count FROM health_records').get();
|
||||||
console.log(`[Migration 003] Restored ${restoredCount.count} health records`);
|
console.log(`[Migration 003] Restored ${restoredCount.count} health records`);
|
||||||
|
|
||||||
// Clean up backup
|
|
||||||
this.db.exec('DROP TABLE health_records_migration_backup');
|
this.db.exec('DROP TABLE health_records_migration_backup');
|
||||||
|
|
||||||
this.db.exec('COMMIT');
|
this.db.exec('COMMIT');
|
||||||
console.log('[Migration 003] ✓ health_records constraint removed successfully!');
|
console.log('[Migration 003] ✓ health_records constraint removed successfully!');
|
||||||
|
|
||||||
@@ -372,7 +364,6 @@ class MigrationRunner {
|
|||||||
const currentVersion = this.getSchemaVersion();
|
const currentVersion = this.getSchemaVersion();
|
||||||
console.log(`Current schema version: ${currentVersion}\n`);
|
console.log(`Current schema version: ${currentVersion}\n`);
|
||||||
|
|
||||||
// Run migrations in order
|
|
||||||
if (currentVersion < 1) {
|
if (currentVersion < 1) {
|
||||||
this.migration001_removeOldParentColumns();
|
this.migration001_removeOldParentColumns();
|
||||||
this.recordMigration(1, 'Migrate sire/dam columns to parents table');
|
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');
|
this.recordMigration(3, 'Remove old record_type CHECK constraint from health_records');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate final schema
|
|
||||||
console.log('');
|
console.log('');
|
||||||
const isValid = this.validateSchema();
|
const isValid = this.validateSchema();
|
||||||
|
|
||||||
@@ -410,7 +400,6 @@ class MigrationRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to run migrations
|
|
||||||
function runMigrations(dbPath) {
|
function runMigrations(dbPath) {
|
||||||
const runner = new MigrationRunner(dbPath);
|
const runner = new MigrationRunner(dbPath);
|
||||||
return runner.runMigrations();
|
return runner.runMigrations();
|
||||||
@@ -418,7 +407,6 @@ function runMigrations(dbPath) {
|
|||||||
|
|
||||||
module.exports = { MigrationRunner, runMigrations };
|
module.exports = { MigrationRunner, runMigrations };
|
||||||
|
|
||||||
// Run migrations if called directly
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
const dbPath = process.env.DB_PATH || path.join(__dirname, '../../data/breedr.db');
|
const dbPath = process.env.DB_PATH || path.join(__dirname, '../../data/breedr.db');
|
||||||
runMigrations(dbPath);
|
runMigrations(dbPath);
|
||||||
|
|||||||
Reference in New Issue
Block a user