feat(db): report the SQLite driver and its durability on the DB health check (#10652)

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!
This commit is contained in:
Dizzle
2026-08-20 15:27:10 +02:00
committed by GitHub
parent 62f6e87869
commit 05a37634c2
4 changed files with 139 additions and 20 deletions

View File

@@ -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:

View File

@@ -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)

View File

@@ -5,10 +5,7 @@ type SqliteDatabase = SqliteAdapter;
type JsonRecord = Record<string, unknown>;
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<SqliteAdapter, "driver" | "name">): 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),
};
}

View File

@@ -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();
}
});