mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
Plan LL1.
apps/daemon/src/storage/db-inspect.ts gains a pure helper:
verifySqliteIntegrity({ db, quick? })
\u2192 DbIntegrityReport { ok, mode, issues[], elapsedMs, generatedAt }
Wraps two SQLite PRAGMAs:
integrity_check (or quick_check when quick=true) \u2014 verifies
each table + index page is internally consistent; quick skips
the index-content check for ~10x speedup on big DBs.
foreign_key_check \u2014 walks every FK and reports rows that
reference a missing parent. Only meaningful when foreign_keys
PRAGMA is enabled (which the daemon does in openDatabase).
Issues come back tagged kind='integrity' | 'foreign_key' so a
consumer can route alerts differently.
apps/daemon/src/server.ts: new POST /api/daemon/db/verify
(loopback-only via requireLocalDaemonRequest) accepts ?quick=1.
CLI: `od daemon db verify [--quick] [--json]`. Exit 0 on
ok=true, 4 on any issue. Operator one-liner:
od daemon db verify --quick
\u2192 [db verify] mode=quick_check ok=true issues=0 3ms
Daemon tests: 1808 \u2192 1813 (+5 cases on storage-db-verify:
healthy fresh DB ok, --quick mode tag, FK violation detection
(via foreign_keys=OFF + insert + foreign_keys=ON dance),
elapsedMs / generatedAt, populated DB still ok).
Co-authored-by: Tom Huang <1043269994@qq.com>
73 lines
2.5 KiB
TypeScript
73 lines
2.5 KiB
TypeScript
// Plan §3.LL1 — verifySqliteIntegrity() pure helper.
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import Database from 'better-sqlite3';
|
|
import { verifySqliteIntegrity } from '../src/storage/db-inspect.js';
|
|
|
|
let tmp: string;
|
|
let db: Database.Database;
|
|
|
|
beforeEach(async () => {
|
|
tmp = await mkdtemp(path.join(os.tmpdir(), 'od-db-verify-'));
|
|
db = new Database(path.join(tmp, 'app.sqlite'));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
db.close();
|
|
await rm(tmp, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('verifySqliteIntegrity', () => {
|
|
it('returns ok=true on a healthy fresh DB', () => {
|
|
db.exec(`CREATE TABLE projects (id TEXT PRIMARY KEY, name TEXT);`);
|
|
const report = verifySqliteIntegrity({ db });
|
|
expect(report.ok).toBe(true);
|
|
expect(report.issues).toEqual([]);
|
|
expect(report.mode).toBe('integrity_check');
|
|
});
|
|
|
|
it("honours the --quick flag (mode='quick_check')", () => {
|
|
const report = verifySqliteIntegrity({ db, quick: true });
|
|
expect(report.mode).toBe('quick_check');
|
|
expect(report.ok).toBe(true);
|
|
});
|
|
|
|
it('detects foreign-key violations', () => {
|
|
db.pragma('foreign_keys = OFF');
|
|
db.exec(`
|
|
CREATE TABLE parents (id INTEGER PRIMARY KEY);
|
|
CREATE TABLE kids (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id));
|
|
INSERT INTO kids (id, parent_id) VALUES (1, 999);
|
|
`);
|
|
db.pragma('foreign_keys = ON');
|
|
const report = verifySqliteIntegrity({ db });
|
|
expect(report.ok).toBe(false);
|
|
expect(report.issues.some((i) => i.kind === 'foreign_key')).toBe(true);
|
|
const fk = report.issues.find((i) => i.kind === 'foreign_key');
|
|
expect(fk?.message).toMatch(/kids/);
|
|
expect(fk?.message).toMatch(/parents/);
|
|
});
|
|
|
|
it('records elapsedMs + generatedAt', () => {
|
|
const before = Date.now();
|
|
const report = verifySqliteIntegrity({ db });
|
|
const after = Date.now();
|
|
expect(report.elapsedMs).toBeGreaterThanOrEqual(0);
|
|
expect(report.generatedAt).toBeGreaterThanOrEqual(before);
|
|
expect(report.generatedAt).toBeLessThanOrEqual(after + 50);
|
|
});
|
|
|
|
it('returns ok=true when there are tables but no FK violations', () => {
|
|
db.exec(`
|
|
CREATE TABLE a (id INTEGER PRIMARY KEY);
|
|
CREATE TABLE b (id INTEGER PRIMARY KEY, a_id INTEGER REFERENCES a(id));
|
|
INSERT INTO a VALUES (1), (2);
|
|
INSERT INTO b VALUES (1, 1), (2, 2);
|
|
`);
|
|
const report = verifySqliteIntegrity({ db });
|
|
expect(report.ok).toBe(true);
|
|
});
|
|
});
|