mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(infra): fail fast for sync sqlite facade execution
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
// Covers the compile-only Kysely facade used by sync node:sqlite helpers.
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
clearNodeSqliteKyselyCacheForDatabase,
|
||||
executeSqliteQuerySync,
|
||||
getNodeSqliteKysely,
|
||||
} from "./kysely-sync.js";
|
||||
|
||||
type SyncHelperTestDatabase = {
|
||||
items: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
describe("kysely sync helpers", () => {
|
||||
let database: DatabaseSync | undefined;
|
||||
|
||||
afterEach(() => {
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
clearNodeSqliteKyselyCacheForDatabase(database);
|
||||
database.close();
|
||||
database = undefined;
|
||||
});
|
||||
|
||||
it("keeps the builder facade compile-only and fails direct execution", async () => {
|
||||
database = new DatabaseSync(":memory:");
|
||||
database.exec("create table items (id integer primary key, name text not null)");
|
||||
const db = getNodeSqliteKysely<SyncHelperTestDatabase>(database);
|
||||
|
||||
const insertQuery = db.insertInto("items").values({ id: 1, name: "Ada" });
|
||||
expect(insertQuery.compile().sql).toContain("insert into");
|
||||
|
||||
executeSqliteQuerySync(database, insertQuery);
|
||||
expect(executeSqliteQuerySync(database, db.selectFrom("items").selectAll()).rows).toEqual([
|
||||
{ id: 1, name: "Ada" },
|
||||
]);
|
||||
|
||||
const compileOnlyError = /compile-only Kysely facade/;
|
||||
await expect(db.selectFrom("items").selectAll().execute()).rejects.toThrow(compileOnlyError);
|
||||
await expect(db.insertInto("items").values({ id: 2, name: "Grace" }).execute()).rejects.toThrow(
|
||||
compileOnlyError,
|
||||
);
|
||||
await expect(
|
||||
db.transaction().execute(async (trx) => {
|
||||
await trx.insertInto("items").values({ id: 3, name: "Lin" }).execute();
|
||||
}),
|
||||
).rejects.toThrow(compileOnlyError);
|
||||
await expectCompileOnlyRejection(db.startTransaction().execute());
|
||||
await expectCompileOnlyRejection(consumeStream(db.selectFrom("items").selectAll().stream()));
|
||||
await expectCompileOnlyRejection(db.selectFrom("items").selectAll().execute());
|
||||
|
||||
expect(
|
||||
executeSqliteQuerySync(database, db.selectFrom("items").select(["id", "name"])).rows,
|
||||
).toEqual([{ id: 1, name: "Ada" }]);
|
||||
});
|
||||
});
|
||||
|
||||
async function expectCompileOnlyRejection(promise: Promise<unknown>): Promise<void> {
|
||||
await expect(Promise.race([promise, timeoutAfter(500)])).rejects.toThrow(
|
||||
/compile-only Kysely facade/,
|
||||
);
|
||||
}
|
||||
|
||||
function timeoutAfter(ms: number): Promise<never> {
|
||||
return new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error("timed out waiting for compile-only rejection")), ms);
|
||||
});
|
||||
}
|
||||
|
||||
async function consumeStream<Row>(stream: AsyncIterableIterator<Row>): Promise<Row[]> {
|
||||
const rows: Row[] = [];
|
||||
for await (const row of stream) {
|
||||
rows.push(row);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
@@ -1,7 +1,14 @@
|
||||
// Adapts node:sqlite sync database calls for Kysely-style query execution.
|
||||
import type { DatabaseSync, SQLInputValue } from "node:sqlite";
|
||||
import type { CompiledQuery, Kysely, QueryResult } from "kysely";
|
||||
import { InsertQueryNode, Kysely as KyselyInstance } from "kysely";
|
||||
import type {
|
||||
CompiledQuery,
|
||||
DatabaseConnection,
|
||||
Driver,
|
||||
Kysely,
|
||||
QueryResult,
|
||||
TransactionSettings,
|
||||
} from "kysely";
|
||||
import { InsertQueryNode, Kysely as KyselyInstance, SqliteAdapter } from "kysely";
|
||||
import { NodeSqliteKyselyDialect } from "./kysely-node-sqlite.js";
|
||||
|
||||
// Sync query helpers execute compiled Kysely SQL against node:sqlite without
|
||||
@@ -18,7 +25,7 @@ export function getNodeSqliteKysely<Database>(db: DatabaseSync): Kysely<Database
|
||||
return existing as Kysely<Database>;
|
||||
}
|
||||
const kysely = new KyselyInstance<Database>({
|
||||
dialect: new NodeSqliteKyselyDialect({ database: db }),
|
||||
dialect: new CompileOnlyNodeSqliteKyselyDialect(),
|
||||
});
|
||||
kyselyByDatabase.set(db, kysely as Kysely<unknown>);
|
||||
return kysely;
|
||||
@@ -70,3 +77,63 @@ export function executeSqliteQueryTakeFirstSync<Row>(
|
||||
export function clearNodeSqliteKyselyCacheForDatabase(db: DatabaseSync): void {
|
||||
kyselyByDatabase.delete(db);
|
||||
}
|
||||
|
||||
class CompileOnlyNodeSqliteKyselyDialect extends NodeSqliteKyselyDialect {
|
||||
constructor() {
|
||||
super({ database: createUnavailableDatabase });
|
||||
}
|
||||
|
||||
override createDriver(): Driver {
|
||||
return new CompileOnlySqliteDriver();
|
||||
}
|
||||
|
||||
override createAdapter(): SqliteAdapter {
|
||||
return new CompileOnlySqliteAdapter();
|
||||
}
|
||||
}
|
||||
|
||||
class CompileOnlySqliteDriver implements Driver {
|
||||
async init(): Promise<void> {}
|
||||
|
||||
async acquireConnection(): Promise<DatabaseConnection> {
|
||||
throw createCompileOnlyExecutionError();
|
||||
}
|
||||
|
||||
async beginTransaction(
|
||||
_connection: DatabaseConnection,
|
||||
_settings: TransactionSettings,
|
||||
): Promise<void> {
|
||||
throw createCompileOnlyExecutionError();
|
||||
}
|
||||
|
||||
async commitTransaction(_connection: DatabaseConnection): Promise<void> {
|
||||
throw createCompileOnlyExecutionError();
|
||||
}
|
||||
|
||||
async rollbackTransaction(_connection: DatabaseConnection): Promise<void> {
|
||||
throw createCompileOnlyExecutionError();
|
||||
}
|
||||
|
||||
async releaseConnection(_connection: DatabaseConnection): Promise<void> {}
|
||||
|
||||
async destroy(): Promise<void> {}
|
||||
}
|
||||
|
||||
function createCompileOnlyExecutionError(): Error {
|
||||
return new Error(
|
||||
"getNodeSqliteKysely() returns a compile-only Kysely facade; use executeSqliteQuerySync() to execute node:sqlite queries.",
|
||||
);
|
||||
}
|
||||
|
||||
function createUnavailableDatabase(): never {
|
||||
throw createCompileOnlyExecutionError();
|
||||
}
|
||||
|
||||
class CompileOnlySqliteAdapter extends SqliteAdapter {
|
||||
override get supportsMultipleConnections(): boolean {
|
||||
// Kysely's SQLite adapter installs a single-connection mutex. This facade
|
||||
// never opens a real connection, so direct execution should reject from
|
||||
// acquisition without leaving controlled transaction calls wedged.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user