Adds expression index on lower(name) for memory-efficient case-insensitive searches, replacing in-memory caches that caused OOM on large codebases. Includes batched reference resolution, enhanced error reporting with detailed breakdown by error type, and improved CLI progress display for scanning phases.
139 lines
3.6 KiB
TypeScript
139 lines
3.6 KiB
TypeScript
/**
|
|
* Database Migrations
|
|
*
|
|
* Schema versioning and migration support.
|
|
*/
|
|
|
|
import { SqliteDatabase } from './sqlite-adapter';
|
|
|
|
/**
|
|
* Current schema version
|
|
*/
|
|
export const CURRENT_SCHEMA_VERSION = 3;
|
|
|
|
/**
|
|
* Migration definition
|
|
*/
|
|
interface Migration {
|
|
version: number;
|
|
description: string;
|
|
up: (db: SqliteDatabase) => 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);
|
|
`);
|
|
},
|
|
},
|
|
{
|
|
version: 3,
|
|
description: 'Add lower(name) expression index for memory-efficient case-insensitive lookups',
|
|
up: (db) => {
|
|
db.exec(`
|
|
CREATE INDEX IF NOT EXISTS idx_nodes_lower_name ON nodes(lower(name));
|
|
`);
|
|
},
|
|
},
|
|
];
|
|
|
|
/**
|
|
* Get the current schema version from the database
|
|
*/
|
|
export function getCurrentVersion(db: SqliteDatabase): 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: SqliteDatabase, 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: SqliteDatabase, 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: SqliteDatabase): boolean {
|
|
const current = getCurrentVersion(db);
|
|
return current < CURRENT_SCHEMA_VERSION;
|
|
}
|
|
|
|
/**
|
|
* Get list of pending migrations
|
|
*/
|
|
export function getPendingMigrations(db: SqliteDatabase): 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: SqliteDatabase
|
|
): 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,
|
|
}));
|
|
}
|