Files
electric-cherry/src/lib/server/db/index.ts
T
josh 8d1bb69591
CI / checks (push) Successful in 5m6s
Use ordered SQLite migrations (Refs #75)
2026-10-10 00:01:18 -04:00

68 lines
2.4 KiB
TypeScript

import Database from 'better-sqlite3';
import { mkdirSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { config } from '../config.js';
import { migrations } from './migrations.js';
import { runMigrations } from './migration-runner.js';
import { tuneConnection, tuneMigrationConnection } from './connection.js';
import { timeStatement } from './slow-statements.js';
let _db: Database.Database | null = null;
let optimizeTimer: NodeJS.Timeout | undefined;
export function db(): Database.Database {
if (_db) return _db;
mkdirSync(dirname(resolve(config.dbPath)), { recursive: true });
const handle = new Database(config.dbPath);
// Do all the risky init on a local handle first — only assign the module
// singleton once migrations succeed. A half-migrated handle that's cached
// silently breaks every subsequent query without a chance to retry.
try {
console.info('[database] startup migrations starting');
handle.pragma('journal_mode = WAL');
handle.pragma('auto_vacuum = INCREMENTAL');
handle.pragma('foreign_keys = ON');
tuneMigrationConnection(handle);
runMigrations(handle, migrations);
handle.pragma('optimize = 0x10002');
handle.pragma('shrink_memory');
tuneConnection(handle);
console.info('[database] startup migrations complete');
} catch (err) {
console.error('[database] startup migrations failed', err);
handle.close();
throw err;
}
// Instrument runtime queries after migrations. Each prepare must return a
// fresh statement: modes, permanent bindings, and active iterators are mutable
// state owned by the caller, even when another caller uses identical SQL.
const slowMs = Number(process.env.EC_SLOW_SQL_MS ?? 0);
if (Number.isFinite(slowMs) && slowMs > 0) {
const prepare = handle.prepare.bind(handle);
handle.prepare = ((sql: string) =>
timeStatement(prepare(sql), sql, slowMs)) as typeof handle.prepare;
}
_db = handle;
optimizeTimer = setInterval(() => {
try {
handle.pragma('optimize');
} catch (error) {
console.error('[database] optimize failed:', error);
}
}, 60 * 60_000);
optimizeTimer.unref();
return _db;
}
export function closeDb(): void {
clearInterval(optimizeTimer);
optimizeTimer = undefined;
if (_db) {
_db.close();
_db = null;
}
}