mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-18 13:11:35 +00:00
* fix(sqlite): require WAL-reset-safe Node runtime * docs(sqlite): document safe Node runtime floor * fix(sqlite): defer runtime library validation until use * fix(ci): align startup memory with Node 24.15
126 lines
4.3 KiB
TypeScript
126 lines
4.3 KiB
TypeScript
import type { DatabaseSync } from "node:sqlite";
|
|
|
|
export type SqliteIntegrityChecks = {
|
|
integrityCheck: "ok";
|
|
quickCheck: "ok";
|
|
};
|
|
|
|
type SqliteCheckPragma = "integrity_check" | "quick_check";
|
|
type SqliteForeignKeyViolation = {
|
|
fkid: bigint;
|
|
parent: string;
|
|
rowid: bigint | null;
|
|
table: string;
|
|
};
|
|
|
|
const MAX_REPORTED_FOREIGN_KEY_VIOLATIONS = 5;
|
|
|
|
/** Require structural, table/index, and referential consistency before trusting a database. */
|
|
export function assertSqliteIntegrity(
|
|
database: DatabaseSync,
|
|
databaseLabel: string,
|
|
): SqliteIntegrityChecks {
|
|
const quickCheck = runSqliteCheck(database, databaseLabel, "quick_check");
|
|
const integrityCheck = runSqliteCheck(database, databaseLabel, "integrity_check");
|
|
runSqliteForeignKeyCheck(database, databaseLabel);
|
|
return { integrityCheck, quickCheck };
|
|
}
|
|
|
|
/** Require table and associated index consistency before trusting indexed reads. */
|
|
export function assertSqliteTableIntegrity(
|
|
database: DatabaseSync,
|
|
databaseLabel: string,
|
|
tableName: string,
|
|
): void {
|
|
runSqliteCheck(database, `${databaseLabel} table ${tableName}`, "integrity_check", tableName);
|
|
}
|
|
|
|
function runSqliteCheck(
|
|
database: DatabaseSync,
|
|
databaseLabel: string,
|
|
pragma: SqliteCheckPragma,
|
|
tableName?: string,
|
|
): "ok" {
|
|
const argument = tableName ? `('${tableName.replaceAll("'", "''")}')` : "";
|
|
const rows = database.prepare(`PRAGMA ${pragma}${argument};`).all() as Array<
|
|
Record<string, unknown>
|
|
>;
|
|
const results = rows.map((row) => row[pragma] ?? Object.values(row)[0]);
|
|
if (results.length === 1 && results[0] === "ok") {
|
|
return "ok";
|
|
}
|
|
const details = results.map((result) => String(result)).join("; ") || "no result";
|
|
throw new Error(`SQLite ${pragma} failed for ${databaseLabel}: ${details}`);
|
|
}
|
|
|
|
function runSqliteForeignKeyCheck(database: DatabaseSync, databaseLabel: string): void {
|
|
let violationCount = 0;
|
|
const violations: SqliteForeignKeyViolation[] = [];
|
|
try {
|
|
// Use direct PRAGMA syntax because a real schema object can shadow the
|
|
// table-valued pragma name and make a corrupt database appear clean.
|
|
const statement = database.prepare("PRAGMA foreign_key_check;");
|
|
statement.setReadBigInts(true);
|
|
// OpenClaw's Node >=22.22.3 floor includes iterate(), added in Node 22.13.
|
|
for (const violation of statement.iterate() as Iterable<SqliteForeignKeyViolation>) {
|
|
violationCount += 1;
|
|
retainSortedForeignKeyViolation(violations, violation);
|
|
}
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
throw new Error(`SQLite foreign_key_check failed for ${databaseLabel}: ${message}`, {
|
|
cause: error,
|
|
});
|
|
}
|
|
if (violations.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const details = violations.map(formatSqliteForeignKeyViolation);
|
|
if (violationCount > MAX_REPORTED_FOREIGN_KEY_VIOLATIONS) {
|
|
details.push("additional violations omitted");
|
|
}
|
|
throw new Error(`SQLite foreign_key_check failed for ${databaseLabel}: ${details.join("; ")}`);
|
|
}
|
|
|
|
function retainSortedForeignKeyViolation(
|
|
retained: SqliteForeignKeyViolation[],
|
|
violation: SqliteForeignKeyViolation,
|
|
): void {
|
|
retained.push(violation);
|
|
retained.sort(compareSqliteForeignKeyViolations);
|
|
if (retained.length > MAX_REPORTED_FOREIGN_KEY_VIOLATIONS) {
|
|
retained.pop();
|
|
}
|
|
}
|
|
|
|
function compareSqliteForeignKeyViolations(
|
|
left: SqliteForeignKeyViolation,
|
|
right: SqliteForeignKeyViolation,
|
|
): number {
|
|
const tableOrder = Buffer.compare(Buffer.from(left.table), Buffer.from(right.table));
|
|
if (tableOrder !== 0) {
|
|
return tableOrder;
|
|
}
|
|
if (left.rowid === null || right.rowid === null) {
|
|
if (left.rowid !== right.rowid) {
|
|
return left.rowid === null ? -1 : 1;
|
|
}
|
|
} else if (left.rowid !== right.rowid) {
|
|
return left.rowid < right.rowid ? -1 : 1;
|
|
}
|
|
const parentOrder = Buffer.compare(Buffer.from(left.parent), Buffer.from(right.parent));
|
|
if (parentOrder !== 0) {
|
|
return parentOrder;
|
|
}
|
|
if (left.fkid === right.fkid) {
|
|
return 0;
|
|
}
|
|
return left.fkid < right.fkid ? -1 : 1;
|
|
}
|
|
|
|
function formatSqliteForeignKeyViolation(violation: SqliteForeignKeyViolation): string {
|
|
const row = violation.rowid === null ? "row without rowid" : `row ${violation.rowid.toString()}`;
|
|
return `${violation.table} ${row} references ${violation.parent} (foreign key ${violation.fkid.toString()})`;
|
|
}
|