const Database = require('better-sqlite3'); const path = require('path'); const fs = require('fs'); /** * Migration System for BREEDR * Automatically runs on startup to ensure schema is correct */ class MigrationRunner { constructor(dbPath) { this.dbPath = dbPath; this.db = null; } connect() { this.db = new Database(this.dbPath); this.db.pragma('foreign_keys = ON'); } close() { if (this.db) { this.db.close(); } } // Get current schema version from database getSchemaVersion() { try { 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, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP, description TEXT ) `); return 0; } } // Record migration completion recordMigration(version, description) { this.db.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(version, description); } // Check if dogs table has old sire/dam columns hasOldSchema() { const columns = this.db.prepare("PRAGMA table_info(dogs)").all(); return columns.some(col => col.name === 'sire' || col.name === 'dam'); } // Check if litter_id column exists hasLitterIdColumn() { const columns = this.db.prepare("PRAGMA table_info(dogs)").all(); return columns.some(col => col.name === 'litter_id'); } // Migration 1: Remove sire/dam columns, use parents table migration001_removeOldParentColumns() { console.log('[Migration 001] Checking for old sire/dam columns...'); if (!this.hasOldSchema()) { console.log('[Migration 001] Schema is already correct, skipping'); return; } console.log('[Migration 001] Found old schema with sire/dam columns'); console.log('[Migration 001] Migrating to parents table...'); this.db.exec('BEGIN TRANSACTION'); try { // Ensure parents table exists this.db.exec(` CREATE TABLE IF NOT EXISTS parents ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL, parent_id INTEGER NOT NULL, parent_type TEXT NOT NULL CHECK(parent_type IN ('sire', 'dam')), FOREIGN KEY (dog_id) REFERENCES dogs(id) ON DELETE CASCADE, FOREIGN KEY (parent_id) REFERENCES dogs(id) ON DELETE CASCADE, UNIQUE(dog_id, parent_type) ); CREATE INDEX IF NOT EXISTS idx_parents_dog ON parents(dog_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('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'); const hasLitterId = columns.some(col => col.name === 'litter_id'); if (hasSire) { const sireResult = this.db.prepare(` INSERT OR IGNORE INTO parents (dog_id, parent_id, parent_type) SELECT id, sire, 'sire' FROM dogs_migration_backup WHERE sire IS NOT NULL `).run(); console.log(`[Migration 001] Migrated ${sireResult.changes} sire relationships`); } if (hasDam) { const damResult = this.db.prepare(` INSERT OR IGNORE INTO parents (dog_id, parent_id, parent_type) SELECT id, dam, 'dam' FROM dogs_migration_backup WHERE dam IS NOT NULL `).run(); 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, name TEXT NOT NULL, registration_number TEXT, microchip TEXT, sex TEXT CHECK(sex IN ('male', 'female')), birth_date DATE, breed TEXT, color TEXT, weight REAL, height REAL, notes TEXT, litter_id INTEGER, photo_urls TEXT, is_active INTEGER DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (litter_id) REFERENCES litters(id) ON DELETE SET NULL ) `); 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 } const columnsStr = columnList.join(', '); this.db.exec(`INSERT INTO dogs (${columnsStr}) SELECT ${columnsStr} FROM dogs_migration_backup`); 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; CREATE INDEX IF NOT EXISTS idx_dogs_name ON dogs(name); 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!'); } catch (error) { this.db.exec('ROLLBACK'); console.error('[Migration 001] ✗ Migration failed:', error.message); throw error; } } // Migration 2: Add litter_id column if missing migration002_addLitterIdColumn() { console.log('[Migration 002] Checking for litter_id column...'); if (this.hasLitterIdColumn()) { console.log('[Migration 002] litter_id column already exists, skipping'); return; } console.log('[Migration 002] Adding litter_id column...'); try { this.db.exec(` ALTER TABLE dogs ADD COLUMN litter_id INTEGER REFERENCES litters(id) ON DELETE SET NULL `); console.log('[Migration 002] ✓ litter_id column added'); } catch (error) { console.error('[Migration 002] ✗ Failed to add litter_id:', error.message); throw error; } } // Validate final schema validateSchema() { console.log('[Validation] Checking database schema...'); const checks = [ { name: 'Dogs table exists', test: () => { const tables = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='dogs'").all(); return tables.length > 0; } }, { name: 'Dogs table has no sire/dam columns', test: () => { const columns = this.db.prepare("PRAGMA table_info(dogs)").all(); return !columns.some(col => col.name === 'sire' || col.name === 'dam'); } }, { name: 'Parents table exists', test: () => { const tables = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='parents'").all(); return tables.length > 0; } }, { name: 'Litter_id column exists', test: () => this.hasLitterIdColumn() }, { name: 'Litters table exists', test: () => { const tables = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='litters'").all(); return tables.length > 0; } } ]; let allPassed = true; checks.forEach(check => { const passed = check.test(); const status = passed ? '✓' : '✗'; console.log(`[Validation] ${status} ${check.name}`); if (!passed) allPassed = false; }); if (allPassed) { console.log('[Validation] ✓ All schema checks passed!'); } else { console.warn('[Validation] ⚠ Some schema checks failed'); } return allPassed; } // Run all migrations runMigrations() { console.log('\n' + '='.repeat(60)); console.log('BREEDR Database Migration System'); console.log('='.repeat(60)); console.log(`Database: ${this.dbPath}\n`); this.connect(); try { 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'); } if (currentVersion < 2) { this.migration002_addLitterIdColumn(); this.recordMigration(2, 'Add litter_id column to dogs table'); } // Validate final schema console.log(''); const isValid = this.validateSchema(); const finalVersion = this.getSchemaVersion(); console.log('\n' + '='.repeat(60)); console.log(`Schema version: ${currentVersion} → ${finalVersion}`); console.log('Migration system complete!'); console.log('='.repeat(60) + '\n'); return isValid; } catch (error) { console.error('\n✗ Migration system failed:', error.message); console.error(error.stack); throw error; } finally { this.close(); } } } // Function to run migrations function runMigrations(dbPath) { const runner = new MigrationRunner(dbPath); return runner.runMigrations(); } 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); }