Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, test } from "vitest";
import { wrapJsNativeDatabase, type JsNativeDatabaseLike } from "./native-database";

function createDatabase(
overrides: Partial<JsNativeDatabaseLike> = {},
): 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",
);
});
});
40 changes: 37 additions & 3 deletions rivetkit-typescript/packages/rivetkit/src/db/native-database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,30 @@ export interface JsNativeDatabaseLike {
exec(sql: string): Promise<NativeExecResult>;
query(sql: string, params?: NativeBindParam[] | null): Promise<NativeQueryResult>;
run(sql: string, params?: NativeBindParam[] | null): Promise<NativeRunResult>;
takeLastKvError?(): string | null;
close(): Promise<void>;
}

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" };
Expand Down Expand Up @@ -126,7 +147,12 @@ export function wrapJsNativeDatabase(
sql: string,
callback?: (row: unknown[], columns: string[]) => void,
): Promise<void> {
const result = await database.exec(sql);
let result: NativeExecResult;
try {
result = await database.exec(sql);
} catch (error) {
enrichNativeDatabaseError(database, error);
}
if (!callback) {
return;
}
Expand All @@ -135,10 +161,18 @@ export function wrapJsNativeDatabase(
}
},
async run(sql: string, params?: SqliteBindings): Promise<void> {
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<void> {
await database.close();
Expand Down
Loading