import { Database } from 'bun:sqlite'; import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js'; // SQLite configuration constants const SQLITE_MMAP_SIZE_BYTES = 256 * 1024 * 1024; // 256MB const SQLITE_CACHE_SIZE_PAGES = 10_000; export interface Migration { version: number; up: (db: Database) => void; down?: (db: Database) => void; } let dbInstance: Database | null = null; /** * SQLite Database singleton with migration support and optimized settings */ export class DatabaseManager { private static instance: DatabaseManager; private db: Database | null = null; private migrations: Migration[] = []; static getInstance(): DatabaseManager { if (!DatabaseManager.instance) { DatabaseManager.instance = new DatabaseManager(); } return DatabaseManager.instance; } /** * Register a migration to be run during initialization */ registerMigration(migration: Migration): void { this.migrations.push(migration); // Keep migrations sorted by version this.migrations.sort((a, b) => a.version - b.version); } /** * Initialize database connection with optimized settings */ async initialize(): Promise { if (this.db) { return this.db; } // Ensure the data directory exists ensureDir(DATA_DIR); this.db = new Database(DB_PATH, { create: true, readwrite: true }); // Apply optimized SQLite settings this.db.run('PRAGMA journal_mode = WAL'); this.db.run('PRAGMA synchronous = NORMAL'); this.db.run('PRAGMA foreign_keys = ON'); this.db.run('PRAGMA temp_store = memory'); this.db.run(`PRAGMA mmap_size = ${SQLITE_MMAP_SIZE_BYTES}`); this.db.run(`PRAGMA cache_size = ${SQLITE_CACHE_SIZE_PAGES}`); // Initialize schema_versions table this.initializeSchemaVersions(); // Run migrations await this.runMigrations(); dbInstance = this.db; return this.db; } /** * Get the current database connection */ getConnection(): Database { if (!this.db) { throw new Error('Database not initialized. Call initialize() first.'); } return this.db; } /** * Execute a function within a transaction */ withTransaction(fn: (db: Database) => T): T { const db = this.getConnection(); const transaction = db.transaction(fn); return transaction(db); } /** * Close the database connection */ close(): void { if (this.db) { this.db.close(); this.db = null; dbInstance = null; } } /** * Initialize the schema_versions table */ private initializeSchemaVersions(): void { if (!this.db) return; this.db.run(` CREATE TABLE IF NOT EXISTS schema_versions ( id INTEGER PRIMARY KEY, version INTEGER UNIQUE NOT NULL, applied_at TEXT NOT NULL ) `); } /** * Run all pending migrations */ private async runMigrations(): Promise { if (!this.db) return; const query = this.db.query('SELECT version FROM schema_versions ORDER BY version'); const appliedVersions = query.all().map((row: any) => row.version); const maxApplied = appliedVersions.length > 0 ? Math.max(...appliedVersions) : 0; for (const migration of this.migrations) { if (migration.version > maxApplied) { console.log(`Applying migration ${migration.version}...`); const transaction = this.db.transaction(() => { migration.up(this.db!); const insertQuery = this.db!.query('INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)'); insertQuery.run(migration.version, new Date().toISOString()); }); transaction(); console.log(`Migration ${migration.version} applied successfully`); } } } /** * Get current schema version */ getCurrentVersion(): number { if (!this.db) return 0; const query = this.db.query('SELECT MAX(version) as version FROM schema_versions'); const result = query.get() as { version: number } | undefined; return result?.version || 0; } } /** * Get the global database instance (for compatibility) */ export function getDatabase(): Database { if (!dbInstance) { throw new Error('Database not initialized. Call DatabaseManager.getInstance().initialize() first.'); } return dbInstance; } /** * Initialize and get database manager */ export async function initializeDatabase(): Promise { const manager = DatabaseManager.getInstance(); return await manager.initialize(); } export { Database };