diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 00000000..5d68ead5
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "semi": true,
+ "printWidth": 130,
+ "tabWidth": 4
+}
diff --git a/package-lock.json b/package-lock.json
index cfd0679b..f090f5ef 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
"@types/better-sqlite3": "^7.6.3",
"@types/jest": "^29.2.5",
"@types/node": "^18.15.5",
+ "bun-types": "^0.8.1",
"jest": "^29.3.1",
"ts-jest": "^29.0.5",
"typedoc": "^0.23.28",
@@ -1466,6 +1467,12 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
+ "node_modules/bun-types": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-0.8.1.tgz",
+ "integrity": "sha512-VuCBox66P/3a8gVOffLCWIS6vdpXq4y3eJuF3VnsyC5HpykmIjkcr5wYDn22qQdeTUmOfCcBy1SZmtrZCeUr3A==",
+ "dev": true
+ },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
diff --git a/package.json b/package.json
index f81158f9..400c6942 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,6 @@
"Jan Plhak "
],
"license": "MIT",
-
"type": "module",
"main": "lib-cjs/node.js",
"types": "lib-esm/node.d.ts",
@@ -33,6 +32,7 @@
"deno": "./lib-esm/web.js",
"edge-light": "./lib-esm/web.js",
"netlify": "./lib-esm/web.js",
+ "bun": "./lib-esm/bun.js",
"node": "./lib-esm/node.js",
"default": "./lib-esm/node.js"
},
@@ -62,22 +62,47 @@
"types": "./lib-esm/web.d.ts",
"import": "./lib-esm/web.js",
"require": "./lib-cjs/web.js"
+ },
+ "./bun": {
+ "types": "./lib-esm/bun.d.ts",
+ "import": "./lib-esm/bun.js",
+ "require": "./lib-cjs/bun.js"
+ },
+ "./bun-sqlite": {
+ "types": "./lib-esm/bun_sqlite.d.ts",
+ "import": "./lib-esm/bun_sqlite.js",
+ "require": "./lib-cjs/bun_sqlite.js"
}
},
"typesVersions": {
"*": {
- ".": ["./lib-esm/node.d.ts"],
- "http": ["./lib-esm/http.d.ts"],
- "hrana": ["./lib-esm/hrana.d.ts"],
- "sqlite3": ["./lib-esm/sqlite3.d.ts"],
- "web": ["./lib-esm/web.d.ts"]
+ ".": [
+ "./lib-esm/node.d.ts"
+ ],
+ "http": [
+ "./lib-esm/http.d.ts"
+ ],
+ "hrana": [
+ "./lib-esm/hrana.d.ts"
+ ],
+ "sqlite3": [
+ "./lib-esm/sqlite3.d.ts"
+ ],
+ "web": [
+ "./lib-esm/web.d.ts"
+ ],
+ "bun": [
+ "./lib-esm/bun.d.ts"
+ ],
+ "bun-sqlite": [
+ "./lib-esm/bun_sqlite.d.ts"
+ ]
}
},
"files": [
"lib-cjs/**",
"lib-esm/**"
],
-
"scripts": {
"prepublishOnly": "npm run build",
"prebuild": "rm -rf ./lib-cjs ./lib-esm",
@@ -86,10 +111,10 @@
"build:esm": "tsc -p tsconfig.build-esm.json",
"postbuild": "cp package-cjs.json ./lib-cjs/package.json",
"test": "jest --runInBand",
+ "test:bun": "bun test bun",
"typecheck": "tsc --noEmit",
"typedoc": "rm -rf ./docs && typedoc"
},
-
"dependencies": {
"@libsql/hrana-client": "^0.5.0-pre.2",
"better-sqlite3": "^8.0.1",
@@ -99,6 +124,7 @@
"@types/better-sqlite3": "^7.6.3",
"@types/jest": "^29.2.5",
"@types/node": "^18.15.5",
+ "bun-types": "^0.8.1",
"jest": "^29.3.1",
"ts-jest": "^29.0.5",
"typedoc": "^0.23.28",
diff --git a/src/__tests__/client.test.ts b/src/__tests__/client.test.ts
index d73d793b..0383f924 100644
--- a/src/__tests__/client.test.ts
+++ b/src/__tests__/client.test.ts
@@ -1,6 +1,4 @@
-import console from "node:console";
import { expect } from "@jest/globals";
-import type { MatcherFunction } from "expect";
import type { Request, Response } from "@libsql/hrana-client";
import { fetch } from "@libsql/hrana-client";
@@ -449,7 +447,7 @@ describe("batch()", () => {
], "write");
const n = 100;
- const promises = [];
+ const promises: Promise[] = [];
for (let i = 0; i < n; ++i) {
const ii = i;
promises.push((async () => {
@@ -495,7 +493,7 @@ describe("batch()", () => {
}));
test("batch with a lot of different statements", withClient(async (c) => {
- const stmts = [];
+ const stmts: string[] = [];
for (let i = 0; i < 1000; ++i) {
stmts.push(`SELECT ${i}`);
}
@@ -509,7 +507,7 @@ describe("batch()", () => {
const n = 20;
const m = 200;
- const stmts = [];
+ const stmts: libsql.InStatement[] = [];
for (let i = 0; i < n; ++i) {
for (let j = 0; j < m; ++j) {
stmts.push({sql: `SELECT ?, ${j}`, args: [i]});
diff --git a/src/__tests__/helpers.ts b/src/__tests__/helpers.ts
index c48d0192..f25f5689 100644
--- a/src/__tests__/helpers.ts
+++ b/src/__tests__/helpers.ts
@@ -10,7 +10,7 @@ const toBeLibsqlError: MatcherFunction<[code?: string, message?: RegExp]> =
&& (messageRe === undefined || actual.message.match(messageRe) !== null);
const message = (): string => {
- const parts = [];
+ const parts: string[] = [];
parts.push("expected ");
parts.push(this.utils.printReceived(actual));
parts.push(pass ? " not to be " : " to be ");
diff --git a/src/bun.ts b/src/bun.ts
new file mode 100644
index 00000000..cd5f1513
--- /dev/null
+++ b/src/bun.ts
@@ -0,0 +1,38 @@
+import type { Config, Client } from "./api.js";
+import { LibsqlError } from "./api.js";
+import type { ExpandedConfig } from "./config.js";
+import { expandConfig } from "./config.js";
+import { supportedUrlLink } from "./util.js";
+
+import { _createClient as _createWsClient } from "./ws.js";
+import { _createClient as _createHttpClient } from "./http.js";
+import { _createClient as _createBunSqliteClient } from "./bun_sqlite.js";
+
+export * from "./api.js";
+
+export function createClient(config: Config): Client {
+ return _createClient(expandConfig(config, true));
+}
+
+export const isBun = () => {
+ if ((globalThis as any).Bun || (globalThis as any).process?.versions?.bun) return;
+ throw new LibsqlError("Bun is not available", "BUN_NOT_AVAILABLE");
+};
+
+/** @private */
+export function _createClient(config: ExpandedConfig): Client {
+ isBun();
+ if (config.scheme === "ws" || config.scheme === "wss") {
+ return _createWsClient(config);
+ } else if (config.scheme === "http" || config.scheme === "https") {
+ return _createHttpClient(config);
+ } else if (config.scheme === "file") {
+ return _createBunSqliteClient(config);
+ } else {
+ throw new LibsqlError(
+ 'The Bun client supports "file", "libsql:", "wss:", "ws:", "https:" and "http:" URLs, ' +
+ `got ${JSON.stringify(config.scheme + ":")}. For more information, please read ${supportedUrlLink}`,
+ "URL_SCHEME_NOT_SUPPORTED"
+ );
+ }
+}
diff --git a/src/bun_sqlite.ts b/src/bun_sqlite.ts
new file mode 100644
index 00000000..8489893b
--- /dev/null
+++ b/src/bun_sqlite.ts
@@ -0,0 +1,288 @@
+// @ts-ignore bun:sqlite is not typed when building
+import { Database, SQLQueryBindings } from "bun:sqlite";
+import type {
+ Config,
+ IntMode,
+ Client,
+ Transaction,
+ TransactionMode,
+ ResultSet,
+ Row,
+ Value,
+ InValue,
+ InStatement,
+} from "./api.js";
+import { LibsqlError } from "./api.js";
+import { expandConfig, type ExpandedConfig } from "./config.js";
+import {
+ transactionModeToBegin,
+ ResultSetImpl,
+ validateFileConfig,
+ parseStatement,
+ minSafeBigint,
+ maxSafeBigint,
+ minInteger,
+ maxInteger,
+} from "./util.js";
+import { isBun } from "./bun.js";
+
+export * from "./api.js";
+
+type ConstructorParameters = T extends new (...args: infer P) => any ? P : never;
+type DatabaseOptions = ConstructorParameters[1];
+
+export function createClient(_config: Config): Client {
+ isBun();
+ const config = validateFileConfig(expandConfig(_config, true));
+ //@note bun bigint handling https://github.com/oven-sh/bun/issues/1536
+ if (config.intMode !== "number") throw intModeNotImplemented(config.intMode);
+ return _createClient(config);
+}
+
+/** @private */
+export function _createClient(config: ExpandedConfig): Client {
+ const path = config.path;
+ const options = undefined; //@todo implement options
+ const db = new Database(path);
+ try {
+ executeStmt(db, "SELECT 1 AS checkThatTheDatabaseCanBeOpened", config.intMode);
+ } finally {
+ db.close();
+ }
+
+ return new BunSqliteClient(path, options, config.intMode);
+}
+
+export class BunSqliteClient implements Client {
+ #path: string;
+ #options: DatabaseOptions;
+ #intMode: IntMode;
+ closed: boolean;
+ protocol: "file";
+
+ /** @private */
+ constructor(path: string, options: DatabaseOptions, intMode: IntMode) {
+ this.#path = path;
+ this.#options = options;
+ this.#intMode = intMode;
+ this.closed = false;
+ this.protocol = "file";
+ }
+
+ async execute(stmt: InStatement): Promise {
+ this.#checkNotClosed();
+ const db = new Database(this.#path, this.#options);
+ try {
+ return executeStmt(db, stmt, this.#intMode);
+ } finally {
+ db.close();
+ }
+ }
+
+ async batch(stmts: Array, mode: TransactionMode = "deferred"): Promise> {
+ this.#checkNotClosed();
+ const db = new Database(this.#path, this.#options);
+ try {
+ executeStmt(db, transactionModeToBegin(mode), this.#intMode);
+ const resultSets = stmts.map((stmt) => {
+ if (!db.inTransaction) {
+ throw new LibsqlError("The transaction has been rolled back", "TRANSACTION_CLOSED");
+ }
+ return executeStmt(db, stmt, this.#intMode);
+ });
+ executeStmt(db, "COMMIT", this.#intMode);
+ return resultSets;
+ } finally {
+ db.close();
+ }
+ }
+
+ async transaction(mode: TransactionMode = "write"): Promise {
+ this.#checkNotClosed();
+ const db = new Database(this.#path, this.#options);
+ try {
+ executeStmt(db, transactionModeToBegin(mode), this.#intMode);
+ return new BunSqliteTransaction(db, this.#intMode);
+ } catch (e) {
+ db.close();
+ throw e;
+ }
+ }
+
+ async executeMultiple(sql: string): Promise {
+ throw executeMultipleNotImplemented();
+ }
+
+ close(): void {
+ this.closed = true;
+ }
+
+ #checkNotClosed(): void {
+ if (this.closed) {
+ throw new LibsqlError("The client is closed", "CLIENT_CLOSED");
+ }
+ }
+}
+
+export class BunSqliteTransaction implements Transaction {
+ #database: Database;
+ #intMode: IntMode;
+ #closed: boolean;
+
+ /** @private */
+ constructor(database: Database, intMode: IntMode) {
+ this.#database = database;
+ this.#intMode = intMode;
+ this.#closed = false;
+ }
+
+ async execute(stmt: InStatement): Promise {
+ this.#checkNotClosed();
+ return executeStmt(this.#database, stmt, this.#intMode);
+ }
+
+ async batch(stmts: Array): Promise> {
+ return stmts.map((stmt) => {
+ this.#checkNotClosed();
+ return executeStmt(this.#database, stmt, this.#intMode);
+ });
+ }
+
+ async executeMultiple(sql: string): Promise {
+ throw executeMultipleNotImplemented();
+ }
+
+ async rollback(): Promise {
+ if (this.#closed) {
+ return;
+ }
+ this.#checkNotClosed();
+ executeStmt(this.#database, "ROLLBACK", this.#intMode);
+ this.close();
+ }
+
+ async commit(): Promise {
+ this.#checkNotClosed();
+ executeStmt(this.#database, "COMMIT", this.#intMode);
+ this.close();
+ }
+
+ close(): void {
+ this.#database.close();
+ this.#closed = true;
+ }
+
+ get closed(): boolean {
+ return this.#closed;
+ }
+
+ #checkNotClosed(): void {
+ if (this.#closed || !this.#database.inTransaction) {
+ throw new LibsqlError("The transaction is closed", "TRANSACTION_CLOSED");
+ }
+ }
+}
+
+function executeStmt(db: Database, stmt: InStatement, intMode: IntMode): ResultSet {
+ const { sql, args } = parseStatement(stmt, valueToSql);
+ try {
+ const sqlStmt = db.prepare(sql);
+ const data = sqlStmt.all(args as SQLQueryBindings) as Record[];
+ sqlStmt.finalize();
+ if (Array.isArray(data) && data.length > 0) {
+ const columns = sqlStmt.columnNames;
+ const rows = convertSqlResultToRows(data, intMode);
+ //@note info about the last insert rowid is not available with bun:sqlite
+ const rowsAffected = 0;
+ const lastInsertRowid = undefined;
+ return new ResultSetImpl(columns, rows, rowsAffected, lastInsertRowid);
+ } else {
+ const rowsAffected = typeof data === "number" ? data : 0;
+ const lastInsertRowid = BigInt(0);
+ return new ResultSetImpl([], [], rowsAffected, lastInsertRowid);
+ }
+ } catch (e) {
+ throw mapSqliteError(e);
+ }
+}
+
+function convertSqlResultToRows(results: Record[], intMode: IntMode): Row[] {
+ return results.map((result) => {
+ const entries = Object.entries(result);
+ const row: Partial = {};
+
+ //We use Object.defineProperty to make the properties non-enumerable
+ entries.forEach(([name, v], index) => {
+ const value = valueFromSql(v, intMode);
+ Object.defineProperty(row, name, { value, enumerable: true, configurable: true });
+ Object.defineProperty(row, index, { value, configurable: true });
+ });
+
+ Object.defineProperty(row, "length", { value: entries.length, configurable: true });
+
+ return row as Row;
+ });
+}
+
+function valueFromSql(sqlValue: unknown, intMode: IntMode): Value {
+ // https://github.com/oven-sh/bun/issues/1536
+ if (typeof sqlValue === "bigint") {
+ if (intMode === "number") {
+ if (sqlValue < minSafeBigint || sqlValue > maxSafeBigint) {
+ throw new RangeError("Received integer which cannot be safely represented as a JavaScript number");
+ }
+ return Number(sqlValue);
+ } else if (intMode === "bigint") {
+ return sqlValue;
+ } else if (intMode === "string") {
+ return String(sqlValue);
+ } else {
+ throw new Error("Invalid value for IntMode");
+ }
+ } else if (ArrayBuffer.isView(sqlValue)) {
+ return sqlValue.buffer as Value;
+ }
+ return sqlValue as Value;
+}
+
+function valueToSql(value: InValue) {
+ if (typeof value === "number") {
+ if (!Number.isFinite(value)) {
+ throw new RangeError("Only finite numbers (not Infinity or NaN) can be passed as arguments");
+ }
+ return value;
+ } else if (typeof value === "bigint") {
+ if (value < minInteger || value > maxInteger) {
+ throw new RangeError("bigint is too large to be represented as a 64-bit integer and passed as argument");
+ }
+ return value;
+ } else if (typeof value === "boolean") {
+ return value;
+ } else if (value instanceof ArrayBuffer) {
+ return Buffer.from(value);
+ } else if (value instanceof Date) {
+ return value.valueOf();
+ } else if (value === undefined) {
+ throw new TypeError("undefined cannot be passed as argument to the database");
+ } else {
+ return value;
+ }
+}
+
+const BUN_SQLITE_ERROR = "BUN_SQLITE ERROR" as const;
+
+function mapSqliteError(e: unknown): unknown {
+ if (e instanceof RangeError) {
+ return e;
+ }
+ if (e instanceof Error) {
+ return new LibsqlError(e.message, BUN_SQLITE_ERROR, e);
+ }
+ return e;
+}
+
+const executeMultipleNotImplemented = () =>
+ new LibsqlError("bun:sqlite doesn't support executeMultiple. Use batch instead.", BUN_SQLITE_ERROR);
+
+const intModeNotImplemented = (mode: string) =>
+ new LibsqlError(`"${mode}" intMode is not supported by bun:sqlite. Only intMode "number" is supported.`, BUN_SQLITE_ERROR);
diff --git a/src/bun_tests/bun.client.test.ts b/src/bun_tests/bun.client.test.ts
new file mode 100644
index 00000000..fe7ba243
--- /dev/null
+++ b/src/bun_tests/bun.client.test.ts
@@ -0,0 +1,1038 @@
+import { describe, test, expect } from "bun:test";
+
+import type { Request, Response } from "@libsql/hrana-client";
+import { fetch } from "@libsql/hrana-client";
+
+import type * as libsql from "../bun";
+import { createClient } from "../bun";
+import { expectBunSqliteError, expectLibSqlError, withPattern } from "./bun.helpers";
+
+const config = {
+ url: process.env.URL ?? "file:///tmp/test.db" ?? "ws://localhost:8080",
+ authToken: process.env.AUTH_TOKEN,
+};
+
+function withClient(f: (c: libsql.Client) => Promise, extraConfig?: Partial): () => Promise {
+ return async () => {
+ const c = createClient({ ...config, ...extraConfig });
+ try {
+ await f(c);
+ } finally {
+ c.close();
+ }
+ };
+}
+
+describe("createClient()", () => {
+ test("URL scheme not supported", () => {
+ expectLibSqlError(() => createClient({ url: "ftp://localhost" }), withPattern("URL_SCHEME_NOT_SUPPORTED", /"ftp:"/));
+ });
+
+ test("URL param not supported", () => {
+ expectLibSqlError(() => createClient({ url: "ws://localhost?foo=bar" }), withPattern("URL_PARAM_NOT_SUPPORTED", /"foo"/));
+ });
+
+ test("URL scheme incompatible with ?tls", () => {
+ const urls = ["ws://localhost?tls=1", "wss://localhost?tls=0", "http://localhost?tls=1", "https://localhost?tls=0"];
+ for (const url of urls) {
+ expectLibSqlError(() => createClient({ url }), withPattern("URL_INVALID", /TLS/));
+ }
+ });
+
+ test("missing port in libsql URL with tls=0", () => {
+ expectLibSqlError(() => createClient({ url: "libsql://localhost?tls=0" }), withPattern("URL_INVALID", /port/));
+ });
+
+ test("invalid value of tls query param", () => {
+ expectLibSqlError(() => createClient({ url: "libsql://localhost?tls=yes" }), withPattern("URL_INVALID", /"tls".*"yes"/));
+ });
+
+ test("passing URL instead of config object", () => {
+ // @ts-expect-error
+ expect(() => createClient("ws://localhost").toThrow(/as object, got string/));
+ });
+
+ test("invalid value for `intMode`", () => {
+ // @ts-expect-error
+ expect(() => createClient({ ...config, intMode: "foo" }).toThrow(/"foo"/));
+ });
+});
+
+describe("execute()", () => {
+ test(
+ "query a single value",
+ withClient(async (c) => {
+ const rs = await c.execute("SELECT 42");
+ expect(rs.columns.length).toStrictEqual(1);
+ expect(rs.rows.length).toStrictEqual(1);
+ expect(rs.rows[0].length).toStrictEqual(1);
+ expect(rs.rows[0][0]).toStrictEqual(42);
+ })
+ );
+
+ test(
+ "query a single row",
+ withClient(async (c) => {
+ const rs = await c.execute("SELECT 1 AS one, 'two' AS two, 0.5 AS three");
+ expect(rs.columns).toStrictEqual(["one", "two", "three"]);
+ expect(rs.rows.length).toStrictEqual(1);
+
+ const r = rs.rows[0];
+ expect(r.length).toStrictEqual(3);
+ expect(Array.from(r)).toStrictEqual([1, "two", 0.5]);
+ expect(Object.entries(r)).toStrictEqual([
+ ["one", 1],
+ ["two", "two"],
+ ["three", 0.5],
+ ]);
+ })
+ );
+
+ test(
+ "query multiple rows",
+ withClient(async (c) => {
+ const rs = await c.execute("VALUES (1, 'one'), (2, 'two'), (3, 'three')");
+ expect(rs.columns.length).toStrictEqual(2);
+ expect(rs.rows.length).toStrictEqual(3);
+
+ expect(Array.from(rs.rows[0])).toStrictEqual([1, "one"]);
+ expect(Array.from(rs.rows[1])).toStrictEqual([2, "two"]);
+ expect(Array.from(rs.rows[2])).toStrictEqual([3, "three"]);
+ })
+ );
+
+ test(
+ "statement that produces error",
+ withClient(async (c) => {
+ await expectBunSqliteError(() => c.execute("SELECT foobar"));
+ })
+ );
+
+ test(
+ "rowsAffected with INSERT",
+ withClient(async (c) => {
+ await c.batch(["DROP TABLE IF EXISTS t", "CREATE TABLE t (a)"], "write");
+ const rs = await c.execute("INSERT INTO t VALUES (1), (2)");
+ expect(rs.rowsAffected).toStrictEqual(2);
+ })
+ );
+
+ test(
+ "rowsAffected with DELETE",
+ withClient(async (c) => {
+ await c.batch(
+ ["DROP TABLE IF EXISTS t", "CREATE TABLE t (a)", "INSERT INTO t VALUES (1), (2), (3), (4), (5)"],
+ "write"
+ );
+ const rs = await c.execute("DELETE FROM t WHERE a >= 3");
+ expect(rs.rowsAffected).toStrictEqual(3);
+ })
+ );
+
+ //@note lastInsertRowId is not implemented with bun
+ test.skip(
+ "lastInsertRowid with INSERT",
+ withClient(async (c) => {
+ await c.batch(["DROP TABLE IF EXISTS t", "CREATE TABLE t (a)", "INSERT INTO t VALUES ('one'), ('two')"], "write");
+ const insertRs = await c.execute("INSERT INTO t VALUES ('three')");
+ expect(insertRs.lastInsertRowid).not.toBeUndefined();
+ const selectRs = await c.execute({
+ sql: "SELECT a FROM t WHERE ROWID = ?",
+ args: [insertRs.lastInsertRowid!],
+ });
+ expect(Array.from(selectRs.rows[0])).toStrictEqual(["three"]);
+ })
+ );
+
+ test(
+ "rows from INSERT RETURNING",
+ withClient(async (c) => {
+ await c.batch(["DROP TABLE IF EXISTS t", "CREATE TABLE t (a)"], "write");
+
+ const rs = await c.execute("INSERT INTO t VALUES (1) RETURNING 42 AS x, 'foo' AS y");
+ expect(rs.columns).toStrictEqual(["x", "y"]);
+ expect(rs.rows.length).toStrictEqual(1);
+ expect(Array.from(rs.rows[0])).toStrictEqual([42, "foo"]);
+ })
+ );
+
+ test(
+ "rowsAffected with WITH INSERT",
+ withClient(async (c) => {
+ await c.batch(["DROP TABLE IF EXISTS t", "CREATE TABLE t (a)", "INSERT INTO t VALUES (1), (2), (3)"], "write");
+
+ const rs = await c.execute(`
+ WITH x(a) AS (SELECT 2*a FROM t)
+ INSERT INTO t SELECT a+1 FROM x
+ `);
+ expect(rs.rowsAffected).toStrictEqual(3);
+ })
+ );
+});
+
+describe("values", () => {
+ function testRoundtrip(name: string, passed: libsql.InValue, expected: libsql.Value, intMode?: libsql.IntMode): void {
+ test(
+ name,
+ withClient(
+ async (c) => {
+ const rs = await c.execute({ sql: "SELECT ?", args: [passed] });
+ expect(rs.rows[0][0]).toStrictEqual(expected);
+ },
+ { intMode }
+ )
+ );
+ }
+
+ function testDifference(name: string, passed: libsql.InValue, intMode?: libsql.IntMode): void {
+ test(
+ name,
+ withClient(
+ async (c) => {
+ const rs = await c.execute({ sql: "SELECT ?", args: [passed] });
+ expect(rs.rows[0][0]).not.toStrictEqual(passed);
+ },
+ { intMode }
+ )
+ );
+ }
+
+ function testRoundtripError(name: string, passed: libsql.InValue, expectedError: unknown, intMode?: libsql.IntMode): void {
+ test(
+ name,
+ withClient(
+ async (c) => {
+ await expect(
+ c.execute({
+ sql: "SELECT ?",
+ args: [passed],
+ })
+ ).rejects.toBeInstanceOf(expectedError);
+ },
+ { intMode }
+ )
+ );
+ }
+
+ testRoundtrip("string", "boomerang", "boomerang");
+ testRoundtrip("string with weird characters", "a\n\r\t ", "a\n\r\t ");
+ testRoundtrip("string with unicode", "žluťoučký kůň úpěl ďábelské ódy", "žluťoučký kůň úpěl ďábelské ódy");
+
+ testRoundtrip("zero number", 0, 0);
+ testRoundtrip("integer number", -2023, -2023);
+ testRoundtrip("float number", 12.345, 12.345);
+
+ describe("'number' int mode", () => {
+ testRoundtrip("zero integer", 0n, 0, "number");
+ testRoundtrip("small integer", -42n, -42, "number");
+ testRoundtrip("largest safe integer", 9007199254740991n, 9007199254740991, "number");
+ testDifference("smallest unsafe positive integer", 9007199254740992n, "number");
+ testDifference("large unsafe negative integer", -1152921504594532842n, "number");
+ });
+
+ //@note not implemented with bun:sqlite
+ describe.skip("'bigint' int mode", () => {
+ testRoundtrip("zero integer", 0n, 0n, "bigint");
+ testRoundtrip("small integer", -42n, -42n, "bigint");
+ testRoundtrip("large positive integer", 1152921504608088318n, 1152921504608088318n, "bigint");
+ testRoundtrip("large negative integer", -1152921504594532842n, -1152921504594532842n, "bigint");
+ testRoundtrip("largest positive integer", 9223372036854775807n, 9223372036854775807n, "bigint");
+ testRoundtrip("largest negative integer", -9223372036854775808n, -9223372036854775808n, "bigint");
+ });
+
+ //@note not implemented with bun:sqlite
+ describe.skip("'string' int mode", () => {
+ testRoundtrip("zero integer", 0n, "0", "string");
+ testRoundtrip("small integer", -42n, "-42", "string");
+ testRoundtrip("large positive integer", 1152921504608088318n, "1152921504608088318", "string");
+ testRoundtrip("large negative integer", -1152921504594532842n, "-1152921504594532842", "string");
+ testRoundtrip("largest positive integer", 9223372036854775807n, "9223372036854775807", "string");
+ testRoundtrip("largest negative integer", -9223372036854775808n, "-9223372036854775808", "string");
+ });
+
+ const buf = new ArrayBuffer(256);
+ const array = new Uint8Array(buf);
+ for (let i = 0; i < 256; ++i) {
+ array[i] = i ^ 0xab;
+ }
+ testRoundtrip("ArrayBuffer", buf, buf);
+ testRoundtrip("Uint8Array", array, buf);
+
+ testRoundtrip("null", null, null);
+ testRoundtrip("true", true, 1);
+ testRoundtrip("false", false, 0);
+
+ testRoundtrip("bigint", -1000n, -1000);
+ testRoundtrip("Date", new Date("2023-01-02T12:34:56Z"), 1672662896000);
+
+ //@ts-expect-error this tests for an error
+ testRoundtripError("undefined produces error", undefined, TypeError);
+ testRoundtripError("NaN produces error", NaN, RangeError);
+ testRoundtripError("Infinity produces error", Infinity, RangeError);
+ testRoundtripError("large bigint produces error", -1267650600228229401496703205376n, RangeError);
+
+ test(
+ "max 64-bit bigint",
+ withClient(async (c) => {
+ const rs = await c.execute({ sql: "SELECT ?||''", args: [9223372036854775807n] });
+ expect(rs.rows[0][0]).toStrictEqual("9223372036854775807");
+ })
+ );
+
+ test(
+ "min 64-bit bigint",
+ withClient(async (c) => {
+ const rs = await c.execute({ sql: "SELECT ?||''", args: [-9223372036854775808n] });
+ expect(rs.rows[0][0]).toStrictEqual("-9223372036854775808");
+ })
+ );
+});
+
+describe("ResultSet.toJSON()", () => {
+ test(
+ "simple result set",
+ withClient(async (c) => {
+ const rs = await c.execute("SELECT 1 AS a");
+ const json = rs.toJSON();
+ expect(json["lastInsertRowid"] === null || json["lastInsertRowid"] === "0").toBe(true);
+ expect(json["columns"]).toStrictEqual(["a"]);
+ expect(json["rows"]).toStrictEqual([[1]]);
+ expect(json["rowsAffected"]).toStrictEqual(0);
+
+ const str = JSON.stringify(rs);
+ expect(
+ str === '{"columns":["a"],"rows":[[1]],"rowsAffected":0,"lastInsertRowid":null}' ||
+ str === '{"columns":["a"],"rows":[[1]],"rowsAffected":0,"lastInsertRowid":"0"}'
+ ).toBe(true);
+ })
+ );
+
+ test(
+ "lastInsertRowid",
+ withClient(async (c) => {
+ await c.execute("DROP TABLE IF EXISTS t");
+ await c.execute("CREATE TABLE t (id INTEGER PRIMARY KEY NOT NULL)");
+ const rs = await c.execute("INSERT INTO t VALUES (12345)");
+ expect(rs.toJSON()).toStrictEqual({
+ columns: [],
+ rows: [],
+ rowsAffected: 1,
+ lastInsertRowid: "0", //@note not implemented with bun
+ });
+ })
+ );
+
+ test(
+ "row values",
+ withClient(async (c) => {
+ const rs = await c.execute("SELECT 42 AS integer, 0.5 AS float, NULL AS \"null\", 'foo' AS text, X'626172' AS blob");
+ const json = rs.toJSON();
+ expect(json["columns"]).toStrictEqual(["integer", "float", "null", "text", "blob"]);
+ expect(json["rows"]).toStrictEqual([[42, 0.5, null, "foo", "YmFy"]]);
+ })
+ );
+
+ //@note not implemented with bun:sqlite
+ test.skip(
+ "bigint row value",
+ withClient(
+ async (c) => {
+ const rs = await c.execute("SELECT 42");
+ const json = rs.toJSON();
+ expect(json["rows"]).toStrictEqual([["42"]]);
+ },
+ { intMode: "bigint" }
+ )
+ );
+});
+
+describe("arguments", () => {
+ test(
+ "? arguments",
+ withClient(async (c) => {
+ const rs = await c.execute({
+ sql: "SELECT ?1, ?2",
+ args: ["one", "two"],
+ });
+ expect(Array.from(rs.rows[0])).toStrictEqual(["one", "two"]);
+ })
+ );
+
+ test(
+ "?NNN arguments",
+ withClient(async (c) => {
+ const rs = await c.execute({
+ sql: "SELECT ?2, ?3, ?1",
+ args: ["one", "two", "three"],
+ });
+ expect(Array.from(rs.rows[0])).toStrictEqual(["two", "three", "one"]);
+ })
+ );
+
+ test(
+ "?NNN arguments with holes",
+ withClient(async (c) => {
+ const rs = await c.execute({
+ sql: "SELECT ?3, ?1",
+ args: ["one", "two", "three"],
+ });
+ expect(Array.from(rs.rows[0])).toStrictEqual(["three", "one"]);
+ })
+ );
+
+ //@note not supported by bun:sqlite
+ test(
+ "?NNN and ? arguments",
+ withClient(async (c) => {
+ const rs = await c.execute({
+ sql: "SELECT ?2, ?, ?3",
+ args: ["one", "two", "three"],
+ });
+ expect(Array.from(rs.rows[0])).toStrictEqual(["two", "three", "three"]);
+ })
+ );
+
+ for (const sign of [":", "@", "$"]) {
+ test(
+ `${sign}AAAA arguments`,
+ withClient(async (c) => {
+ const rs = await c.execute({
+ sql: `SELECT ${sign}b, ${sign}a`,
+ args: { [`${sign}a`]: "one", [`${sign}b`]: "two" },
+ });
+ expect(Array.from(rs.rows[0])).toStrictEqual(["two", "one"]);
+ })
+ );
+
+ test(
+ `${sign}AAAA arguments used multiple times`,
+ withClient(async (c) => {
+ const rs = await c.execute({
+ sql: `SELECT ${sign}b, ${sign}a, ${sign}b || ${sign}a`,
+ args: { [`${sign}a`]: "one", [`${sign}b`]: "two" },
+ });
+ expect(Array.from(rs.rows[0])).toStrictEqual(["two", "one", "twoone"]);
+ })
+ );
+
+ test(
+ `${sign}AAAA arguments and ?NNN arguments`,
+ withClient(async (c) => {
+ const rs = await c.execute({
+ sql: `SELECT ${sign}b, ${sign}a, ?1`,
+ args: { [`${sign}a`]: "one", [`${sign}b`]: "two" },
+ });
+ expect(Array.from(rs.rows[0])).toStrictEqual(["two", "one", "two"]);
+ })
+ );
+ }
+});
+
+describe("batch()", () => {
+ test(
+ "multiple queries",
+ withClient(async (c) => {
+ const rss = await c.batch(
+ [
+ "SELECT 1+1",
+ "SELECT 1 AS one, 2 AS two",
+ { sql: "SELECT ?", args: ["boomerang"] },
+ { sql: "VALUES (?), (?)", args: ["big", "ben"] },
+ ],
+ "read"
+ );
+
+ expect(rss.length).toStrictEqual(4);
+ const [rs0, rs1, rs2, rs3] = rss;
+
+ expect(rs0.rows.length).toStrictEqual(1);
+ expect(Array.from(rs0.rows[0])).toStrictEqual([2]);
+
+ expect(rs1.rows.length).toStrictEqual(1);
+ expect(Array.from(rs1.rows[0])).toStrictEqual([1, 2]);
+
+ expect(rs2.rows.length).toStrictEqual(1);
+ expect(Array.from(rs2.rows[0])).toStrictEqual(["boomerang"]);
+
+ expect(rs3.rows.length).toStrictEqual(2);
+ expect(Array.from(rs3.rows[0])).toStrictEqual(["big"]);
+ expect(Array.from(rs3.rows[1])).toStrictEqual(["ben"]);
+ })
+ );
+
+ test(
+ "statements are executed sequentially",
+ withClient(async (c) => {
+ const rss = await c.batch(
+ [
+ /* 0 */ "DROP TABLE IF EXISTS t",
+ /* 1 */ "CREATE TABLE t (a, b)",
+ /* 2 */ "INSERT INTO t VALUES (1, 'one')",
+ /* 3 */ "SELECT * FROM t ORDER BY a",
+ /* 4 */ "INSERT INTO t VALUES (2, 'two')",
+ /* 5 */ "SELECT * FROM t ORDER BY a",
+ /* 6 */ "DROP TABLE t",
+ ],
+ "write"
+ );
+
+ expect(rss.length).toStrictEqual(7);
+ expect(rss[3].rows).toEqual([{ a: 1, b: "one" }]);
+ expect(rss[5].rows).toEqual([
+ { a: 1, b: "one" },
+ { a: 2, b: "two" },
+ ]);
+ })
+ );
+
+ test(
+ "statements are executed in a transaction",
+ withClient(async (c) => {
+ await c.batch(
+ ["DROP TABLE IF EXISTS t1", "DROP TABLE IF EXISTS t2", "CREATE TABLE t1 (a)", "CREATE TABLE t2 (a)"],
+ "write"
+ );
+
+ const n = 100;
+ const promises: Promise[] = [];
+ for (let i = 0; i < n; ++i) {
+ const ii = i;
+ promises.push(
+ (async () => {
+ const rss = await c.batch(
+ [
+ { sql: "INSERT INTO t1 VALUES (?)", args: [ii] },
+ { sql: "INSERT INTO t2 VALUES (?)", args: [ii * 10] },
+ "SELECT SUM(a) FROM t1",
+ "SELECT SUM(a) FROM t2",
+ ],
+ "write"
+ );
+
+ const sum1 = rss[2].rows[0][0] as number;
+ const sum2 = rss[3].rows[0][0] as number;
+ expect(sum2).toStrictEqual(sum1 * 10);
+ })()
+ );
+ }
+ await Promise.all(promises);
+
+ const rs1 = await c.execute("SELECT SUM(a) FROM t1");
+ expect(rs1.rows[0][0]).toStrictEqual((n * (n - 1)) / 2);
+ const rs2 = await c.execute("SELECT SUM(a) FROM t2");
+ expect(rs2.rows[0][0]).toStrictEqual(((n * (n - 1)) / 2) * 10);
+ }),
+ 10000
+ );
+
+ test(
+ "error in batch",
+ withClient(async (c) => {
+ await expectBunSqliteError(() => c.batch(["SELECT 1+1", "SELECT foobar"], "read"));
+ })
+ );
+
+ test(
+ "error in batch rolls back transaction",
+ withClient(async (c) => {
+ await c.execute("DROP TABLE IF EXISTS t");
+ await c.execute("CREATE TABLE t (a)");
+ await c.execute("INSERT INTO t VALUES ('one')");
+ await expectBunSqliteError(() =>
+ c.batch(["INSERT INTO t VALUES ('two')", "SELECT foobar", "INSERT INTO t VALUES ('three')"], "write")
+ );
+
+ const rs = await c.execute("SELECT COUNT(*) FROM t");
+ expect(rs.rows[0][0]).toStrictEqual(1);
+ })
+ );
+
+ test(
+ "batch with a lot of different statements",
+ withClient(async (c) => {
+ const stmts: string[] = [];
+ for (let i = 0; i < 1000; ++i) {
+ stmts.push(`SELECT ${i}`);
+ }
+ const rss = await c.batch(stmts, "read");
+ for (let i = 0; i < stmts.length; ++i) {
+ expect(rss[i].rows[0][0]).toStrictEqual(i);
+ }
+ })
+ );
+
+ test(
+ "batch with a lot of the same statements",
+ withClient(async (c) => {
+ const n = 2;
+ const m = 3;
+
+ const stmts: libsql.InStatement[] = [];
+ for (let i = 0; i < n; ++i) {
+ for (let j = 0; j < m; ++j) {
+ stmts.push({ sql: `SELECT $a, $b`, args: { $a: i, $b: j } });
+ }
+ }
+
+ const rss = await c.batch(stmts, "read");
+ for (let i = 0; i < n; ++i) {
+ for (let j = 0; j < m; ++j) {
+ const rs = rss[i * m + j];
+ expect(rs.rows[0][0]).toStrictEqual(i);
+ expect(rs.rows[0][1]).toStrictEqual(j);
+ }
+ }
+ })
+ );
+
+ test(
+ "deferred batch",
+ withClient(async (c) => {
+ const rss = await c.batch(
+ ["SELECT 1+1", "DROP TABLE IF EXISTS t", "CREATE TABLE t (a)", "INSERT INTO t VALUES (21) RETURNING 2*a"],
+ "deferred"
+ );
+ expect(rss.length).toStrictEqual(4);
+ const [rs0, _rs1, _rs2, rs3] = rss;
+
+ expect(rs0.rows.length).toStrictEqual(1);
+ expect(Array.from(rs0.rows[0])).toStrictEqual([2]);
+
+ expect(rs3.rows.length).toStrictEqual(1);
+ expect(Array.from(rs3.rows[0])).toStrictEqual([42]);
+ })
+ );
+
+ test(
+ "ROLLBACK statement stops execution of batch",
+ withClient(async (c) => {
+ await c.execute("DROP TABLE IF EXISTS t");
+ await c.execute("CREATE TABLE t (a)");
+
+ await expectLibSqlError(() =>
+ c.batch(["INSERT INTO t VALUES (1), (2), (3)", "ROLLBACK", "INSERT INTO t VALUES (4), (5)"], "write")
+ );
+
+ const rs = await c.execute("SELECT COUNT(*) FROM t");
+ expect(rs.rows[0][0]).toStrictEqual(0);
+ })
+ );
+});
+
+describe("transaction()", () => {
+ test(
+ "query multiple rows",
+ withClient(async (c) => {
+ const txn = await c.transaction("read");
+
+ const rs = await txn.execute("VALUES (1, 'one'), (2, 'two'), (3, 'three')");
+ expect(rs.columns.length).toStrictEqual(2);
+ expect(rs.rows.length).toStrictEqual(3);
+
+ expect(Array.from(rs.rows[0])).toStrictEqual([1, "one"]);
+ expect(Array.from(rs.rows[1])).toStrictEqual([2, "two"]);
+ expect(Array.from(rs.rows[2])).toStrictEqual([3, "three"]);
+
+ txn.close();
+ })
+ );
+
+ test(
+ "commit()",
+ withClient(async (c) => {
+ await c.batch(["DROP TABLE IF EXISTS t", "CREATE TABLE t (a)"], "write");
+
+ const txn = await c.transaction("write");
+ await txn.execute("INSERT INTO t VALUES ('one')");
+ await txn.execute("INSERT INTO t VALUES ('two')");
+ expect(txn.closed).toStrictEqual(false);
+ await txn.commit();
+ expect(txn.closed).toStrictEqual(true);
+
+ const rs = await c.execute("SELECT COUNT(*) FROM t");
+ expect(rs.rows[0][0]).toStrictEqual(2);
+ await expectLibSqlError(() => txn.execute("SELECT 1"), withPattern("TRANSACTION_CLOSED"));
+ })
+ );
+
+ test(
+ "rollback()",
+ withClient(async (c) => {
+ await c.batch(["DROP TABLE IF EXISTS t", "CREATE TABLE t (a)"], "write");
+
+ const txn = await c.transaction("write");
+ await txn.execute("INSERT INTO t VALUES ('one')");
+ await txn.execute("INSERT INTO t VALUES ('two')");
+ expect(txn.closed).toStrictEqual(false);
+ await txn.rollback();
+ expect(txn.closed).toStrictEqual(true);
+
+ const rs = await c.execute("SELECT COUNT(*) FROM t");
+ expect(rs.rows[0][0]).toStrictEqual(0);
+ await expectLibSqlError(() => txn.execute("SELECT 1"), withPattern("TRANSACTION_CLOSED"));
+ })
+ );
+
+ test(
+ "close()",
+ withClient(async (c) => {
+ await c.batch(["DROP TABLE IF EXISTS t", "CREATE TABLE t (a)"], "write");
+
+ const txn = await c.transaction("write");
+ await txn.execute("INSERT INTO t VALUES ('one')");
+ expect(txn.closed).toStrictEqual(false);
+ txn.close();
+ expect(txn.closed).toStrictEqual(true);
+
+ const rs = await c.execute("SELECT COUNT(*) FROM t");
+ expect(rs.rows[0][0]).toStrictEqual(0);
+ await expectLibSqlError(() => txn.execute("SELECT 1"), withPattern("TRANSACTION_CLOSED"));
+ })
+ );
+
+ test(
+ "error does not rollback",
+ withClient(async (c) => {
+ await c.batch(["DROP TABLE IF EXISTS t", "CREATE TABLE t (a)"], "write");
+
+ const txn = await c.transaction("write");
+ await expectBunSqliteError(() => txn.execute("SELECT foo"));
+
+ await txn.execute("INSERT INTO t VALUES ('one')");
+ await expectBunSqliteError(() => txn.execute("SELECT bar"));
+
+ await txn.commit();
+
+ const rs = await c.execute("SELECT COUNT(*) FROM t");
+ expect(rs.rows[0][0]).toStrictEqual(1);
+ })
+ );
+
+ test(
+ "ROLLBACK statement stops execution of transaction",
+ withClient(async (c) => {
+ await c.execute("DROP TABLE IF EXISTS t");
+ await c.execute("CREATE TABLE t (a)");
+
+ const txn = await c.transaction("write");
+ const prom1 = txn.execute("INSERT INTO t VALUES (1), (2), (3)");
+ const prom2 = txn.execute("ROLLBACK");
+ const prom3 = txn.execute("INSERT INTO t VALUES (4), (5)");
+
+ await prom1;
+ await prom2;
+ await expectLibSqlError(() => prom3, withPattern("TRANSACTION_CLOSED"));
+ await expectLibSqlError(() => txn.commit());
+ txn.close();
+
+ const rs = await c.execute("SELECT COUNT(*) FROM t");
+ expect(rs.rows[0][0]).toStrictEqual(0);
+ })
+ );
+
+ test(
+ "OR ROLLBACK statement stops execution of transaction",
+ withClient(async (c) => {
+ await c.execute("DROP TABLE IF EXISTS t");
+ await c.execute("CREATE TABLE t (a UNIQUE)");
+
+ const txn = await c.transaction("write");
+ const prom1 = txn.execute("INSERT INTO t VALUES (1), (2), (3)");
+ const prom2 = txn.execute("INSERT OR ROLLBACK INTO t VALUES (1)");
+ const prom3 = txn.execute("INSERT INTO t VALUES (4), (5)");
+
+ await prom1;
+ await expectBunSqliteError(() => prom2);
+ await expectLibSqlError(() => prom3, withPattern("TRANSACTION_CLOSED"));
+ await expectLibSqlError(() => txn.commit(), withPattern("TRANSACTION_CLOSED"));
+ txn.close();
+
+ const rs = await c.execute("SELECT COUNT(*) FROM t");
+ expect(rs.rows[0][0]).toStrictEqual(0);
+ })
+ );
+
+ test(
+ "OR ROLLBACK as the first statement stops execution of transaction",
+ withClient(async (c) => {
+ await c.execute("DROP TABLE IF EXISTS t");
+ await c.execute("CREATE TABLE t (a UNIQUE)");
+ await c.execute("INSERT INTO t VALUES (1), (2), (3)");
+
+ const txn = await c.transaction("write");
+ const prom1 = txn.execute("INSERT OR ROLLBACK INTO t VALUES (1)");
+ const prom2 = txn.execute("INSERT INTO t VALUES (4), (5)");
+
+ await expectBunSqliteError(() => prom1);
+ await expectLibSqlError(() => prom2, withPattern("TRANSACTION_CLOSED"));
+ await expectLibSqlError(() => txn.commit(), withPattern("TRANSACTION_CLOSED"));
+
+ txn.close();
+
+ const rs = await c.execute("SELECT COUNT(*) FROM t");
+ expect(rs.rows[0][0]).toStrictEqual(3);
+ })
+ );
+
+ test(
+ "commit empty",
+ withClient(async (c) => {
+ const txn = await c.transaction("read");
+ await txn.commit();
+ })
+ );
+
+ test(
+ "rollback empty",
+ withClient(async (c) => {
+ const txn = await c.transaction("read");
+ await txn.rollback();
+ })
+ );
+});
+
+describe("batch()", () => {
+ test(
+ "as the first operation on transaction",
+ withClient(async (c) => {
+ const txn = await c.transaction("write");
+
+ await txn.batch([
+ "DROP TABLE IF EXISTS t",
+ "CREATE TABLE t (a)",
+ { sql: "INSERT INTO t VALUES (?)", args: [1] },
+ { sql: "INSERT INTO t VALUES (?)", args: [2] },
+ { sql: "INSERT INTO t VALUES (?)", args: [4] },
+ ]);
+
+ const rs = await txn.execute("SELECT SUM(a) FROM t");
+ expect(rs.rows[0][0]).toStrictEqual(7);
+ txn.close();
+ })
+ );
+
+ test(
+ "as the second operation on transaction",
+ withClient(async (c) => {
+ const txn = await c.transaction("write");
+
+ await txn.execute("DROP TABLE IF EXISTS t");
+ await txn.batch([
+ "CREATE TABLE t (a)",
+ { sql: "INSERT INTO t VALUES (?)", args: [1] },
+ { sql: "INSERT INTO t VALUES (?)", args: [2] },
+ { sql: "INSERT INTO t VALUES (?)", args: [4] },
+ ]);
+
+ const rs = await txn.execute("SELECT SUM(a) FROM t");
+ expect(rs.rows[0][0]).toStrictEqual(7);
+ txn.close();
+ })
+ );
+
+ test(
+ "after error, further statements are not executed",
+ withClient(async (c) => {
+ const txn = await c.transaction("write");
+
+ await expectBunSqliteError(() =>
+ txn.batch([
+ "DROP TABLE IF EXISTS t",
+ "CREATE TABLE t (a UNIQUE)",
+ "INSERT INTO t VALUES (1), (2), (4)",
+ "INSERT INTO t VALUES (1)",
+ "INSERT INTO t VALUES (8), (16)",
+ ])
+ );
+ const rs = await txn.execute("SELECT SUM(a) FROM t");
+ expect(rs.rows[0][0]).toStrictEqual(7);
+
+ await txn.commit();
+ })
+ );
+});
+
+//@note Bun:sqlite doesn't implement executeMultiple due to lack of mulitine statement support.
+describe.skip("executeMultiple()", () => {
+ test(
+ "as the first operation on transaction",
+ withClient(async (c) => {
+ const txn = await c.transaction("write");
+
+ await txn.executeMultiple(
+ `DROP TABLE IF EXISTS t;
+ CREATE TABLE t (a);
+ INSERT INTO t VALUES (1), (2), (4), (8);`
+ );
+
+ const rs = await txn.execute("SELECT SUM(a) FROM t");
+ expect(rs.rows[0][0]).toStrictEqual(15);
+ txn.close();
+ })
+ );
+
+ test(
+ "as the second operation on transaction",
+ withClient(async (c) => {
+ const txn = await c.transaction("write");
+ await txn.execute("DROP TABLE IF EXISTS t");
+ await txn.executeMultiple(`
+ CREATE TABLE t (a);
+ INSERT INTO t VALUES (1), (2), (4), (8);
+ `);
+
+ const rs = await txn.execute("SELECT SUM(a) FROM t");
+ expect(rs.rows[0][0]).toStrictEqual(15);
+ txn.close();
+ })
+ );
+
+ test(
+ "after error, further statements are not executed",
+ withClient(async (c) => {
+ const txn = await c.transaction("write");
+
+ await expectBunSqliteError(() =>
+ txn.executeMultiple(`
+ DROP TABLE IF EXISTS t;
+ CREATE TABLE t (a UNIQUE);
+ INSERT INTO t VALUES (1), (2), (4);
+ INSERT INTO t VALUES (1);
+ INSERT INTO t VALUES (8), (16);`)
+ );
+ const rs = await txn.execute("SELECT SUM(a) FROM t");
+ expect(rs.rows[0][0]).toStrictEqual(7);
+
+ await txn.commit();
+ })
+ );
+
+ test(
+ "multiple statements",
+ withClient(async (c) => {
+ await c.executeMultiple(`
+ DROP TABLE IF EXISTS t;
+ CREATE TABLE t (a);
+ INSERT INTO t VALUES (1), (2), (4), (8);
+ `);
+ const rs = await c.execute("SELECT SUM(a) FROM t");
+ expect(rs.rows[0][0]).toStrictEqual(15);
+ })
+ );
+
+ test(
+ "after an error, statements are not executed",
+ withClient(async (c) => {
+ await expectBunSqliteError(() =>
+ c.executeMultiple(`
+ DROP TABLE IF EXISTS t;
+ CREATE TABLE t (a);
+ INSERT INTO t VALUES (1), (2), (4);
+ INSERT INTO t VALUES (foo());
+ INSERT INTO t VALUES (100), (1000);`)
+ );
+ const rs = await c.execute("SELECT SUM(a) FROM t");
+ expect(rs.rows[0][0]).toStrictEqual(15);
+ })
+ );
+
+ test(
+ "manual transaction control statements",
+ withClient(async (c) => {
+ await c.executeMultiple(`
+ DROP TABLE IF EXISTS t;
+ CREATE TABLE t (a);
+ BEGIN;
+ INSERT INTO t VALUES (1), (2), (4);
+ INSERT INTO t VALUES (8), (16);
+ COMMIT;
+ `);
+
+ const rs = await c.execute("SELECT SUM(a) FROM t");
+ expect(rs.rows[0][0]).toStrictEqual(31);
+ })
+ );
+
+ test(
+ "error rolls back a manual transaction",
+ withClient(async (c) => {
+ await expect(
+ c.executeMultiple(`
+ DROP TABLE IF EXISTS t;
+ CREATE TABLE t (a);
+ INSERT INTO t VALUES (0);
+ BEGIN;
+ INSERT INTO t VALUES (1), (2), (4);
+ INSERT INTO t VALUES (foo());
+ INSERT INTO t VALUES (8), (16);
+ COMMIT;
+ `)
+ ).toThrow();
+ // .rejects.toBeLibsqlError();
+ const rs = await c.execute("SELECT SUM(a) FROM t");
+ expect(rs.rows[0][0]).toStrictEqual(0);
+ })
+ );
+});
+
+//@note bun implementation is tested locally.
+describe.skip("network errors", () => {
+ const testCases = [
+ { title: "WebSocket close", sql: ".close_ws" },
+ { title: "TCP close", sql: ".close_tcp" },
+ ];
+
+ for (const { title, sql } of testCases) {
+ test(
+ `${title} in execute()`,
+ withClient(async (c) => {
+ await expect(c.execute(sql)).toThrow();
+ // .rejects.toBeLibsqlError("HRANA_WEBSOCKET_ERROR");
+
+ expect((await c.execute("SELECT 42")).rows[0][0]).toStrictEqual(42);
+ })
+ );
+
+ test(
+ `${title} in transaction()`,
+ withClient(async (c) => {
+ const txn = await c.transaction("read");
+ await expect(txn.execute(sql)).rejects.toThrow();
+ // .toBeLibsqlError("HRANA_WEBSOCKET_ERROR");
+ await expect(txn.commit()).toThrow();
+ // .rejects.toBeLibsqlError("TRANSACTION_CLOSED");
+ txn.close();
+
+ expect((await c.execute("SELECT 42")).rows[0][0]).toStrictEqual(42);
+ })
+ );
+
+ test(
+ `${title} in batch()`,
+ withClient(async (c) => {
+ await expect(c.batch(["SELECT 42", sql, "SELECT 24"], "read")).toThrow();
+ // .rejects.toBeLibsqlError("HRANA_WEBSOCKET_ERROR");
+
+ expect((await c.execute("SELECT 42")).rows[0][0]).toStrictEqual(42);
+ })
+ );
+ }
+});
+
+//@note bun implementation is tested locally.
+test.skip("custom fetch", async () => {
+ let fetchCalledCount = 0;
+ function customFetch(request: Request): Promise {
+ fetchCalledCount += 1;
+ return fetch(request);
+ }
+
+ const c = createClient({ ...config, fetch: customFetch });
+ try {
+ const rs = await c.execute("SELECT 42");
+ expect(rs.rows[0][0]).toStrictEqual(42);
+ expect(fetchCalledCount).toBeGreaterThan(0);
+ } finally {
+ c.close();
+ }
+});
diff --git a/src/bun_tests/bun.helpers.ts b/src/bun_tests/bun.helpers.ts
new file mode 100644
index 00000000..2aad1974
--- /dev/null
+++ b/src/bun_tests/bun.helpers.ts
@@ -0,0 +1,29 @@
+import { expect } from "bun:test";
+
+import { LibsqlError } from "../bun";
+
+export const withPattern = (...patterns: Array) =>
+ new RegExp(
+ patterns
+ .map((pattern) =>
+ typeof pattern === "string"
+ ? `(?=.*${pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`
+ : `(?=.*${pattern.source})`
+ )
+ .join("")
+ );
+
+export const expectLibSqlError = async (f: () => any, pattern?: string | RegExp) => {
+ try {
+ await f();
+ } catch (e: any) {
+ expect(e).toBeInstanceOf(LibsqlError);
+ expect(e.code.length).toBeGreaterThan(0);
+ if (pattern !== undefined) {
+ expect(e.message).toMatch(pattern);
+ }
+ }
+};
+
+export const expectBunSqliteError = async (f: () => any, pattern?: string | RegExp) =>
+ expectLibSqlError(f, pattern ? withPattern("BUN_SQLITE ERROR", pattern) : withPattern("BUN_SQLITE ERROR"));
diff --git a/src/bun_tests/bun.uri.test.ts b/src/bun_tests/bun.uri.test.ts
new file mode 100644
index 00000000..ad96ab82
--- /dev/null
+++ b/src/bun_tests/bun.uri.test.ts
@@ -0,0 +1,246 @@
+import { describe, test, expect } from "bun:test";
+import { parseUri, encodeBaseUrl } from "../uri";
+import { expectLibSqlError, withPattern } from "./bun.helpers";
+
+describe("parseUri()", () => {
+ test("authority and path", () => {
+ const cases = [
+ { text: "file://localhost", path: "" },
+ { text: "file://localhost/", path: "/" },
+ { text: "file://localhost/absolute/path", path: "/absolute/path" },
+ { text: "file://localhost/k%C5%AF%C5%88", path: "/kůň" }
+ ];
+ for (const { text, path } of cases) {
+ expect(parseUri(text)).toEqual({
+ scheme: "file",
+ authority: { host: "localhost" },
+ path
+ });
+ }
+ });
+
+ test("empty authority and path", () => {
+ const cases = [
+ { text: "file:///absolute/path", path: "/absolute/path" },
+ { text: "file://", path: "" },
+ { text: "file:///k%C5%AF%C5%88", path: "/kůň" }
+ ];
+ for (const { text, path } of cases) {
+ expect(parseUri(text)).toEqual({
+ scheme: "file",
+ authority: { host: "" },
+ path
+ });
+ }
+ });
+
+ test("no authority and path", () => {
+ const cases = [
+ { text: "file:/absolute/path", path: "/absolute/path" },
+ { text: "file:relative/path", path: "relative/path" },
+ { text: "file:", path: "" },
+ { text: "file:C:/path/to/file", path: "C:/path/to/file" },
+ { text: "file:k%C5%AF%C5%88", path: "kůň" }
+ ];
+ for (const { text, path } of cases) {
+ expect(parseUri(text)).toEqual({
+ scheme: "file",
+ path
+ });
+ }
+ });
+
+ test("authority", () => {
+ const hosts = [
+ { text: "localhost", host: "localhost" },
+ { text: "domain.name", host: "domain.name" },
+ { text: "some$weird.%20!name", host: "some$weird. !name" },
+ { text: "1.20.255.99", host: "1.20.255.99" },
+ { text: "[2001:4860:4802:32::a]", host: "2001:4860:4802:32::a" },
+ { text: "%61", host: "a" },
+ { text: "100%2e100%2e100%2e100", host: "100.100.100.100" },
+ { text: "k%C5%AF%C5%88", host: "kůň" }
+ ];
+ const ports = [
+ { text: "", port: undefined },
+ { text: ":", port: undefined },
+ { text: ":0", port: 0 },
+ { text: ":99", port: 99 },
+ { text: ":65535", port: 65535 }
+ ];
+ const userinfos = [
+ { text: "", userinfo: undefined },
+ { text: "@", userinfo: { username: "" } },
+ { text: "alice@", userinfo: { username: "alice" } },
+ { text: "alice:secret@", userinfo: { username: "alice", password: "secret" } },
+ { text: "alice:sec:et@", userinfo: { username: "alice", password: "sec:et" } },
+ { text: "alice%3Asecret@", userinfo: { username: "alice:secret" } },
+ { text: "alice:s%65cret@", userinfo: { username: "alice", password: "secret" } }
+ ];
+
+ for (const { text: hostText, host } of hosts) {
+ for (const { text: portText, port } of ports) {
+ for (const { text: userText, userinfo } of userinfos) {
+ const text = `http://${userText}${hostText}${portText}`;
+ expect(parseUri(text)).toEqual({
+ scheme: "http",
+ authority: { host, port, userinfo },
+ path: ""
+ });
+ }
+ }
+ }
+ });
+
+ test("query", () => {
+ const cases = [
+ { text: "?", pairs: [] },
+ { text: "?key=value", pairs: [{ key: "key", value: "value" }] },
+ { text: "?&key=value", pairs: [{ key: "key", value: "value" }] },
+ { text: "?key=value&&", pairs: [{ key: "key", value: "value" }] },
+ { text: "?a", pairs: [{ key: "a", value: "" }] },
+ { text: "?a=", pairs: [{ key: "a", value: "" }] },
+ { text: "?=a", pairs: [{ key: "", value: "a" }] },
+ { text: "?=", pairs: [{ key: "", value: "" }] },
+ { text: "?a=b=c", pairs: [{ key: "a", value: "b=c" }] },
+ {
+ text: "?a=b&c=d",
+ pairs: [
+ { key: "a", value: "b" },
+ { key: "c", value: "d" }
+ ]
+ },
+ { text: "?a+b=c", pairs: [{ key: "a b", value: "c" }] },
+ { text: "?a=b+c", pairs: [{ key: "a", value: "b c" }] },
+ { text: "?a?b", pairs: [{ key: "a?b", value: "" }] },
+ { text: "?%61=%62", pairs: [{ key: "a", value: "b" }] },
+ { text: "?a%3db", pairs: [{ key: "a=b", value: "" }] },
+ { text: "?a=%2b", pairs: [{ key: "a", value: "+" }] },
+ { text: "?%2b=b", pairs: [{ key: "+", value: "b" }] },
+ { text: "?a=b%26c", pairs: [{ key: "a", value: "b&c" }] },
+ { text: "?a=k%C5%AF%C5%88", pairs: [{ key: "a", value: "kůň" }] }
+ ];
+ for (const { text: queryText, pairs } of cases) {
+ const text = `file:${queryText}`;
+ expect(parseUri(text)).toEqual({
+ scheme: "file",
+ path: "",
+ query: { pairs }
+ });
+ }
+ });
+
+ test("fragment", () => {
+ const cases = [
+ { text: "", fragment: undefined },
+ { text: "#a", fragment: "a" },
+ { text: "#a?b", fragment: "a?b" },
+ { text: "#%61", fragment: "a" },
+ { text: "#k%C5%AF%C5%88", fragment: "kůň" }
+ ];
+ for (const { text: fragmentText, fragment } of cases) {
+ const text = `file:${fragmentText}`;
+ expect(parseUri(text)).toEqual({
+ scheme: "file",
+ path: "",
+ fragment
+ });
+ }
+ });
+
+ test("parse errors", () => {
+ const cases = [
+ { text: "", message: /format/ },
+ { text: "foo", message: /format/ },
+ { text: "foo.bar.com", message: /format/ },
+ { text: "h$$p://localhost", message: /format/ },
+ { text: "h%74%74p://localhost", message: /format/ },
+ { text: "http://localhost:%38%38", message: /authority/ },
+ { text: "file:k%C5%C5%88", message: /percent encoding/ }
+ ];
+
+ for (const { text, message } of cases) {
+ expectLibSqlError(() => parseUri(text), withPattern("URL_INVALID", message));
+ }
+ });
+});
+
+test("encodeBaseUrl()", () => {
+ const cases = [
+ {
+ scheme: "http",
+ host: "localhost",
+ path: "",
+ url: "http://localhost"
+ },
+ {
+ scheme: "http",
+ host: "localhost",
+ path: "/",
+ url: "http://localhost/"
+ },
+ {
+ scheme: "http",
+ host: "localhost",
+ port: 8080,
+ path: "",
+ url: "http://localhost:8080"
+ },
+ {
+ scheme: "http",
+ host: "localhost",
+ path: "/foo/bar",
+ url: "http://localhost/foo/bar"
+ },
+ {
+ scheme: "http",
+ host: "localhost",
+ path: "foo/bar",
+ url: "http://localhost/foo/bar"
+ },
+ {
+ scheme: "http",
+ host: "some.long.domain.name",
+ path: "",
+ url: "http://some.long.domain.name"
+ },
+ {
+ scheme: "http",
+ host: "1.2.3.4",
+ path: "",
+ url: "http://1.2.3.4"
+ },
+ {
+ scheme: "http",
+ host: "2001:4860:4802:32::a",
+ path: "",
+ url: "http://[2001:4860:4802:32::a]"
+ },
+ {
+ scheme: "http",
+ host: "localhost",
+ userinfo: { username: "alice", password: undefined },
+ path: "",
+ url: "http://alice@localhost"
+ },
+ {
+ scheme: "http",
+ host: "localhost",
+ userinfo: { username: "alice", password: "secr:t" },
+ path: "",
+ url: "http://alice:secr%3At@localhost"
+ },
+ {
+ scheme: "https",
+ host: "localhost",
+ userinfo: { username: "alice", password: "secret" },
+ port: 8080,
+ path: "/some/path",
+ url: "https://alice:secret@localhost:8080/some/path"
+ }
+ ];
+
+ for (const { scheme, host, port, userinfo, path, url } of cases) {
+ expect(encodeBaseUrl(scheme, { host, port, userinfo }, path)).toEqual(new URL(url));
+ }
+});
diff --git a/src/node.ts b/src/node.ts
index f0800bba..4ba096eb 100644
--- a/src/node.ts
+++ b/src/node.ts
@@ -1,5 +1,4 @@
import type { Config, Client } from "./api.js";
-import { LibsqlError } from "./api.js";
import type { ExpandedConfig } from "./config.js";
import { expandConfig } from "./config.js";
import { _createClient as _createSqlite3Client } from "./sqlite3.js";
diff --git a/src/sqlite3.ts b/src/sqlite3.ts
index c5e32b46..fdefade6 100644
--- a/src/sqlite3.ts
+++ b/src/sqlite3.ts
@@ -2,51 +2,38 @@ import Database from "better-sqlite3";
import { Buffer } from "node:buffer";
import type {
- Config, IntMode, Client, Transaction, TransactionMode,
- ResultSet, Row, Value, InValue, InStatement,
+ Config,
+ IntMode,
+ Client,
+ Transaction,
+ TransactionMode,
+ ResultSet,
+ Row,
+ Value,
+ InValue,
+ InStatement,
} from "./api.js";
import { LibsqlError } from "./api.js";
-import type { ExpandedConfig } from "./config.js";
-import { expandConfig } from "./config.js";
-import { supportedUrlLink, transactionModeToBegin, ResultSetImpl } from "./util.js";
+import { expandConfig, type ExpandedConfig } from "./config.js";
+import {
+ transactionModeToBegin,
+ ResultSetImpl,
+ validateFileConfig,
+ parseStatement,
+ minSafeBigint,
+ maxSafeBigint,
+ minInteger,
+ maxInteger,
+} from "./util.js";
export * from "./api.js";
export function createClient(config: Config): Client {
- return _createClient(expandConfig(config, true));
+ return _createClient(validateFileConfig(expandConfig(config, true)));
}
/** @private */
export function _createClient(config: ExpandedConfig): Client {
- if (config.scheme !== "file") {
- throw new LibsqlError(
- `URL scheme ${JSON.stringify(config.scheme + ":")} is not supported by the local sqlite3 client. ` +
- `For more information, please read ${supportedUrlLink}`,
- "URL_SCHEME_NOT_SUPPORTED",
- );
- }
-
- const authority = config.authority;
- if (authority !== undefined) {
- const host = authority.host.toLowerCase();
- if (host !== "" && host !== "localhost") {
- throw new LibsqlError(
- `Invalid host in file URL: ${JSON.stringify(authority.host)}. ` +
- 'A "file:" URL with an absolute path should start with one slash ("file:/absolute/path.db") ' +
- 'or with three slashes ("file:///absolute/path.db"). ' +
- `For more information, please read ${supportedUrlLink}`,
- "URL_INVALID",
- );
- }
-
- if (authority.port !== undefined) {
- throw new LibsqlError("File URL cannot have a port", "URL_INVALID");
- }
- if (authority.userinfo !== undefined) {
- throw new LibsqlError("File URL cannot have username and password", "URL_INVALID");
- }
- }
-
const path = config.path;
const options = {};
@@ -97,7 +84,7 @@ export class Sqlite3Client implements Client {
}
return executeStmt(db, stmt, this.#intMode);
});
- executeStmt(db, "COMMIT", this.#intMode)
+ executeStmt(db, "COMMIT", this.#intMode);
return resultSets;
} finally {
db.close();
@@ -195,24 +182,8 @@ export class Sqlite3Transaction implements Transaction {
}
function executeStmt(db: Database.Database, stmt: InStatement, intMode: IntMode): ResultSet {
- let sql: string;
- let args: Array | Record;
- if (typeof stmt === "string") {
- sql = stmt;
- args = [];
- } else {
- sql = stmt.sql;
- if (Array.isArray(stmt.args)) {
- args = stmt.args.map(valueToSql);
- } else {
- args = {};
- for (const name in stmt.args) {
- const argName = (name[0] === "@" || name[0] === "$" || name[0] === ":")
- ? name.substring(1) : name;
- args[argName] = valueToSql(stmt.args[name]);
- }
- }
- }
+ const transformKeys = (name: string) => (name[0] === "@" || name[0] === "$" || name[0] === ":" ? name.substring(1) : name);
+ const { sql, args } = parseStatement(stmt, valueToSql, transformKeys);
try {
const sqlStmt = db.prepare(sql);
@@ -227,7 +198,7 @@ function executeStmt(db: Database.Database, stmt: InStatement, intMode: IntMode)
}
if (returnsData) {
- const columns = Array.from(sqlStmt.columns().map(col => col.name));
+ const columns = Array.from(sqlStmt.columns().map((col) => col.name));
const rows = sqlStmt.all(args).map((sqlRow) => {
return rowFromSql(sqlRow as Array, columns, intMode);
});
@@ -266,28 +237,23 @@ function valueFromSql(sqlValue: unknown, intMode: IntMode): Value {
if (typeof sqlValue === "bigint") {
if (intMode === "number") {
if (sqlValue < minSafeBigint || sqlValue > maxSafeBigint) {
- throw new RangeError(
- "Received integer which cannot be safely represented as a JavaScript number"
- );
+ throw new RangeError("Received integer which cannot be safely represented as a JavaScript number");
}
return Number(sqlValue);
} else if (intMode === "bigint") {
return sqlValue;
} else if (intMode === "string") {
- return ""+sqlValue;
+ return "" + sqlValue;
} else {
throw new Error("Invalid value for IntMode");
}
} else if (sqlValue instanceof Buffer) {
- return sqlValue.buffer;
+ return sqlValue.buffer as Value;
}
return sqlValue as Value;
}
-const minSafeBigint = -9007199254740991n;
-const maxSafeBigint = 9007199254740991n;
-
-function valueToSql(value: InValue): unknown {
+function valueToSql(value: InValue) {
if (typeof value === "number") {
if (!Number.isFinite(value)) {
throw new RangeError("Only finite numbers (not Infinity or NaN) can be passed as arguments");
@@ -295,9 +261,7 @@ function valueToSql(value: InValue): unknown {
return value;
} else if (typeof value === "bigint") {
if (value < minInteger || value > maxInteger) {
- throw new RangeError(
- "bigint is too large to be represented as a 64-bit integer and passed as argument"
- );
+ throw new RangeError("bigint is too large to be represented as a 64-bit integer and passed as argument");
}
return value;
} else if (typeof value === "boolean") {
@@ -313,9 +277,6 @@ function valueToSql(value: InValue): unknown {
}
}
-const minInteger = -9223372036854775808n;
-const maxInteger = 9223372036854775807n;
-
function executeMultiple(db: Database.Database, sql: string): void {
try {
db.exec(sql);
diff --git a/src/util.ts b/src/util.ts
index 521b35e8..fbc29dcf 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1,8 +1,65 @@
import { Base64 } from "js-base64";
-import { ResultSet, Row, Value, TransactionMode, InStatement, LibsqlError } from "./api.js";
+import { ResultSet, Row, Value, TransactionMode, LibsqlError, type InStatement, type InValue } from "./api.js";
+import type { ExpandedConfig } from "./config.js";
export const supportedUrlLink = "https://github.com/libsql/libsql-client-ts#supported-urls";
+type TransformFunction = (v: InValue) => any;
+type ParseStatementReturn =
+ | {
+ sql: string;
+ args: [];
+ }
+ | {
+ sql: string;
+ args: ReturnType[] | { [k: string]: ReturnType };
+ };
+
+export const parseStatement = (
+ stmt: InStatement,
+ transformValues: T,
+ transformKeys = (v: string) => v
+): ParseStatementReturn =>
+ typeof stmt === "string"
+ ? { sql: stmt, args: [] }
+ : {
+ sql: stmt.sql,
+ args: Array.isArray(stmt.args)
+ ? stmt.args.map(transformValues)
+ : Object.fromEntries(Object.entries(stmt.args).map(([k, v]) => [transformKeys(k), transformValues(v)])),
+ };
+
+export function validateFileConfig(config: ExpandedConfig) {
+ if (config.scheme !== "file") {
+ throw new LibsqlError(
+ `URL scheme ${JSON.stringify(config.scheme + ":")} is not supported by the local sqlite3 client. ` +
+ `For more information, please read ${supportedUrlLink}`,
+ "URL_SCHEME_NOT_SUPPORTED"
+ );
+ }
+
+ const authority = config.authority;
+ if (authority !== undefined) {
+ const host = authority.host.toLowerCase();
+ if (host !== "" && host !== "localhost") {
+ throw new LibsqlError(
+ `Invalid host in file URL: ${JSON.stringify(authority.host)}. ` +
+ 'A "file:" URL with an absolute path should start with one slash ("file:/absolute/path.db") ' +
+ 'or with three slashes ("file:///absolute/path.db"). ' +
+ `For more information, please read ${supportedUrlLink}`,
+ "URL_INVALID"
+ );
+ }
+
+ if (authority.port !== undefined) {
+ throw new LibsqlError("File URL cannot have a port", "URL_INVALID");
+ }
+ if (authority.userinfo !== undefined) {
+ throw new LibsqlError("File URL cannot have username and password", "URL_INVALID");
+ }
+ }
+ return config;
+}
export function transactionModeToBegin(mode: TransactionMode): string {
if (mode === "write") {
return "BEGIN IMMEDIATE";
@@ -21,12 +78,7 @@ export class ResultSetImpl implements ResultSet {
rowsAffected: number;
lastInsertRowid: bigint | undefined;
- constructor(
- columns: Array,
- rows: Array,
- rowsAffected: number,
- lastInsertRowid: bigint | undefined,
- ) {
+ constructor(columns: Array, rows: Array, rowsAffected: number, lastInsertRowid: bigint | undefined) {
this.columns = columns;
this.rows = rows;
this.rowsAffected = rowsAffected;
@@ -35,10 +87,10 @@ export class ResultSetImpl implements ResultSet {
toJSON(): any {
return {
- "columns": this.columns,
- "rows": this.rows.map(rowToJson),
- "rowsAffected": this.rowsAffected,
- "lastInsertRowid": this.lastInsertRowid !== undefined ? ""+this.lastInsertRowid : null,
+ columns: this.columns,
+ rows: this.rows.map(rowToJson),
+ rowsAffected: this.rowsAffected,
+ lastInsertRowid: this.lastInsertRowid !== undefined ? "" + this.lastInsertRowid : null,
};
}
}
@@ -49,10 +101,15 @@ function rowToJson(row: Row): unknown {
function valueToJson(value: Value): unknown {
if (typeof value === "bigint") {
- return ""+value;
+ return "" + value;
} else if (value instanceof ArrayBuffer) {
return Base64.fromUint8Array(new Uint8Array(value));
} else {
return value;
}
}
+
+export const minInteger = -9223372036854775808n;
+export const maxInteger = 9223372036854775807n;
+export const minSafeBigint = -9007199254740991n;
+export const maxSafeBigint = 9007199254740991n;
diff --git a/tsconfig.build-cjs.json b/tsconfig.build-cjs.json
index 857027a0..920fe448 100644
--- a/tsconfig.build-cjs.json
+++ b/tsconfig.build-cjs.json
@@ -4,6 +4,6 @@
"module": "commonjs",
"declaration": false,
"outDir": "./lib-cjs/"
- }
+ },
+ "exclude": ["**/__tests__", "**/bun_tests"]
}
-
diff --git a/tsconfig.build-esm.json b/tsconfig.build-esm.json
index 9a01705b..5337ef0c 100644
--- a/tsconfig.build-esm.json
+++ b/tsconfig.build-esm.json
@@ -4,6 +4,6 @@
"module": "esnext",
"declaration": true,
"outDir": "./lib-esm/"
- }
+ },
+ "exclude": ["**/__tests__", "**/bun_tests"]
}
-
diff --git a/tsconfig.json b/tsconfig.json
index bc064274..c52bc3f4 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,8 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
+ "incremental": true,
"noEmit": true,
- "incremental": true
+ "types": ["bun-types", "@types/jest"]
}
}