diff --git a/rivetkit-typescript/packages/rivetkit/src/db/native-database.test.ts b/rivetkit-typescript/packages/rivetkit/src/db/native-database.test.ts new file mode 100644 index 0000000000..1415a1535d --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/src/db/native-database.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, test } from "vitest"; +import { wrapJsNativeDatabase, type JsNativeDatabaseLike } from "./native-database"; + +function createDatabase( + overrides: Partial = {}, +): JsNativeDatabaseLike { + return { + async exec() { + return { columns: [], rows: [] }; + }, + async query() { + return { columns: [], rows: [] }; + }, + async run() { + return { changes: 0 }; + }, + async close() {}, + ...overrides, + }; +} + +describe("wrapJsNativeDatabase", () => { + test("appends native sqlite kv errors to generic sqlite I/O failures", async () => { + const db = wrapJsNativeDatabase( + createDatabase({ + async run() { + throw new Error( + "failed to execute sqlite statement: disk I/O error", + ); + }, + takeLastKvError() { + return "envoy channel closed while writing sqlite page"; + }, + }), + ); + + await expect(db.run("INSERT INTO foo VALUES (1)")).rejects.toThrow( + "failed to execute sqlite statement: disk I/O error (native sqlite kv error: envoy channel closed while writing sqlite page)", + ); + }); + + test("does not attach native sqlite kv errors to unrelated sqlite failures", async () => { + const db = wrapJsNativeDatabase( + createDatabase({ + async run() { + throw new Error( + "failed to execute sqlite statement: no such table: foo", + ); + }, + takeLastKvError() { + return "envoy channel closed while writing sqlite page"; + }, + }), + ); + + await expect(db.run("INSERT INTO foo VALUES (1)")).rejects.toThrow( + "failed to execute sqlite statement: no such table: foo", + ); + }); +}); diff --git a/rivetkit-typescript/packages/rivetkit/src/db/native-database.ts b/rivetkit-typescript/packages/rivetkit/src/db/native-database.ts index cfd69ad08d..04603de464 100644 --- a/rivetkit-typescript/packages/rivetkit/src/db/native-database.ts +++ b/rivetkit-typescript/packages/rivetkit/src/db/native-database.ts @@ -26,9 +26,30 @@ export interface JsNativeDatabaseLike { exec(sql: string): Promise; query(sql: string, params?: NativeBindParam[] | null): Promise; run(sql: string, params?: NativeBindParam[] | null): Promise; + takeLastKvError?(): string | null; close(): Promise; } +function shouldAttachNativeKvError(message: string): boolean { + return /i\/o error|unable to open database file/i.test(message); +} + +function enrichNativeDatabaseError( + database: JsNativeDatabaseLike, + error: unknown, +): never { + const kvError = database.takeLastKvError?.(); + if ( + error instanceof Error && + kvError && + shouldAttachNativeKvError(error.message) && + !error.message.includes(kvError) + ) { + error.message = `${error.message} (native sqlite kv error: ${kvError})`; + } + throw error; +} + function toNativeBinding(arg: unknown): NativeBindParam { if (arg === null || arg === undefined) { return { kind: "null" }; @@ -126,7 +147,12 @@ export function wrapJsNativeDatabase( sql: string, callback?: (row: unknown[], columns: string[]) => void, ): Promise { - const result = await database.exec(sql); + let result: NativeExecResult; + try { + result = await database.exec(sql); + } catch (error) { + enrichNativeDatabaseError(database, error); + } if (!callback) { return; } @@ -135,10 +161,18 @@ export function wrapJsNativeDatabase( } }, async run(sql: string, params?: SqliteBindings): Promise { - await database.run(sql, toNativeBindings(sql, params)); + try { + await database.run(sql, toNativeBindings(sql, params)); + } catch (error) { + enrichNativeDatabaseError(database, error); + } }, async query(sql: string, params?: SqliteBindings) { - return await database.query(sql, toNativeBindings(sql, params)); + try { + return await database.query(sql, toNativeBindings(sql, params)); + } catch (error) { + enrichNativeDatabaseError(database, error); + } }, async close(): Promise { await database.close();