index.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. /**
  2. * Database Layer
  3. *
  4. * Handles SQLite database initialization and connection management.
  5. */
  6. import Database from 'better-sqlite3';
  7. import * as fs from 'fs';
  8. import * as path from 'path';
  9. import { SchemaVersion } from '../types';
  10. import { runMigrations, getCurrentVersion, CURRENT_SCHEMA_VERSION } from './migrations';
  11. /**
  12. * Database connection wrapper with lifecycle management
  13. */
  14. export class DatabaseConnection {
  15. private db: Database.Database;
  16. private dbPath: string;
  17. private constructor(db: Database.Database, dbPath: string) {
  18. this.db = db;
  19. this.dbPath = dbPath;
  20. }
  21. /**
  22. * Initialize a new database at the given path
  23. */
  24. static initialize(dbPath: string): DatabaseConnection {
  25. // Ensure parent directory exists
  26. const dir = path.dirname(dbPath);
  27. if (!fs.existsSync(dir)) {
  28. fs.mkdirSync(dir, { recursive: true });
  29. }
  30. // Create and configure database
  31. const db = new Database(dbPath);
  32. // Enable foreign keys and WAL mode for better performance
  33. db.pragma('foreign_keys = ON');
  34. db.pragma('journal_mode = WAL');
  35. // Wait up to 2 minutes if database is locked by another process
  36. // (indexing operations can hold locks for extended periods)
  37. db.pragma('busy_timeout = 120000');
  38. // Performance tuning
  39. db.pragma('synchronous = NORMAL'); // Safe with WAL mode
  40. db.pragma('cache_size = -64000'); // 64 MB page cache
  41. db.pragma('temp_store = MEMORY'); // Temp tables in memory
  42. db.pragma('mmap_size = 268435456'); // 256 MB memory-mapped I/O
  43. // Run schema initialization
  44. const schemaPath = path.join(__dirname, 'schema.sql');
  45. const schema = fs.readFileSync(schemaPath, 'utf-8');
  46. db.exec(schema);
  47. // Record current schema version so migrations aren't re-applied on open
  48. const currentVersion = getCurrentVersion(db);
  49. if (currentVersion < CURRENT_SCHEMA_VERSION) {
  50. db.prepare(
  51. 'INSERT OR IGNORE INTO schema_versions (version, applied_at, description) VALUES (?, ?, ?)'
  52. ).run(CURRENT_SCHEMA_VERSION, Date.now(), 'Initial schema includes all migrations');
  53. }
  54. return new DatabaseConnection(db, dbPath);
  55. }
  56. /**
  57. * Open an existing database
  58. */
  59. static open(dbPath: string): DatabaseConnection {
  60. if (!fs.existsSync(dbPath)) {
  61. throw new Error(`Database not found: ${dbPath}`);
  62. }
  63. const db = new Database(dbPath);
  64. // Enable foreign keys and WAL mode
  65. db.pragma('foreign_keys = ON');
  66. db.pragma('journal_mode = WAL');
  67. // Wait up to 2 minutes if database is locked by another process
  68. // (indexing operations can hold locks for extended periods)
  69. db.pragma('busy_timeout = 120000');
  70. // Performance tuning
  71. db.pragma('synchronous = NORMAL');
  72. db.pragma('cache_size = -64000');
  73. db.pragma('temp_store = MEMORY');
  74. db.pragma('mmap_size = 268435456');
  75. // Check and run migrations if needed
  76. const conn = new DatabaseConnection(db, dbPath);
  77. const currentVersion = getCurrentVersion(db);
  78. if (currentVersion < CURRENT_SCHEMA_VERSION) {
  79. runMigrations(db, currentVersion);
  80. }
  81. return conn;
  82. }
  83. /**
  84. * Get the underlying database instance
  85. */
  86. getDb(): Database.Database {
  87. return this.db;
  88. }
  89. /**
  90. * Get database file path
  91. */
  92. getPath(): string {
  93. return this.dbPath;
  94. }
  95. /**
  96. * Get current schema version
  97. */
  98. getSchemaVersion(): SchemaVersion | null {
  99. const row = this.db
  100. .prepare('SELECT version, applied_at, description FROM schema_versions ORDER BY version DESC LIMIT 1')
  101. .get() as { version: number; applied_at: number; description: string | null } | undefined;
  102. if (!row) return null;
  103. return {
  104. version: row.version,
  105. appliedAt: row.applied_at,
  106. description: row.description ?? undefined,
  107. };
  108. }
  109. /**
  110. * Execute a function within a transaction
  111. */
  112. transaction<T>(fn: () => T): T {
  113. return this.db.transaction(fn)();
  114. }
  115. /**
  116. * Get database file size in bytes
  117. */
  118. getSize(): number {
  119. const stats = fs.statSync(this.dbPath);
  120. return stats.size;
  121. }
  122. /**
  123. * Optimize database (vacuum and analyze)
  124. */
  125. optimize(): void {
  126. this.db.exec('VACUUM');
  127. this.db.exec('ANALYZE');
  128. }
  129. /**
  130. * Close the database connection
  131. */
  132. close(): void {
  133. this.db.close();
  134. }
  135. /**
  136. * Check if the database connection is open
  137. */
  138. isOpen(): boolean {
  139. return this.db.open;
  140. }
  141. }
  142. /**
  143. * Default database filename
  144. */
  145. export const DATABASE_FILENAME = 'codegraph.db';
  146. /**
  147. * Get the default database path for a project
  148. */
  149. export function getDatabasePath(projectRoot: string): string {
  150. return path.join(projectRoot, '.codegraph', DATABASE_FILENAME);
  151. }