From 05a37634c2a9dbcb88ce79e4ebadc70111fa4a37 Mon Sep 17 00:00:00 2001 From: Dizzle <112548150+maxmad64bis@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:27:10 +0200 Subject: [PATCH] feat(db): report the SQLite driver and its durability on the DB health check (#10652) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — validated together with a batch of related maxmad64bis PRs in one combined worktree (typecheck:core clean, complexity/cognitive-complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution! --- docs/ops/DATABASE_GUIDE.md | 49 +++++++++++----- scripts/check/check-fabricated-docs.mjs | 1 + src/lib/db/healthCheck.ts | 34 +++++++++-- tests/unit/db-health-driver.test.ts | 75 +++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 20 deletions(-) create mode 100644 tests/unit/db-health-driver.test.ts diff --git a/docs/ops/DATABASE_GUIDE.md b/docs/ops/DATABASE_GUIDE.md index e0a0292a05..55d3771ce6 100644 --- a/docs/ops/DATABASE_GUIDE.md +++ b/docs/ops/DATABASE_GUIDE.md @@ -453,26 +453,47 @@ Run monthly during low-traffic windows. (WAL mode reduces the need, but doesn't `src/lib/db/healthCheck.ts` provides **DB-level health diagnostics**: -````bash -GET /api/db/health +Both verbs require authentication (`401` otherwise). `GET` diagnoses only; `POST` runs the +same check with `autoRepair` enabled. -Returns: +```bash +GET /api/db/health # diagnose +POST /api/db/health # diagnose + repair +``` + +The response is the `DbHealthCheckResult` produced by `runDbHealthCheck()` +(`src/lib/db/healthCheck.ts`): ```json { - "status": "healthy", - "checks": { - "writable": { "status": "pass" }, - "integrity": { "status": "pass", "result": "ok" }, - "foreign_keys": { "status": "pass", "violations": 0 }, - "orphaned_artifacts": { "status": "warn", "count": 12 }, - "table_sizes": { - "usage_history": { "rows": 12345, "size_mb": 12.3 }, - "call_logs": { "rows": 567, "size_mb": 2.1 } + "isHealthy": false, + "issues": [ + { + "type": "broken_reference", + "table": "domain_budgets", + "description": "Domain budgets referenced API keys that no longer exist.", + "count": 2 } - } + ], + "repairedCount": 0, + "backupCreated": false, + "autoRepair": false, + "checkedAt": "2026-08-18T09:00:00.000Z", + "driver": { "name": "better-sqlite3", "degraded": false } } -```` +``` + +| Field | Meaning | +| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| `isHealthy` | `true` when `issues` is empty. `driver` never influences it. | +| `issues[].type` | One of `integrity_check_failed`, `broken_reference`, `stale_snapshot`, `invalid_state`. | +| `repairedCount` | Rows repaired during this run; always `0` when `autoRepair` is false. | +| `backupCreated` | Whether a backup was taken before repairing. | +| `checkedAt` | ISO timestamp shared by the run and by any repair note it writes. | +| `driver.name` | SQLite driver serving the checked database. | +| `driver.degraded` | `true` when writes are not durably backed by the database file — the `sql.js` WASM fallback (whole-file persistence) or an in-memory database. | + +The same payload is returned by the `omniroute_db_health_check` MCP tool. Run `PRAGMA integrity_check` to detect corruption: diff --git a/scripts/check/check-fabricated-docs.mjs b/scripts/check/check-fabricated-docs.mjs index 53e7103bfe..90efcc9384 100644 --- a/scripts/check/check-fabricated-docs.mjs +++ b/scripts/check/check-fabricated-docs.mjs @@ -326,6 +326,7 @@ const ENV_VAR_DENYLIST = new Set([ "AUTHZ_NOT_INITIALIZED", // AuthzAssertionError code (AUTHZ_GUIDE.md) "MODULE_NOT_FOUND", // Node runtime error code watched by service supervisor (ELECTRON_GUIDE.md) "ERR_DLOPEN_FAILED", // Node native-module load error code (ELECTRON_GUIDE.md) + "SQLITE_FULL", // SQLite result code returned when the disk is full (DATABASE_GUIDE.md) // ── Code-symbol / naming-convention examples documented in prose ───────────── "UPPER_SNAKE", // the literal naming-convention token in the style guide (CODEBASE_DOCUMENTATION.md) "DEFAULT_TIMEOUT", // example constant name in the UPPER_SNAKE convention row (AGENTS.md) diff --git a/src/lib/db/healthCheck.ts b/src/lib/db/healthCheck.ts index be0a9ee45e..3916580036 100644 --- a/src/lib/db/healthCheck.ts +++ b/src/lib/db/healthCheck.ts @@ -5,10 +5,7 @@ type SqliteDatabase = SqliteAdapter; type JsonRecord = Record; export type DbHealthIssueType = - | "integrity_check_failed" - | "broken_reference" - | "stale_snapshot" - | "invalid_state"; + "integrity_check_failed" | "broken_reference" | "stale_snapshot" | "invalid_state"; export interface DbHealthIssue { type: DbHealthIssueType; @@ -17,6 +14,20 @@ export interface DbHealthIssue { count: number; } +/** Derived from the adapter contract so a new driver cannot drift out of sync here. */ +export type DbDriverName = SqliteAdapter["driver"]; + +export interface DbDriverHealth { + name: DbDriverName; + /** + * True when writes are not durably backed by the database file: the `sql.js` WASM + * fallback, or an in-memory database — which the cloud/build path opens through the + * NATIVE cascade, so the driver name alone would read as healthy. + * Informative only; `isHealthy` stays defined by `issues`. + */ + degraded: boolean; +} + export interface DbHealthCheckResult { isHealthy: boolean; issues: DbHealthIssue[]; @@ -24,6 +35,17 @@ export interface DbHealthCheckResult { backupCreated: boolean; autoRepair: boolean; checkedAt: string; + driver: DbDriverHealth; +} + +const IN_MEMORY_DB_NAME = ":memory:"; + +/** PURE: describe the driver serving `db`, and whether its writes survive a crash. */ +export function describeDbDriver(db: Pick): DbDriverHealth { + return { + name: db.driver, + degraded: db.driver === "sql.js" || db.name === IN_MEMORY_DB_NAME, + }; } interface RunDbHealthCheckOptions { @@ -383,8 +405,7 @@ function repairInvalidJsonRows( function getSchemaVersionIssueCount(db: SqliteDatabase, expectedSchemaVersion: string): number { if (!hasRows(db, "db_meta")) return 0; const row = db.prepare("SELECT value FROM db_meta WHERE key = 'schema_version'").get() as - | { value?: string | null } - | undefined; + { value?: string | null } | undefined; const current = typeof row?.value === "string" ? row.value : null; return current === expectedSchemaVersion ? 0 : 1; } @@ -561,5 +582,6 @@ export function runDbHealthCheck( backupCreated, autoRepair, checkedAt, + driver: describeDbDriver(db), }; } diff --git a/tests/unit/db-health-driver.test.ts b/tests/unit/db-health-driver.test.ts new file mode 100644 index 0000000000..efbc6b08f6 --- /dev/null +++ b/tests/unit/db-health-driver.test.ts @@ -0,0 +1,75 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-db-health-driver-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "db-health-driver-secret"; + +const core = await import("../../src/lib/db/core.ts"); +const healthCheckDb = await import("../../src/lib/db/healthCheck.ts"); +const driverFactory = await import("../../src/lib/db/adapters/driverFactory.ts"); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +// ─── describeDbDriver: pure decision ─────────────────── + +test("the sql.js WASM fallback is reported degraded", () => { + assert.deepEqual( + healthCheckDb.describeDbDriver({ driver: "sql.js", name: "/data/storage.sqlite" }), + { + name: "sql.js", + degraded: true, + } + ); +}); + +test("a native driver backed by a real file is not degraded", () => { + for (const driver of ["better-sqlite3", "node:sqlite", "bun:sqlite"] as const) { + assert.deepEqual(healthCheckDb.describeDbDriver({ driver, name: "/data/storage.sqlite" }), { + name: driver, + degraded: false, + }); + } +}); + +test("an in-memory database is degraded even on a native driver", () => { + for (const driver of ["better-sqlite3", "node:sqlite", "bun:sqlite", "sql.js"] as const) { + assert.deepEqual(healthCheckDb.describeDbDriver({ driver, name: ":memory:" }), { + name: driver, + degraded: true, + }); + } +}); + +// ─── runDbHealthCheck: wiring against ground truth ───── + +test("the health check reports the driver of the database it actually ran against", () => { + const db = core.getDbInstance(); + const result = healthCheckDb.runDbHealthCheck(db, { autoRepair: false }); + + assert.equal(result.driver.name, db.driver); + + // Precondition: the fixture is file-backed, so `degraded` is asserted as a literal + // rather than re-derived from the implementation's own condition. + assert.ok(db.name.endsWith(".sqlite"), `expected a file-backed fixture, got ${db.name}`); + assert.equal(result.driver.degraded, false); +}); + +test("an in-memory database opened through the real cascade is reported degraded", () => { + // Same tryOpenSync() the cloud/build path reaches through openSqliteDatabase(), so the + // `:memory:` name is observed from the adapter rather than assumed. + const memoryAdapter = driverFactory.tryOpenSync(":memory:"); + assert.ok(memoryAdapter, "expected the native cascade to open an in-memory database"); + try { + assert.equal(memoryAdapter.name, ":memory:"); + assert.equal(healthCheckDb.describeDbDriver(memoryAdapter).degraded, true); + } finally { + memoryAdapter.close(); + } +});