import { Database } from 'bun:sqlite'; import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js'; import { logger } from '../../utils/logger.js'; import { MigrationRunner } from './migrations/runner.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; /** * ClaudeMemDatabase - New entry point for the sqlite module * * Replaces SessionStore as the database coordinator. * Sets up bun:sqlite with optimized settings and runs all migrations. * * Usage: * const db = new ClaudeMemDatabase(); // uses default DB_PATH * const db = new ClaudeMemDatabase('/path/to/db.sqlite'); * const db = new ClaudeMemDatabase(':memory:'); // for tests */ export class ClaudeMemDatabase { public db: Database; constructor(dbPath: string = DB_PATH) { // Ensure data directory exists (skip for in-memory databases) if (dbPath !== ':memory:') { ensureDir(DATA_DIR); } // Create database connection this.db = new Database(dbPath, { 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}`); // Run all migrations const migrationRunner = new MigrationRunner(this.db); migrationRunner.runAllMigrations(); } /** * Close the database connection */ close(): void { this.db.close(); } } /** * SQLite Database singleton with migration support and optimized settings * @deprecated Use ClaudeMemDatabase instead for new code */ 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) { logger.info('DB', `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(); logger.info('DB', `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(); } // Re-export bun:sqlite Database type export { Database }; // Re-export MigrationRunner for external use export { MigrationRunner } from './migrations/runner.js'; // Re-export all module functions for convenient imports export * from './Sessions.js'; export * from './Observations.js'; export * from './Summaries.js'; export * from './Prompts.js'; export * from './Timeline.js'; export * from './Import.js'; export * from './transactions.js';