mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 21:22:28 +03:00
* fix(db): tolerate a SQLite build without the dbstat virtual table
getDatabaseStats() queried `dbstat` once per table with no guard. `dbstat` is
compile-time optional (ENABLE_DBSTAT_VTAB) and is absent from sql.js/WASM
builds, so on those runtimes the query throws and the error propagates out of
getDatabaseStats().
Every caller dies with it. Most visibly, GET and PATCH /api/settings/database
return HTTP 500, which makes the entire database settings page unusable — users
cannot read or change page size, cache size, or vacuum settings.
The function already anticipated missing virtual-table modules: the COUNT(*)
lookup a few lines above swallows "no such module:" errors. The dbstat query
simply sat outside that guard.
Probe dbstat once per call and skip the per-table size lookups when it is
unavailable, reporting size 0. Database-level figures (total size, page count,
cache size) come from pragmas and stay accurate; only per-table byte sizes are
lost, which is the correct trade against a hard 500.
Unrelated failures (I/O errors, corruption) still propagate.
Both spellings are handled: sql.js reports "no such module: dbstat" while
better-sqlite3 can surface "no such table: dbstat".
* test(db): cover prefixed driver errors and dbstat edge cases
Review follow-up on the previous commit.
The guard is deliberately unanchored because real drivers stringify errors
with their class name attached ("SqliteError: no such table: dbstat",
"RuntimeError: ..."). Nothing pinned that, so anchoring the regex would have
passed the suite while silently breaking every real driver. Add a case for the
prefixed form; it fails if a caret is introduced.
Also cover three shapes the fake previously could not express:
- a database with no user tables, which is what a fresh install hits first
- SUM(pgsize) returning NULL for a table occupying no pages
- dbstat answering the probe but failing on a later table, which documents
that a mid-iteration fault still propagates rather than being mistaken for
an absent module
Correct the source comment: the two error spellings track the SQLite build,
not the driver package, so the earlier attribution to better-sqlite3 was
wrong.
* docs(changelog): add fragment for the dbstat availability guard
Registers the new test with Stryker alongside the sibling db suites and adds
the changelog fragment for this fix.
---------
Co-authored-by: Nick Sullivan <nick@technick.ai>
108 lines
3.1 KiB
TypeScript
108 lines
3.1 KiB
TypeScript
/**
|
|
* Database Statistics Module
|
|
*
|
|
* Provides functions to retrieve database statistics including size, table counts, and performance metrics.
|
|
*/
|
|
|
|
import type { SqliteAdapter } from "./adapters/types";
|
|
import { getDbInstance } from "./core";
|
|
|
|
export interface DatabaseStats {
|
|
totalSize: number;
|
|
pageSize: number;
|
|
pageCount: number;
|
|
tables: Array<{
|
|
name: string;
|
|
rowCount: number;
|
|
size: number;
|
|
}>;
|
|
indexes: Array<{
|
|
name: string;
|
|
tableName: string;
|
|
}>;
|
|
walSize?: number;
|
|
cacheSize: number;
|
|
}
|
|
|
|
/**
|
|
* `dbstat` is a compile-time-optional SQLite virtual table (ENABLE_DBSTAT_VTAB).
|
|
* Builds without it — sql.js/WASM among them — reject the query with either
|
|
* "no such module: dbstat" or "no such table: dbstat" depending on the build,
|
|
* and drivers prefix their error class onto the message, so match loosely.
|
|
*
|
|
* Per-table byte sizes are a nice-to-have, so probe once and degrade to 0
|
|
* rather than failing the whole stats call — and with it every caller,
|
|
* including the database settings API.
|
|
*/
|
|
function isDbstatAvailable(db: SqliteAdapter): boolean {
|
|
try {
|
|
db.prepare(`SELECT SUM(pgsize) as size FROM dbstat WHERE name = ?`).get("sqlite_master");
|
|
return true;
|
|
} catch (error) {
|
|
if (error instanceof Error && /no such (module|table): dbstat/i.test(error.message)) {
|
|
return false;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export function getDatabaseStats(db: SqliteAdapter = getDbInstance()): DatabaseStats {
|
|
const pageSize = db.pragma("page_size", { simple: true }) as number;
|
|
const pageCount = db.pragma("page_count", { simple: true }) as number;
|
|
const cacheSize = db.pragma("cache_size", { simple: true }) as number;
|
|
const totalSize = pageSize * pageCount;
|
|
|
|
const tables = db
|
|
.prepare(
|
|
`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`
|
|
)
|
|
.all() as Array<{ name: string }>;
|
|
|
|
const dbstatAvailable = isDbstatAvailable(db);
|
|
|
|
const tableStats = tables.map((table) => {
|
|
let rowCount = 0;
|
|
try {
|
|
const quotedName = `"${table.name.replaceAll('"', '""')}"`;
|
|
const row = db.prepare(`SELECT COUNT(*) as count FROM ${quotedName}`).get() as
|
|
| { count: number }
|
|
| undefined;
|
|
rowCount = row?.count ?? 0;
|
|
} catch (error) {
|
|
if (!(error instanceof Error) || !error.message.startsWith("no such module:")) {
|
|
throw error;
|
|
}
|
|
// Optional virtual-table modules may be unavailable on this connection.
|
|
}
|
|
|
|
let size = 0;
|
|
if (dbstatAvailable) {
|
|
const tableSize = db
|
|
.prepare(`SELECT SUM(pgsize) as size FROM dbstat WHERE name = ?`)
|
|
.get(table.name) as { size: number | null } | undefined;
|
|
size = tableSize?.size || 0;
|
|
}
|
|
|
|
return {
|
|
name: table.name,
|
|
rowCount,
|
|
size,
|
|
};
|
|
});
|
|
|
|
const indexes = db
|
|
.prepare(
|
|
`SELECT name, tbl_name as tableName FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%' ORDER BY name`
|
|
)
|
|
.all() as Array<{ name: string; tableName: string }>;
|
|
|
|
return {
|
|
totalSize,
|
|
pageSize,
|
|
pageCount,
|
|
tables: tableStats,
|
|
indexes,
|
|
cacheSize,
|
|
};
|
|
}
|