| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178 |
- /**
- * Database Layer
- *
- * Handles SQLite database initialization and connection management.
- */
- import Database from 'better-sqlite3';
- import * as fs from 'fs';
- import * as path from 'path';
- import { SchemaVersion } from '../types';
- import { runMigrations, getCurrentVersion, CURRENT_SCHEMA_VERSION } from './migrations';
- /**
- * Database connection wrapper with lifecycle management
- */
- export class DatabaseConnection {
- private db: Database.Database;
- private dbPath: string;
- private constructor(db: Database.Database, dbPath: string) {
- this.db = db;
- this.dbPath = dbPath;
- }
- /**
- * Initialize a new database at the given path
- */
- static initialize(dbPath: string): DatabaseConnection {
- // Ensure parent directory exists
- const dir = path.dirname(dbPath);
- if (!fs.existsSync(dir)) {
- fs.mkdirSync(dir, { recursive: true });
- }
- // Create and configure database
- const db = new Database(dbPath);
- // Enable foreign keys and WAL mode for better performance
- db.pragma('foreign_keys = ON');
- db.pragma('journal_mode = WAL');
- // Wait up to 2 minutes if database is locked by another process
- // (indexing operations can hold locks for extended periods)
- db.pragma('busy_timeout = 120000');
- // Performance tuning
- db.pragma('synchronous = NORMAL'); // Safe with WAL mode
- db.pragma('cache_size = -64000'); // 64 MB page cache
- db.pragma('temp_store = MEMORY'); // Temp tables in memory
- db.pragma('mmap_size = 268435456'); // 256 MB memory-mapped I/O
- // Run schema initialization
- const schemaPath = path.join(__dirname, 'schema.sql');
- const schema = fs.readFileSync(schemaPath, 'utf-8');
- db.exec(schema);
- // Record current schema version so migrations aren't re-applied on open
- const currentVersion = getCurrentVersion(db);
- if (currentVersion < CURRENT_SCHEMA_VERSION) {
- db.prepare(
- 'INSERT OR IGNORE INTO schema_versions (version, applied_at, description) VALUES (?, ?, ?)'
- ).run(CURRENT_SCHEMA_VERSION, Date.now(), 'Initial schema includes all migrations');
- }
- return new DatabaseConnection(db, dbPath);
- }
- /**
- * Open an existing database
- */
- static open(dbPath: string): DatabaseConnection {
- if (!fs.existsSync(dbPath)) {
- throw new Error(`Database not found: ${dbPath}`);
- }
- const db = new Database(dbPath);
- // Enable foreign keys and WAL mode
- db.pragma('foreign_keys = ON');
- db.pragma('journal_mode = WAL');
- // Wait up to 2 minutes if database is locked by another process
- // (indexing operations can hold locks for extended periods)
- db.pragma('busy_timeout = 120000');
- // Performance tuning
- db.pragma('synchronous = NORMAL');
- db.pragma('cache_size = -64000');
- db.pragma('temp_store = MEMORY');
- db.pragma('mmap_size = 268435456');
- // Check and run migrations if needed
- const conn = new DatabaseConnection(db, dbPath);
- const currentVersion = getCurrentVersion(db);
- if (currentVersion < CURRENT_SCHEMA_VERSION) {
- runMigrations(db, currentVersion);
- }
- return conn;
- }
- /**
- * Get the underlying database instance
- */
- getDb(): Database.Database {
- return this.db;
- }
- /**
- * Get database file path
- */
- getPath(): string {
- return this.dbPath;
- }
- /**
- * Get current schema version
- */
- getSchemaVersion(): SchemaVersion | null {
- const row = this.db
- .prepare('SELECT version, applied_at, description FROM schema_versions ORDER BY version DESC LIMIT 1')
- .get() as { version: number; applied_at: number; description: string | null } | undefined;
- if (!row) return null;
- return {
- version: row.version,
- appliedAt: row.applied_at,
- description: row.description ?? undefined,
- };
- }
- /**
- * Execute a function within a transaction
- */
- transaction<T>(fn: () => T): T {
- return this.db.transaction(fn)();
- }
- /**
- * Get database file size in bytes
- */
- getSize(): number {
- const stats = fs.statSync(this.dbPath);
- return stats.size;
- }
- /**
- * Optimize database (vacuum and analyze)
- */
- optimize(): void {
- this.db.exec('VACUUM');
- this.db.exec('ANALYZE');
- }
- /**
- * Close the database connection
- */
- close(): void {
- this.db.close();
- }
- /**
- * Check if the database connection is open
- */
- isOpen(): boolean {
- return this.db.open;
- }
- }
- /**
- * Default database filename
- */
- export const DATABASE_FILENAME = 'codegraph.db';
- /**
- * Get the default database path for a project
- */
- export function getDatabasePath(projectRoot: string): string {
- return path.join(projectRoot, '.codegraph', DATABASE_FILENAME);
- }
|