mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-22 07:02:16 +03:00
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:
@@ -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:
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
75
tests/unit/db-health-driver.test.ts
Normal file
75
tests/unit/db-health-driver.test.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user