mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
Plan GG1.
apps/daemon/src/storage/db-inspect.ts ships a pure helper:
inspectSqliteDatabase({ db, file }) \u2192 DaemonDbStatusReport
{
kind: 'sqlite',
location: <abs path>,
sizeBytes: <primary + WAL + SHM>,
schemaVersion: <user_version PRAGMA>,
tables: [{ name, rowCount }, ...],
generatedAt: <epoch ms>,
}
User tables only (sqlite_* / better_sqlite3_* excluded). Tables
walk in lexicographic order so the report is byte-deterministic.
Each table's row count is computed via a parameterised query
behind an identifier sanitiser ([A-Za-z_][A-Za-z0-9_]*) that
rejects malformed names; a corrupted view doesn't crash the
whole inspection — its row count just falls back to 0.
apps/daemon/src/server.ts: new GET /api/daemon/db wires the
inspector against the live DB handle + RUNTIME_DATA_DIR-relative
file path.
CLI: `od daemon db status [--json]`. Pretty-prints two columns
(table name padded to longest; row count). Helps ops sanity-check
deployments + compare expected-vs-actual table rosters across
daemon upgrades.
Daemon tests: 1776 \u2192 1784 (+8 cases on storage-db-inspect:
kind+location reporting, schemaVersion from PRAGMA, fresh DB
defaults to 0, system tables excluded, lexicographic ordering,
WAL companion size summed, generatedAt timestamp, empty DB
non-crash).
Co-authored-by: Tom Huang <1043269994@qq.com>
88 lines
3.4 KiB
TypeScript
88 lines
3.4 KiB
TypeScript
// Plan §3.GG1 — inspectSqliteDatabase() pure helper.
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import Database from 'better-sqlite3';
|
|
import { inspectSqliteDatabase } from '../src/storage/db-inspect.js';
|
|
|
|
let tmp: string;
|
|
let dbFile: string;
|
|
let db: Database.Database;
|
|
|
|
beforeEach(async () => {
|
|
tmp = await mkdtemp(path.join(os.tmpdir(), 'od-db-inspect-'));
|
|
dbFile = path.join(tmp, 'app.sqlite');
|
|
db = new Database(dbFile);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
db.close();
|
|
await rm(tmp, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('inspectSqliteDatabase', () => {
|
|
it('returns kind=sqlite and the absolute file path', async () => {
|
|
const report = await inspectSqliteDatabase({ db, file: dbFile });
|
|
expect(report.kind).toBe('sqlite');
|
|
expect(report.location).toBe(dbFile);
|
|
});
|
|
|
|
it('reports schemaVersion from the user_version PRAGMA', async () => {
|
|
db.pragma('user_version = 7');
|
|
const report = await inspectSqliteDatabase({ db, file: dbFile });
|
|
expect(report.schemaVersion).toBe(7);
|
|
});
|
|
|
|
it("reports schemaVersion=0 for a fresh DB (PRAGMA defaults)", async () => {
|
|
const report = await inspectSqliteDatabase({ db, file: dbFile });
|
|
expect(report.schemaVersion).toBe(0);
|
|
});
|
|
|
|
it('lists user tables with row counts; excludes sqlite_* system tables', async () => {
|
|
db.exec(`
|
|
CREATE TABLE projects (id INTEGER PRIMARY KEY, name TEXT);
|
|
CREATE TABLE files (id INTEGER PRIMARY KEY, path TEXT);
|
|
INSERT INTO projects (name) VALUES ('alpha'), ('beta'), ('gamma');
|
|
INSERT INTO files (path) VALUES ('a.txt'), ('b.txt');
|
|
`);
|
|
const report = await inspectSqliteDatabase({ db, file: dbFile });
|
|
const byName = Object.fromEntries(report.tables.map((t) => [t.name, t.rowCount]));
|
|
expect(byName).toEqual({ projects: 3, files: 2 });
|
|
expect(report.tables.some((t) => t.name.startsWith('sqlite_'))).toBe(false);
|
|
});
|
|
|
|
it('walks tables in lexicographic order', async () => {
|
|
db.exec(`
|
|
CREATE TABLE zebra (id INTEGER PRIMARY KEY);
|
|
CREATE TABLE alpha (id INTEGER PRIMARY KEY);
|
|
CREATE TABLE mango (id INTEGER PRIMARY KEY);
|
|
`);
|
|
const report = await inspectSqliteDatabase({ db, file: dbFile });
|
|
expect(report.tables.map((t) => t.name)).toEqual(['alpha', 'mango', 'zebra']);
|
|
});
|
|
|
|
it('sums file size = primary + WAL + SHM', async () => {
|
|
db.exec('CREATE TABLE x (id INTEGER PRIMARY KEY); INSERT INTO x VALUES (1);');
|
|
// Force-create a WAL companion file so the size sum exercises the
|
|
// WAL-aware path even on platforms where SQLite hasn't yet flushed.
|
|
await writeFile(`${dbFile}-wal`, Buffer.alloc(2048));
|
|
const report = await inspectSqliteDatabase({ db, file: dbFile });
|
|
expect(report.sizeBytes).toBeGreaterThanOrEqual(2048);
|
|
});
|
|
|
|
it('records generatedAt as a recent epoch ms', async () => {
|
|
const before = Date.now();
|
|
const report = await inspectSqliteDatabase({ db, file: dbFile });
|
|
const after = Date.now();
|
|
expect(report.generatedAt).toBeGreaterThanOrEqual(before);
|
|
expect(report.generatedAt).toBeLessThanOrEqual(after + 50);
|
|
});
|
|
|
|
it('handles an empty database without error', async () => {
|
|
const report = await inspectSqliteDatabase({ db, file: dbFile });
|
|
expect(report.tables).toEqual([]);
|
|
expect(report.sizeBytes).toBeGreaterThanOrEqual(0);
|
|
});
|
|
});
|