openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
184 lines (183 loc) • 10.8 kB
JavaScript
import { t as openNodeSqliteDatabase, u as runSqliteImmediateTransactionSync } from "./node-sqlite-BpQX3W0e.js";
import { t as assertSqliteIntegrity } from "./sqlite-integrity-NpEtFIdK.js";
//#region src/infra/sqlite-strict.ts
const DEFAULT_STRICT_MIGRATION_BUSY_TIMEOUT_MS = 5e3;
const STRICT_MIGRATION_TABLE_PREFIX = "__openclaw_strict_migration_";
const SQLITE_ROWID_ALIASES = [
"_rowid_",
"rowid",
"oid"
];
function quoteSqliteIdentifier(identifier) {
return `"${identifier.replaceAll("\"", "\"\"")}"`;
}
function readMainTableList(db) {
return db.prepare("PRAGMA table_list").all().filter((row) => row.schema === "main" && typeof row.name === "string" && !row.name.startsWith("sqlite_"));
}
function readTableColumns(db, tableName) {
return db.prepare(`PRAGMA table_xinfo(${quoteSqliteIdentifier(tableName)})`).all();
}
function readVisibleColumns(db, tableName) {
return readTableColumns(db, tableName).filter((row) => Number(row.hidden ?? 0) === 0).map((row) => {
if (typeof row.name !== "string" || row.name.length === 0) throw new Error(`SQLite table ${tableName} has an invalid column name`);
return row.name;
});
}
function readTableRowidModel(db, tableName, tableRow) {
if (Number(tableRow.wr ?? 0) === 1) return {
alias: null,
storage: "without-rowid"
};
const columns = readTableColumns(db, tableName);
const primaryKeyColumns = columns.filter((column) => Number(column.pk ?? 0) > 0);
const primaryKeyIndex = db.prepare(`SELECT 1 AS found FROM pragma_index_list(?) WHERE origin = 'pk' LIMIT 1`).get(tableName);
const primaryKeyType = primaryKeyColumns[0]?.type;
if (primaryKeyColumns.length === 1 && typeof primaryKeyType === "string" && primaryKeyType.toUpperCase() === "INTEGER" && !primaryKeyIndex) return {
alias: null,
storage: "integer-primary-key"
};
const declaredNames = new Set(columns.flatMap((column) => typeof column.name === "string" ? [column.name.toLowerCase()] : []));
const alias = SQLITE_ROWID_ALIASES.find((candidate) => !declaredNames.has(candidate)) ?? null;
if (!alias) throw new Error(`SQLite table ${tableName} shadows every rowid alias; its implicit rowids cannot be migrated safely`);
return {
alias,
storage: "implicit"
};
}
function readCanonicalStrictTables(schemaSql) {
const canonical = openNodeSqliteDatabase(":memory:");
try {
canonical.exec(schemaSql);
const tables = readMainTableList(canonical).filter((row) => row.type === "table");
const nonStrict = tables.flatMap((row) => Number(row.strict ?? 0) === 1 || typeof row.name !== "string" ? [] : [row.name]);
if (nonStrict.length > 0) throw new Error(`Canonical SQLite schema contains non-STRICT tables: ${nonStrict.toSorted().join(", ")}`);
return tables.map((row) => {
if (typeof row.name !== "string") throw new Error("Canonical SQLite schema contains an unnamed table");
const schemaRow = canonical.prepare("SELECT sql FROM sqlite_schema WHERE type = 'table' AND name = ?").get(row.name);
if (typeof schemaRow?.sql !== "string") throw new Error(`Canonical SQLite table ${row.name} has no CREATE statement`);
const rowidModel = readTableRowidModel(canonical, row.name, row);
return {
columns: readVisibleColumns(canonical, row.name),
createSql: schemaRow.sql,
name: row.name,
rowidAlias: rowidModel.alias,
rowidStorage: rowidModel.storage,
usesAutoincrement: /\bAUTOINCREMENT\b/iu.test(schemaRow.sql)
};
}).toSorted((left, right) => left.name.localeCompare(right.name));
} finally {
canonical.close();
}
}
function rewriteCreateTableName(createSql, replacementName) {
const openingParen = createSql.indexOf("(");
if (openingParen === -1) throw new Error("Canonical SQLite table CREATE statement has no column list");
return `CREATE TABLE ${quoteSqliteIdentifier(replacementName)} ${createSql.slice(openingParen)}`;
}
function readPreservedSchemaObjects(db, tableNames) {
return db.prepare("SELECT type, name, tbl_name, sql FROM sqlite_schema WHERE type IN ('index', 'trigger', 'view')").all().flatMap((row) => {
if (row.type !== "index" && row.type !== "trigger" && row.type !== "view" || typeof row.name !== "string" || typeof row.tbl_name !== "string" || typeof row.sql !== "string" || row.type === "index" && !tableNames.has(row.tbl_name)) return [];
return [{
name: row.name,
sql: row.sql,
type: row.type
}];
}).toSorted((left, right) => {
const typeOrder = {
view: 0,
index: 1,
trigger: 2
};
return typeOrder[left.type] - typeOrder[right.type] || left.name.localeCompare(right.name);
});
}
function readAutoincrementHighWater(db, tableName) {
if (!db.prepare("SELECT 1 AS found FROM sqlite_schema WHERE type = 'table' AND name = 'sqlite_sequence'").get()) return null;
const row = db.prepare("SELECT CAST(seq AS TEXT) AS seq FROM sqlite_sequence WHERE name = ?").get(tableName);
if (row === void 0) return null;
const normalized = typeof row.seq === "string" ? /^(\d+)(?:\.0+)?$/u.exec(row.seq)?.[1] : null;
if (!normalized) throw new Error(`SQLite table ${tableName} has an invalid AUTOINCREMENT high-water mark (${typeof row.seq}: ${String(row.seq)})`);
return normalized;
}
function restoreAutoincrementHighWater(db, tableName, previousHighWater) {
if (previousHighWater === null) return;
const currentHighWater = readAutoincrementHighWater(db, tableName);
const restored = currentHighWater === null || BigInt(previousHighWater) > BigInt(currentHighWater) ? previousHighWater : currentHighWater;
db.prepare("DELETE FROM sqlite_sequence WHERE name = ?").run(tableName);
db.prepare("INSERT INTO sqlite_sequence (name, seq) VALUES (?, CAST(? AS INTEGER))").run(tableName, restored);
}
function assertMatchingColumns(tableName, currentColumns, canonicalColumns) {
const current = new Set(currentColumns);
const canonical = new Set(canonicalColumns);
const missing = canonicalColumns.filter((column) => !current.has(column));
const extra = currentColumns.filter((column) => !canonical.has(column));
if (missing.length === 0 && extra.length === 0) return;
const details = [missing.length > 0 ? `missing ${missing.join(", ")}` : "", extra.length > 0 ? `extra ${extra.join(", ")}` : ""].filter(Boolean).join("; ");
throw new Error(`SQLite table ${tableName} does not match its canonical columns (${details})`);
}
function readForeignKeysEnabled(db) {
const row = db.prepare("PRAGMA foreign_keys").get();
return Number(row?.foreign_keys ?? 0) === 1;
}
/**
* Rebuild canonical non-STRICT tables inside the caller's transaction.
* Foreign-key enforcement must be disabled before BEGIN; integrity is checked
* before this function returns so any bad row or relationship rolls back.
*/
function migrateSqliteSchemaToStrictInTransaction(db, schemaSql, options = {}) {
if (!db.isTransaction) throw new Error("SQLite STRICT schema migration requires an active transaction");
const canonicalTables = readCanonicalStrictTables(schemaSql);
db.exec(schemaSql);
const currentTableRows = new Map(readMainTableList(db).filter((row) => row.type === "table" && typeof row.name === "string").map((row) => [row.name, row]));
const tablesToMigrate = canonicalTables.filter((table) => Number(currentTableRows.get(table.name)?.strict ?? 0) !== 1);
if (tablesToMigrate.length === 0) return { migratedTables: [] };
if (readForeignKeysEnabled(db)) throw new Error("SQLite STRICT schema migration requires foreign_keys=OFF before BEGIN");
const preservedObjects = readPreservedSchemaObjects(db, new Set(tablesToMigrate.map((table) => table.name)));
for (const object of preservedObjects) if (object.type === "trigger") db.exec(`DROP TRIGGER ${quoteSqliteIdentifier(object.name)};`);
for (const object of preservedObjects) if (object.type === "view") db.exec(`DROP VIEW ${quoteSqliteIdentifier(object.name)};`);
for (const [index, table] of tablesToMigrate.entries()) {
const migrationTable = `${STRICT_MIGRATION_TABLE_PREFIX}${index}_${table.name}`;
if (currentTableRows.has(migrationTable)) throw new Error(`SQLite STRICT migration table already exists: ${migrationTable}`);
const currentColumns = readVisibleColumns(db, table.name);
assertMatchingColumns(table.name, currentColumns, table.columns);
const currentTableRow = currentTableRows.get(table.name);
if (!currentTableRow) throw new Error(`SQLite table ${table.name} disappeared during STRICT migration`);
const currentRowidModel = readTableRowidModel(db, table.name, currentTableRow);
if (currentRowidModel.storage !== table.rowidStorage) throw new Error(`SQLite table ${table.name} changes rowid storage from ${currentRowidModel.storage} to ${table.rowidStorage}; refusing an identity-changing STRICT migration`);
const previousHighWater = table.usesAutoincrement ? readAutoincrementHighWater(db, table.name) : null;
db.exec(rewriteCreateTableName(table.createSql, migrationTable));
const columns = table.columns.map(quoteSqliteIdentifier);
if (table.rowidAlias) columns.unshift(quoteSqliteIdentifier(table.rowidAlias));
const copyColumns = columns.join(", ");
try {
db.exec(`INSERT INTO ${quoteSqliteIdentifier(migrationTable)} (${copyColumns}) SELECT ${copyColumns} FROM ${quoteSqliteIdentifier(table.name)};`);
} catch (error) {
throw new Error(`Failed migrating SQLite table ${table.name} to STRICT`, { cause: error });
}
db.exec(`DROP TABLE ${quoteSqliteIdentifier(table.name)};`);
db.exec(`ALTER TABLE ${quoteSqliteIdentifier(migrationTable)} RENAME TO ${quoteSqliteIdentifier(table.name)};`);
restoreAutoincrementHighWater(db, table.name, previousHighWater);
}
db.exec(schemaSql);
const findObject = db.prepare("SELECT 1 AS found FROM sqlite_schema WHERE type = ? AND name = ? LIMIT 1");
for (const object of preservedObjects) if (!findObject.get(object.type, object.name)) db.exec(object.sql);
assertSqliteIntegrity(db, options.databaseLabel ?? "SQLite STRICT schema migration");
return { migratedTables: tablesToMigrate.map((table) => table.name) };
}
/** Atomically upgrade OpenClaw-owned tables described by a canonical STRICT schema. */
function migrateSqliteSchemaToStrict(db, schemaSql, options = {}) {
if (db.isTransaction) throw new Error("SQLite STRICT schema migration cannot start inside a transaction");
const foreignKeysWereEnabled = readForeignKeysEnabled(db);
if (foreignKeysWereEnabled) db.exec("PRAGMA foreign_keys = OFF;");
try {
return runSqliteImmediateTransactionSync(db, () => migrateSqliteSchemaToStrictInTransaction(db, schemaSql, options), {
busyTimeoutMs: options.busyTimeoutMs ?? DEFAULT_STRICT_MIGRATION_BUSY_TIMEOUT_MS,
databaseLabel: options.databaseLabel,
operationLabel: "sqlite.strict-schema-migration"
});
} finally {
if (foreignKeysWereEnabled) db.exec("PRAGMA foreign_keys = ON;");
}
}
//#endregion
export { migrateSqliteSchemaToStrictInTransaction as n, migrateSqliteSchemaToStrict as t };