/** * Database Migrations * * Schema versioning and migration support. */ import Database from 'better-sqlite3'; /** * Current schema version */ export const CURRENT_SCHEMA_VERSION = 2; /** * Migration definition */ interface Migration { version: number; description: string; up: (db: Database.Database) => void; } /** * All migrations in order * * Note: Version 1 is the initial schema, handled by schema.sql * Future migrations go here. */ const migrations: Migration[] = [ { version: 2, description: 'Add project metadata, provenance tracking, and unresolved ref context', up: (db) => { db.exec(` CREATE TABLE IF NOT EXISTS project_metadata ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL ); ALTER TABLE unresolved_refs ADD COLUMN file_path TEXT NOT NULL DEFAULT ''; ALTER TABLE unresolved_refs ADD COLUMN language TEXT NOT NULL DEFAULT 'unknown'; ALTER TABLE edges ADD COLUMN provenance TEXT DEFAULT NULL; CREATE INDEX IF NOT EXISTS idx_unresolved_file_path ON unresolved_refs(file_path); CREATE INDEX IF NOT EXISTS idx_edges_provenance ON edges(provenance); `); }, }, ]; /** * Get the current schema version from the database */ export function getCurrentVersion(db: Database.Database): number { try { const row = db .prepare('SELECT MAX(version) as version FROM schema_versions') .get() as { version: number | null } | undefined; return row?.version ?? 0; } catch { // Table doesn't exist yet return 0; } } /** * Record a migration as applied */ function recordMigration(db: Database.Database, version: number, description: string): void { db.prepare( 'INSERT INTO schema_versions (version, applied_at, description) VALUES (?, ?, ?)' ).run(version, Date.now(), description); } /** * Run all pending migrations */ export function runMigrations(db: Database.Database, fromVersion: number): void { const pending = migrations.filter((m) => m.version > fromVersion); if (pending.length === 0) { return; } // Sort by version pending.sort((a, b) => a.version - b.version); // Run each migration in a transaction for (const migration of pending) { db.transaction(() => { migration.up(db); recordMigration(db, migration.version, migration.description); })(); } } /** * Check if the database needs migration */ export function needsMigration(db: Database.Database): boolean { const current = getCurrentVersion(db); return current < CURRENT_SCHEMA_VERSION; } /** * Get list of pending migrations */ export function getPendingMigrations(db: Database.Database): Migration[] { const current = getCurrentVersion(db); return migrations .filter((m) => m.version > current) .sort((a, b) => a.version - b.version); } /** * Get migration history from database */ export function getMigrationHistory( db: Database.Database ): Array<{ version: number; appliedAt: number; description: string | null }> { const rows = db .prepare('SELECT version, applied_at, description FROM schema_versions ORDER BY version') .all() as Array<{ version: number; applied_at: number; description: string | null }>; return rows.map((row) => ({ version: row.version, appliedAt: row.applied_at, description: row.description, })); }