/** * DatabaseConnection — High-level database abstraction. * * Wraps better-sqlite3 with: * - QueryBuilder for type-safe, injection-proof queries * - QueryAudit for logging and compliance * - Statement caching for performance * - Transaction support * * Usage: * const db = new DatabaseConnection(betterSqlite3Db, options); * const qb = new QueryBuilder('holdings').select(['ticker', 'shares']).where('type = ?', ['stock']); * const rows = db.all(qb); * const row = db.get(qb); * db.run(qb); */ import type BetterSqlite3 from 'better-sqlite3'; import { QueryBuilder } from './QueryBuilder'; import { QueryAudit, AuditAction } from './QueryAudit'; export interface DatabaseOptions { audit?: QueryAudit; logSlowQueries?: number; // milliseconds; logs queries slower than this } /** * DatabaseConnection — Safe, auditable, performant SQLite wrapper. */ export class DatabaseConnection { private db: BetterSqlite3.Database; private audit: QueryAudit; private logSlowQueries: number; private statementCache = new Map(); constructor(db: BetterSqlite3.Database, options: DatabaseOptions = {}) { this.db = db; this.audit = options.audit ?? new QueryAudit(); this.logSlowQueries = options.logSlowQueries ?? 100; // 100ms default } /** * Execute a SELECT query and return all rows. * Logs the query to the audit trail. */ all>(qb: QueryBuilder): T[] { const sql = qb.build(); const params = qb.params(); const startMs = performance.now(); try { const stmt = this.getOrCacheStatement(sql); const rows = stmt.all(...params) as T[]; const durationMs = performance.now() - startMs; this.audit.log(sql, params, AuditAction.READ, durationMs, rows.length); this.logIfSlow(sql, durationMs); return rows; } catch (err) { const durationMs = performance.now() - startMs; const errorMsg = err instanceof Error ? err.message : String(err); this.audit.log(sql, params, AuditAction.READ, durationMs, undefined, errorMsg); throw err; } } /** * Execute a SELECT query and return the first row only. * Returns null if no rows match. * Logs the query to the audit trail. */ get>(qb: QueryBuilder): T | null { const sql = qb.build(); const params = qb.params(); const startMs = performance.now(); try { const stmt = this.getOrCacheStatement(sql); const row = stmt.get(...params) as T | undefined; const durationMs = performance.now() - startMs; this.audit.log(sql, params, AuditAction.READ, durationMs, row ? 1 : 0); this.logIfSlow(sql, durationMs); return row ?? null; } catch (err) { const durationMs = performance.now() - startMs; const errorMsg = err instanceof Error ? err.message : String(err); this.audit.log(sql, params, AuditAction.READ, durationMs, undefined, errorMsg); throw err; } } /** * Execute an INSERT, UPDATE, or DELETE query. * Returns the number of rows affected. * Logs the query to the audit trail. */ run(qb: QueryBuilder): number { const sql = qb.build(); const params = qb.params(); const startMs = performance.now(); // Determine audit action from SQL const sqlUpper = sql.toUpperCase().trim(); const action = sqlUpper.startsWith('DELETE') ? AuditAction.DELETE : sqlUpper.startsWith('INSERT') ? AuditAction.WRITE : AuditAction.WRITE; try { const stmt = this.getOrCacheStatement(sql); const result = stmt.run(...params); const durationMs = performance.now() - startMs; this.audit.log(sql, params, action, durationMs, result.changes); this.logIfSlow(sql, durationMs); return result.changes; } catch (err) { const durationMs = performance.now() - startMs; const errorMsg = err instanceof Error ? err.message : String(err); this.audit.log(sql, params, action, durationMs, 0, errorMsg); throw err; } } /** * Execute a transaction — multiple queries as an atomic unit. * All queries must succeed, or all are rolled back. * * Usage: * db.transaction(() => { * db.run(qb1); * db.run(qb2); * }); */ transaction(fn: () => T): T { const txn = this.db.transaction(fn); return txn(); } /** * Get the raw better-sqlite3 Db instance (for advanced use only). * Prefer the DatabaseConnection methods. */ raw(): BetterSqlite3.Database { return this.db; } /** * Get the audit trail instance. */ getAudit(): QueryAudit { return this.audit; } /** * Clear the statement cache (for testing or extreme memory pressure). */ clearStatementCache(): void { this.statementCache.clear(); } /** * Get the audit trail instance. * Call db.printAudit() to see the most recent 100 queries. */ printAudit(): void { console.log(this.audit.report()); } // ── Private helpers ───────────────────────────────────────────────────── /** * Get or create a cached prepared statement. * Reduces compilation overhead for frequently-run queries. */ private getOrCacheStatement(sql: string): BetterSqlite3.Statement { let stmt = this.statementCache.get(sql); if (!stmt) { stmt = this.db.prepare(sql); this.statementCache.set(sql, stmt); } return stmt; } /** * Log slow queries to console. */ private logIfSlow(sql: string, durationMs: number): void { if (durationMs > this.logSlowQueries) { console.warn(`[SLOW QUERY] ${durationMs.toFixed(2)}ms\n ${sql}`); } } }