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
6 changes: 6 additions & 0 deletions packages/libsql-client/src/__tests__/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2083,6 +2083,9 @@ describe("timeout option (local files)", () => {
throw new Error("Expected PRIMARY KEY constraint error");
} catch (e: any) {
expect(e.code).toBe("SQLITE_CONSTRAINT");
// rawCode is now populated for remote errors too (gh-117).
expect(e.rawCode).toBeDefined();
expect(e.rawCode & 0xff).toBe(19);
if (e.extendedCode !== undefined) {
expect(e.extendedCode).toBe(
"SQLITE_CONSTRAINT_PRIMARYKEY",
Expand Down Expand Up @@ -2110,6 +2113,9 @@ describe("timeout option (local files)", () => {
throw new Error("Expected UNIQUE constraint error");
} catch (e: any) {
expect(e.code).toBe("SQLITE_CONSTRAINT");
// rawCode is now populated for remote errors too (gh-117).
expect(e.rawCode).toBeDefined();
expect(e.rawCode & 0xff).toBe(19);
if (e.extendedCode !== undefined) {
expect(e.extendedCode).toBe("SQLITE_CONSTRAINT_UNIQUE");
}
Expand Down
47 changes: 47 additions & 0 deletions packages/libsql-client/src/__tests__/sqlite_error_codes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { expect } from "@jest/globals";

import { mapToBaseCode, mapToRawCode } from "../sqlite_error_codes.js";

describe("mapToBaseCode()", () => {
test("maps a primary result code", () => {
expect(mapToBaseCode(1)).toBe("SQLITE_ERROR");
expect(mapToBaseCode(19)).toBe("SQLITE_CONSTRAINT");
});

test("strips the extended bits", () => {
// SQLITE_CONSTRAINT_PRIMARYKEY = 19 | (6 << 8) = 1555
expect(mapToBaseCode(1555)).toBe("SQLITE_CONSTRAINT");
});

test("falls back for unknown or missing codes", () => {
expect(mapToBaseCode(undefined)).toBe("SQLITE_UNKNOWN");
expect(mapToBaseCode(99)).toBe("SQLITE_UNKNOWN_99");
});
});

describe("mapToRawCode()", () => {
test("maps a base SQLITE_* code back to its number", () => {
expect(mapToRawCode("SQLITE_ERROR")).toBe(1);
expect(mapToRawCode("SQLITE_CONSTRAINT")).toBe(19);
expect(mapToRawCode("SQLITE_BUSY")).toBe(5);
});

test("resolves an extended code to its base number", () => {
expect(mapToRawCode("SQLITE_CONSTRAINT_PRIMARYKEY")).toBe(19);
expect(mapToRawCode("SQLITE_CONSTRAINT_UNIQUE")).toBe(19);
expect(mapToRawCode("SQLITE_IOERR_WRITE")).toBe(10);
});

test("returns undefined for non-SQLite and missing codes", () => {
expect(mapToRawCode(undefined)).toBeUndefined();
expect(mapToRawCode("HRANA_PROTO_ERROR")).toBeUndefined();
expect(mapToRawCode("SERVER_ERROR")).toBeUndefined();
expect(mapToRawCode("SQLITE_NOT_A_REAL_CODE")).toBeUndefined();
});

test("round-trips with mapToBaseCode", () => {
for (const raw of [1, 5, 10, 19, 25, 28]) {
expect(mapToRawCode(mapToBaseCode(raw))).toBe(raw);
}
});
});
13 changes: 11 additions & 2 deletions packages/libsql-client/src/hrana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import type {
InArgs,
} from "@libsql/core/api";
import { LibsqlError, LibsqlBatchError } from "@libsql/core/api";
import type { SqlCache } from "./sql_cache.js";
import { transactionModeToBegin, ResultSetImpl } from "@libsql/core/util";
import type { SqlCache } from "./sql_cache.js";
import { mapToRawCode } from "./sqlite_error_codes.js";

export abstract class HranaTransaction implements Transaction {
#mode: TransactionMode;
Expand Down Expand Up @@ -402,8 +403,16 @@ export function resultSetFromHrana(hranaRows: hrana.RowsResult): ResultSet {
export function mapHranaError(e: unknown): unknown {
if (e instanceof hrana.ClientError) {
const code = mapHranaErrorCode(e);
// The SQL-over-HTTP protocol only carries the string error code, so
// derive the raw numeric code from it when the code is a SQLITE_* one.
// TODO: Parse extendedCode once the SQL over HTTP protocol supports it
return new LibsqlError(e.message, code, undefined, undefined, e);
return new LibsqlError(
e.message,
code,
undefined,
mapToRawCode(code),
e,
);
}
return e;
}
Expand Down
44 changes: 1 addition & 43 deletions packages/libsql-client/src/sqlite3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
transactionModeToBegin,
ResultSetImpl,
} from "@libsql/core/util";
import { mapToBaseCode } from "./sqlite_error_codes.js";

export * from "@libsql/core/api";

Expand Down Expand Up @@ -824,46 +825,3 @@ function mapSqliteError(e: unknown): unknown {
}
return e;
}

// Map SQLite raw error code to base error code string.
// Extended error codes are (base | (extended << 8)), so base = rawCode & 0xFF
function mapToBaseCode(rawCode: number | undefined): string {
if (rawCode === undefined) {
return "SQLITE_UNKNOWN";
}
const baseCode = rawCode & 0xff;
return (
sqliteErrorCodes[baseCode] ?? `SQLITE_UNKNOWN_${baseCode.toString()}`
);
}

const sqliteErrorCodes: Record<number, string> = {
1: "SQLITE_ERROR",
2: "SQLITE_INTERNAL",
3: "SQLITE_PERM",
4: "SQLITE_ABORT",
5: "SQLITE_BUSY",
6: "SQLITE_LOCKED",
7: "SQLITE_NOMEM",
8: "SQLITE_READONLY",
9: "SQLITE_INTERRUPT",
10: "SQLITE_IOERR",
11: "SQLITE_CORRUPT",
12: "SQLITE_NOTFOUND",
13: "SQLITE_FULL",
14: "SQLITE_CANTOPEN",
15: "SQLITE_PROTOCOL",
16: "SQLITE_EMPTY",
17: "SQLITE_SCHEMA",
18: "SQLITE_TOOBIG",
19: "SQLITE_CONSTRAINT",
20: "SQLITE_MISMATCH",
21: "SQLITE_MISUSE",
22: "SQLITE_NOLFS",
23: "SQLITE_AUTH",
24: "SQLITE_FORMAT",
25: "SQLITE_RANGE",
26: "SQLITE_NOTADB",
27: "SQLITE_NOTICE",
28: "SQLITE_WARNING",
};
70 changes: 70 additions & 0 deletions packages/libsql-client/src/sqlite_error_codes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SQLite primary result codes. See https://www.sqlite.org/rescode.html
// Extended codes are (base | (extended << 8)), so base = rawCode & 0xFF.
export const sqliteErrorCodes: Record<number, string> = {
1: "SQLITE_ERROR",
2: "SQLITE_INTERNAL",
3: "SQLITE_PERM",
4: "SQLITE_ABORT",
5: "SQLITE_BUSY",
6: "SQLITE_LOCKED",
7: "SQLITE_NOMEM",
8: "SQLITE_READONLY",
9: "SQLITE_INTERRUPT",
10: "SQLITE_IOERR",
11: "SQLITE_CORRUPT",
12: "SQLITE_NOTFOUND",
13: "SQLITE_FULL",
14: "SQLITE_CANTOPEN",
15: "SQLITE_PROTOCOL",
16: "SQLITE_EMPTY",
17: "SQLITE_SCHEMA",
18: "SQLITE_TOOBIG",
19: "SQLITE_CONSTRAINT",
20: "SQLITE_MISMATCH",
21: "SQLITE_MISUSE",
22: "SQLITE_NOLFS",
23: "SQLITE_AUTH",
24: "SQLITE_FORMAT",
25: "SQLITE_RANGE",
26: "SQLITE_NOTADB",
27: "SQLITE_NOTICE",
28: "SQLITE_WARNING",
};

const rawCodeByName: Record<string, number> = Object.fromEntries(
Object.entries(sqliteErrorCodes).map(([raw, name]) => [name, Number(raw)]),
);

// Map a SQLite raw numeric error code to its base error code string.
export function mapToBaseCode(rawCode: number | undefined): string {
if (rawCode === undefined) {
return "SQLITE_UNKNOWN";
}
const baseCode = rawCode & 0xff;
return (
sqliteErrorCodes[baseCode] ?? `SQLITE_UNKNOWN_${baseCode.toString()}`
);
}

// Map a SQLite error code string back to its base raw numeric code. Accepts
// extended names (e.g. "SQLITE_CONSTRAINT_PRIMARYKEY") and resolves them to the
// base code (19). Returns undefined for anything not recognised, including the
// client's own non-SQLite codes (e.g. "HRANA_PROTO_ERROR").
export function mapToRawCode(code: string | undefined): number | undefined {
if (code === undefined) {
return undefined;
}
let name = code;
while (name.length > "SQLITE_".length) {
const rawCode = rawCodeByName[name];
if (rawCode !== undefined) {
return rawCode;
}
const lastUnderscore = name.lastIndexOf("_");
if (lastUnderscore <= "SQLITE".length) {
break;
}
name = name.slice(0, lastUnderscore);
}
return undefined;
}