import { Database } from 'bun:sqlite';
import { DATA_DIR, DB_PATH, ensureDir } from '../../common/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;
/**
* ClaudeRecallDatabase - 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 ClaudeRecallDatabase(); // uses default DB_PATH
* const db = new ClaudeRecallDatabase('/path/to/db.sqlite');
* const db = new ClaudeRecallDatabase(':memory:'); // for tests
*/
export class ClaudeRecallDatabase {
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 ClaudeRecallDatabase 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<Database> {
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<T>(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<void> {
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<Database> {
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';