migrations.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. /**
  2. * Database Migrations
  3. *
  4. * Schema versioning and migration support.
  5. */
  6. import { SqliteDatabase } from './sqlite-adapter';
  7. /**
  8. * Current schema version
  9. */
  10. export const CURRENT_SCHEMA_VERSION = 5;
  11. /**
  12. * Migration definition
  13. */
  14. interface Migration {
  15. version: number;
  16. description: string;
  17. up: (db: SqliteDatabase) => void;
  18. }
  19. /**
  20. * All migrations in order
  21. *
  22. * Note: Version 1 is the initial schema, handled by schema.sql
  23. * Future migrations go here.
  24. */
  25. const migrations: Migration[] = [
  26. {
  27. version: 2,
  28. description: 'Add project metadata, provenance tracking, and unresolved ref context',
  29. up: (db) => {
  30. db.exec(`
  31. CREATE TABLE IF NOT EXISTS project_metadata (
  32. key TEXT PRIMARY KEY,
  33. value TEXT NOT NULL,
  34. updated_at INTEGER NOT NULL
  35. );
  36. ALTER TABLE unresolved_refs ADD COLUMN file_path TEXT NOT NULL DEFAULT '';
  37. ALTER TABLE unresolved_refs ADD COLUMN language TEXT NOT NULL DEFAULT 'unknown';
  38. ALTER TABLE edges ADD COLUMN provenance TEXT DEFAULT NULL;
  39. CREATE INDEX IF NOT EXISTS idx_unresolved_file_path ON unresolved_refs(file_path);
  40. CREATE INDEX IF NOT EXISTS idx_edges_provenance ON edges(provenance);
  41. `);
  42. },
  43. },
  44. {
  45. version: 3,
  46. description: 'Add lower(name) expression index for memory-efficient case-insensitive lookups',
  47. up: (db) => {
  48. db.exec(`
  49. CREATE INDEX IF NOT EXISTS idx_nodes_lower_name ON nodes(lower(name));
  50. `);
  51. },
  52. },
  53. {
  54. version: 4,
  55. description:
  56. 'Drop redundant idx_edges_source / idx_edges_target (covered by source_kind / target_kind composites)',
  57. up: (db) => {
  58. db.exec(`
  59. DROP INDEX IF EXISTS idx_edges_source;
  60. DROP INDEX IF EXISTS idx_edges_target;
  61. `);
  62. },
  63. },
  64. {
  65. version: 5,
  66. description:
  67. 'Add nodes.return_type — normalized return/result type for receiver-type inference (C++ singletons/factories, #645)',
  68. up: (db) => {
  69. db.exec(`
  70. ALTER TABLE nodes ADD COLUMN return_type TEXT;
  71. `);
  72. },
  73. },
  74. ];
  75. /**
  76. * Get the current schema version from the database
  77. */
  78. export function getCurrentVersion(db: SqliteDatabase): number {
  79. try {
  80. const row = db
  81. .prepare('SELECT MAX(version) as version FROM schema_versions')
  82. .get() as { version: number | null } | undefined;
  83. return row?.version ?? 0;
  84. } catch {
  85. // Table doesn't exist yet
  86. return 0;
  87. }
  88. }
  89. /**
  90. * Record a migration as applied
  91. */
  92. function recordMigration(db: SqliteDatabase, version: number, description: string): void {
  93. db.prepare(
  94. 'INSERT INTO schema_versions (version, applied_at, description) VALUES (?, ?, ?)'
  95. ).run(version, Date.now(), description);
  96. }
  97. /**
  98. * Run all pending migrations
  99. */
  100. export function runMigrations(db: SqliteDatabase, fromVersion: number): void {
  101. const pending = migrations.filter((m) => m.version > fromVersion);
  102. if (pending.length === 0) {
  103. return;
  104. }
  105. // Sort by version
  106. pending.sort((a, b) => a.version - b.version);
  107. // Run each migration in a transaction
  108. for (const migration of pending) {
  109. db.transaction(() => {
  110. migration.up(db);
  111. recordMigration(db, migration.version, migration.description);
  112. })();
  113. }
  114. }
  115. /**
  116. * Check if the database needs migration
  117. */
  118. export function needsMigration(db: SqliteDatabase): boolean {
  119. const current = getCurrentVersion(db);
  120. return current < CURRENT_SCHEMA_VERSION;
  121. }
  122. /**
  123. * Get list of pending migrations
  124. */
  125. export function getPendingMigrations(db: SqliteDatabase): Migration[] {
  126. const current = getCurrentVersion(db);
  127. return migrations
  128. .filter((m) => m.version > current)
  129. .sort((a, b) => a.version - b.version);
  130. }
  131. /**
  132. * Get migration history from database
  133. */
  134. export function getMigrationHistory(
  135. db: SqliteDatabase
  136. ): Array<{ version: number; appliedAt: number; description: string | null }> {
  137. const rows = db
  138. .prepare('SELECT version, applied_at, description FROM schema_versions ORDER BY version')
  139. .all() as Array<{ version: number; applied_at: number; description: string | null }>;
  140. return rows.map((row) => ({
  141. version: row.version,
  142. appliedAt: row.applied_at,
  143. description: row.description,
  144. }));
  145. }