diff --git a/CHANGELOG.md b/CHANGELOG.md index 68f3003..d7a7847 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Added `intMode` field to the `Config`, which chooses whether SQLite integers are represented as numbers, +bigints or strings in JavaScript ([#51](https://github.com/libsql/libsql-client-ts/pull/51)). + ## 0.2.1 -- 2023-06-13 - Added `TransactionMode` argument to `batch()` and `transaction()` ([#46](https://github.com/libsql/libsql-client-ts/pull/46)) diff --git a/package-lock.json b/package-lock.json index e643755..7def000 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.2.2-pre", "license": "MIT", "dependencies": { - "@libsql/hrana-client": "^0.4.1", + "@libsql/hrana-client": "^0.4.2", "better-sqlite3": "^8.0.1", "js-base64": "^3.7.5" }, @@ -953,9 +953,9 @@ "dev": true }, "node_modules/@libsql/hrana-client": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@libsql/hrana-client/-/hrana-client-0.4.1.tgz", - "integrity": "sha512-PeqJ+W2Ceh9Mt7yS8O6YyluPmQ4d/tuP+pHNa9lLsZ0LDoycBqFMvS7jLq7Nf9JWaH3dlnUZ0EQzWIV7fAdG1w==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@libsql/hrana-client/-/hrana-client-0.4.2.tgz", + "integrity": "sha512-62ACYRD8ATBVnZCScFmWnSiqhQgmKo2ktkgSBFqiur27pBlA4XnwnJnajIaXqwnjVsIvXExih9Gk9zfTfvqrIA==", "dependencies": { "@libsql/isomorphic-fetch": "^0.1.1", "@libsql/isomorphic-ws": "^0.1.2", @@ -963,9 +963,9 @@ } }, "node_modules/@libsql/isomorphic-fetch": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@libsql/isomorphic-fetch/-/isomorphic-fetch-0.1.1.tgz", - "integrity": "sha512-Ib8TkSPY+aBDLl78hA0Ry5KDY4EKtKAZccCx/Xa4Qb4hc7T5sDGg+aEM+4A76TO7hCD8iLnSt1eE2t/tsu7jlw==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@libsql/isomorphic-fetch/-/isomorphic-fetch-0.1.4.tgz", + "integrity": "sha512-eRgV4b1d6RVLciafGEKZghDOvrQ7fDuPgfGzelsfPz1HxFERaTQuOwL5FthZcKBtHI6Wqu5zmhUu5bREZr1mpA==", "dependencies": { "@types/node-fetch": "^2.2.6", "node-fetch": "^2.2.6" @@ -1385,9 +1385,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.8.tgz", - "integrity": "sha512-j+7xYe+v+q2Id9qbBeCI8WX5NmZSRe8es1+0xntD/+gaWXznP8tFEkv5IgSaHf5dS1YwVMbX/4W6m937mj+wQw==", + "version": "4.21.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", + "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", "dev": true, "funding": [ { @@ -1404,8 +1404,8 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001502", - "electron-to-chromium": "^1.4.428", + "caniuse-lite": "^1.0.30001503", + "electron-to-chromium": "^1.4.431", "node-releases": "^2.0.12", "update-browserslist-db": "^1.0.11" }, @@ -1485,9 +1485,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001502", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001502.tgz", - "integrity": "sha512-AZ+9tFXw1sS0o0jcpJQIXvFTOB/xGiQ4OQ2t98QX3NDn2EZTSRBC801gxrsGgViuq2ak/NLkNgSNEPtCr5lfKg==", + "version": "1.0.30001505", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001505.tgz", + "integrity": "sha512-jaAOR5zVtxHfL0NjZyflVTtXm3D3J9P15zSJ7HmQF8dSKGA6tqzQq+0ZI3xkjyQj46I4/M0K2GbMpcAFOcbr3A==", "dev": true, "funding": [ { @@ -1729,9 +1729,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.428", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.428.tgz", - "integrity": "sha512-L7uUknyY286of0AYC8CKfgWstD0Smk2DvHDi9F0GWQhSH90Bzi7iDrmCbZKz75tYJxeGSAc7TYeKpmbjMDoh1w==", + "version": "1.4.434", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.434.tgz", + "integrity": "sha512-5Gvm09UZTQRaWrimRtWRO5rvaX6Kpk5WHAPKDa7A4Gj6NIPuJ8w8WNpnxCXdd+CJJt6RBU6tUw0KyULoW6XuHw==", "dev": true }, "node_modules/emittery": { @@ -2732,9 +2732,9 @@ } }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -3129,9 +3129,9 @@ } }, "node_modules/node-abi/node_modules/semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -3940,9 +3940,9 @@ } }, "node_modules/ts-jest/node_modules/semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" diff --git a/package.json b/package.json index 53f169e..5db4df9 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "build:cjs": "tsc -p tsconfig.build-cjs.json", "build:esm": "tsc -p tsconfig.build-esm.json", "postbuild": "cp package-cjs.json ./lib-cjs/package.json", - "test": "jest", + "test": "jest --runInBand", "typecheck": "tsc --noEmit", "typedoc": "rm -rf ./docs && typedoc" }, @@ -87,7 +87,7 @@ "typescript": "^4.9.4" }, "dependencies": { - "@libsql/hrana-client": "^0.4.1", + "@libsql/hrana-client": "^0.4.2", "better-sqlite3": "^8.0.1", "js-base64": "^3.7.5" } diff --git a/src/__tests__/client.test.ts b/src/__tests__/client.test.ts index 716df1f..0727afe 100644 --- a/src/__tests__/client.test.ts +++ b/src/__tests__/client.test.ts @@ -21,9 +21,12 @@ const isFile = config.url.startsWith("file:"); // - "sqld" is sqld const server = process.env.SERVER ?? "test_v2"; -function withClient(f: (c: libsql.Client) => Promise): () => Promise { +function withClient( + f: (c: libsql.Client) => Promise, + extraConfig: Partial = {}, +): () => Promise { return async () => { - const c = createClient(config); + const c = createClient({...config, ...extraConfig}); try { await f(c); } finally { @@ -70,6 +73,11 @@ describe("createClient()", () => { // @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()", () => { @@ -172,13 +180,26 @@ describe("values", () => { name: string, passed: libsql.InValue, expected: libsql.Value, - opts: { skip?: boolean } = {}, + intMode?: libsql.IntMode, ): void { - const skip = opts.skip ?? false; - (skip ? test.skip : test)(name, withClient(async (c) => { + test(name, withClient(async (c) => { const rs = await c.execute({sql: "SELECT ?", args: [passed]}); expect(rs.rows[0][0]).toStrictEqual(expected); - })); + }, {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"); @@ -186,10 +207,36 @@ describe("values", () => { testRoundtrip("string with unicode", "žluťoučký kůň úpěl ďábelské ódy", "žluťoučký kůň úpěl ďábelské ódy"); - testRoundtrip("zero", 0, 0); + 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"); + testRoundtripError("smallest unsafe integer", 9007199254740992n, RangeError, "number"); + testRoundtripError("large unsafe integer", -1152921504594532842n, RangeError, "number"); + }); + + describe("'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"); + }); + + describe("'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) { @@ -205,34 +252,11 @@ describe("values", () => { testRoundtrip("bigint", -1000n, -1000); testRoundtrip("Date", new Date("2023-01-02T12:34:56Z"), 1672662896000); - test("undefined produces error", withClient(async (c) => { - await expect(c.execute({ - sql: "SELECT ?", - // @ts-expect-error - args: [undefined], - })).rejects.toBeInstanceOf(TypeError); - })); - - test("NaN produces error", withClient(async (c) => { - await expect(c.execute({ - sql: "SELECT ?", - args: [NaN], - })).rejects.toBeInstanceOf(RangeError); - })); - - test("Infinity produces error", withClient(async (c) => { - await expect(c.execute({ - sql: "SELECT ?", - args: [Infinity], - })).rejects.toBeInstanceOf(RangeError); - })); - - test("large bigint produces error", withClient(async (c) => { - await expect(c.execute({ - sql: "SELECT ?", - args: [-1267650600228229401496703205376n], - })).rejects.toBeInstanceOf(RangeError); - })); + // @ts-expect-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]}); diff --git a/src/api.ts b/src/api.ts index 4c45024..2e5566b 100644 --- a/src/api.ts +++ b/src/api.ts @@ -17,8 +17,22 @@ export interface Config { * By default, `libsql:` URLs use TLS. You can set this option to `false` to disable TLS. */ tls?: boolean; + + /** How to convert SQLite integers to JavaScript values: + * + * - `"number"` (default): returns SQLite integers as JavaScript `number`-s (double precision floats). + * `number` cannot precisely represent integers larger than 2^53-1 in absolute value, so attempting to read + * larger integers will throw a `RangeError`. + * - `"bigint"`: returns SQLite integers as JavaScript `bigint`-s (arbitrary precision integers). Bigints can + * precisely represent all SQLite integers. + * - `"string"`: returns SQLite integers as strings. + */ + intMode?: IntMode; } +/** Representation of integers from database as JavaScript values. See {@link Config.intMode}. */ +export type IntMode = "number" | "bigint" | "string"; + /** Client object for a remote or local database. * * After you are done with the client, you **should** close it by calling {@link close}. diff --git a/src/config.ts b/src/config.ts index 706c5b2..544ab67 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,4 @@ -import type { Config } from "./api.js"; +import type { Config, IntMode } from "./api.js"; import { LibsqlError } from "./api.js"; import type { Authority } from "./uri.js"; import { parseUri } from "./uri.js"; @@ -10,6 +10,7 @@ export interface ExpandedConfig { authority: Authority | undefined; path: string; authToken: string | undefined; + intMode: IntMode; } export type ExpandedScheme = "wss" | "ws" | "https" | "http" | "file"; @@ -83,11 +84,20 @@ export function expandConfig(config: Config, preferHttp: boolean): ExpandedConfi ); } + const intMode = ""+(config.intMode ?? "number"); + if (intMode !== "number" && intMode !== "bigint" && intMode !== "string") { + throw new TypeError( + `Invalid value for intMode, expected "number", "bigint" or "string", \ + got ${JSON.stringify(intMode)}` + ); + } + return { scheme, tls: tls ?? true, authority: uri.authority, path: uri.path, authToken, + intMode, }; } diff --git a/src/http.ts b/src/http.ts index 37c5412..67d9acd 100644 --- a/src/http.ts +++ b/src/http.ts @@ -1,8 +1,7 @@ import * as hrana from "@libsql/hrana-client"; -import { fetch } from "@libsql/isomorphic-fetch"; import type { Config, Client } from "./api.js"; -import type { InStatement, ResultSet, Transaction } from "./api.js"; +import type { InStatement, ResultSet, Transaction, IntMode } from "./api.js"; import { TransactionMode, LibsqlError } from "./api.js"; import type { ExpandedConfig } from "./config.js"; import { expandConfig } from "./config.js"; @@ -25,7 +24,7 @@ export function _createClient(config: ExpandedConfig): Client { if (config.scheme !== "https" && config.scheme !== "http") { throw new LibsqlError( 'The HTTP client supports only "libsql:", "https:" and "http:" URLs, ' + - `got ${JSON.stringify(config.scheme)}. For more information, please read ${supportedUrlLink}`, + `got ${JSON.stringify(config.scheme + ":")}. For more information, please read ${supportedUrlLink}`, "URL_SCHEME_NOT_SUPPORTED", ); } @@ -37,7 +36,7 @@ export function _createClient(config: ExpandedConfig): Client { } const url = encodeBaseUrl(config.scheme, config.authority, config.path); - return new HttpClient(url, config.authToken); + return new HttpClient(url, config.authToken, config.intMode); } const sqlCacheCapacity = 30; @@ -46,8 +45,9 @@ export class HttpClient implements Client { #client: hrana.HttpClient; /** @private */ - constructor(url: URL, authToken: string | undefined) { + constructor(url: URL, authToken: string | undefined, intMode: IntMode) { this.#client = hrana.openHttp(url, authToken); + this.#client.intMode = intMode; } async execute(stmt: InStatement): Promise { diff --git a/src/sqlite3.ts b/src/sqlite3.ts index 0f9cc99..146a125 100644 --- a/src/sqlite3.ts +++ b/src/sqlite3.ts @@ -2,7 +2,7 @@ import Database from "better-sqlite3"; import { Buffer } from "node:buffer"; import type { - Config, Client, Transaction, TransactionMode, + Config, IntMode, Client, Transaction, TransactionMode, ResultSet, Row, Value, InValue, InStatement, } from "./api.js"; import { LibsqlError } from "./api.js"; @@ -52,31 +52,33 @@ export function _createClient(config: ExpandedConfig): Client { const db = new Database(path, options); try { - executeStmt(db, "SELECT 1 AS checkThatTheDatabaseCanBeOpened"); + executeStmt(db, "SELECT 1 AS checkThatTheDatabaseCanBeOpened", config.intMode); } finally { db.close(); } - return new Sqlite3Client(path, options); + return new Sqlite3Client(path, options, config.intMode); } export class Sqlite3Client implements Client { - path: string; - options: Database.Options; + #path: string; + #options: Database.Options; + #intMode: IntMode; closed: boolean; /** @private */ - constructor(path: string, options: Database.Options) { - this.path = path; - this.options = options; + constructor(path: string, options: Database.Options, intMode: IntMode) { + this.#path = path; + this.#options = options; + this.#intMode = intMode; this.closed = false; } async execute(stmt: InStatement): Promise { this.#checkNotClosed(); - const db = new Database(this.path, this.options); + const db = new Database(this.#path, this.#options); try { - return executeStmt(db, stmt); + return executeStmt(db, stmt, this.#intMode); } finally { db.close(); } @@ -88,11 +90,11 @@ export class Sqlite3Client implements Client { const {mode, stmts} = extractBatchArgs(arg1, arg2); this.#checkNotClosed(); - const db = new Database(this.path, this.options); + const db = new Database(this.#path, this.#options); try { - executeStmt(db, transactionModeToBegin(mode)); - const resultSets = stmts.map(stmt => executeStmt(db, stmt)); - executeStmt(db, "COMMIT"); + executeStmt(db, transactionModeToBegin(mode), this.#intMode); + const resultSets = stmts.map(stmt => executeStmt(db, stmt, this.#intMode)); + executeStmt(db, "COMMIT", this.#intMode); return resultSets; } finally { db.close(); @@ -101,10 +103,10 @@ export class Sqlite3Client implements Client { async transaction(mode: TransactionMode = "write"): Promise { this.#checkNotClosed(); - const db = new Database(this.path, this.options); + const db = new Database(this.#path, this.#options); try { - executeStmt(db, transactionModeToBegin(mode)); - return new Sqlite3Transaction(db); + executeStmt(db, transactionModeToBegin(mode), this.#intMode); + return new Sqlite3Transaction(db, this.#intMode); } catch (e) { db.close(); throw e; @@ -113,7 +115,7 @@ export class Sqlite3Client implements Client { async executeMultiple(sql: string): Promise { this.#checkNotClosed(); - const db = new Database(this.path, this.options); + const db = new Database(this.#path, this.#options); try { return executeMultiple(db, sql); } finally { @@ -133,58 +135,60 @@ export class Sqlite3Client implements Client { } export class Sqlite3Transaction implements Transaction { - database: Database.Database + #database: Database.Database; + #intMode: IntMode; /** @private */ - constructor(database: Database.Database) { - this.database = database; + constructor(database: Database.Database, intMode: IntMode) { + this.#database = database; + this.#intMode = intMode; } async execute(stmt: InStatement): Promise { this.#checkNotClosed(); - return executeStmt(this.database, stmt); + return executeStmt(this.#database, stmt, this.#intMode); } async batch(stmts: Array): Promise> { this.#checkNotClosed(); - return stmts.map(stmt => executeStmt(this.database, stmt)); + return stmts.map(stmt => executeStmt(this.#database, stmt, this.#intMode)); } async executeMultiple(sql: string): Promise { this.#checkNotClosed(); - return executeMultiple(this.database, sql); + return executeMultiple(this.#database, sql); } async rollback(): Promise { - if (!this.database.open) { + if (!this.#database.open) { return; } - executeStmt(this.database, "ROLLBACK"); - this.database.close(); + executeStmt(this.#database, "ROLLBACK", this.#intMode); + this.#database.close(); } async commit(): Promise { this.#checkNotClosed(); - executeStmt(this.database, "COMMIT"); - this.database.close(); + executeStmt(this.#database, "COMMIT", this.#intMode); + this.#database.close(); } close(): void { - this.database.close(); + this.#database.close(); } get closed(): boolean { - return !this.database.open; + return !this.#database.open; } #checkNotClosed(): void { - if (!this.database.open) { + if (!this.#database.open) { throw new LibsqlError("The transaction is closed", "TRANSACTION_CLOSED"); } } } -function executeStmt(db: Database.Database, stmt: InStatement): ResultSet { +function executeStmt(db: Database.Database, stmt: InStatement, intMode: IntMode): ResultSet { let sql: string; let args: Array | Record; if (typeof stmt === "string") { @@ -206,6 +210,7 @@ function executeStmt(db: Database.Database, stmt: InStatement): ResultSet { try { const sqlStmt = db.prepare(sql); + sqlStmt.safeIntegers(true); let returnsData = true; try { @@ -217,7 +222,9 @@ function executeStmt(db: Database.Database, stmt: InStatement): ResultSet { if (returnsData) { const columns = Array.from(sqlStmt.columns().map(col => col.name)); - const rows = sqlStmt.all(args).map(sqlRow => rowFromSql(sqlRow as Array, columns)); + const rows = sqlStmt.all(args).map((sqlRow) => { + return rowFromSql(sqlRow as Array, columns, intMode); + }); // TODO: can we get this info from better-sqlite3? const rowsAffected = 0; const lastInsertRowid = undefined; @@ -233,12 +240,12 @@ function executeStmt(db: Database.Database, stmt: InStatement): ResultSet { } } -function rowFromSql(sqlRow: Array, columns: Array): Row { +function rowFromSql(sqlRow: Array, columns: Array, intMode: IntMode): Row { const row = {}; // make sure that the "length" property is not enumerable Object.defineProperty(row, "length", { value: sqlRow.length }); for (let i = 0; i < sqlRow.length; ++i) { - const value = valueFromSql(sqlRow[i]); + const value = valueFromSql(sqlRow[i], intMode); Object.defineProperty(row, i, { value }); const column = columns[i]; @@ -249,13 +256,31 @@ function rowFromSql(sqlRow: Array, columns: Array): Row { return row as Row; } -function valueFromSql(sqlValue: unknown): Value { - if (sqlValue instanceof Buffer) { +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" + ); + } + return Number(sqlValue); + } else if (intMode === "bigint") { + return sqlValue; + } else if (intMode === "string") { + return ""+sqlValue; + } else { + throw new Error("Invalid value for IntMode"); + } + } else if (sqlValue instanceof Buffer) { return sqlValue.buffer; } return sqlValue as Value; } +const minSafeBigint = -9007199254740991n; +const maxSafeBigint = 9007199254740991n; + function valueToSql(value: InValue): unknown { if (typeof value === "number") { if (!Number.isFinite(value)) { diff --git a/src/ws.ts b/src/ws.ts index cdce1a8..7a3506a 100644 --- a/src/ws.ts +++ b/src/ws.ts @@ -1,6 +1,6 @@ import * as hrana from "@libsql/hrana-client"; -import type { Config, Client, Transaction, ResultSet, InStatement } from "./api.js"; +import type { Config, IntMode, Client, Transaction, ResultSet, InStatement } from "./api.js"; import { TransactionMode, LibsqlError } from "./api.js"; import type { ExpandedConfig } from "./config.js"; import { expandConfig } from "./config.js"; @@ -53,7 +53,7 @@ export function _createClient(config: ExpandedConfig): WsClient { throw mapHranaError(e); } - return new WsClient(client, url, config.authToken); + return new WsClient(client, url, config.authToken, config.intMode); } // This object maintains state for a single WebSocket connection. @@ -85,6 +85,7 @@ const sqlCacheCapacity = 100; export class WsClient implements Client { #url: URL; #authToken: string | undefined; + #intMode: IntMode; // State of the current connection. The `hrana.WsClient` inside may be closed at any moment due to an // asynchronous error. #connState: ConnState; @@ -93,9 +94,10 @@ export class WsClient implements Client { closed: boolean; /** @private */ - constructor(client: hrana.WsClient, url: URL, authToken: string | undefined) { + constructor(client: hrana.WsClient, url: URL, authToken: string | undefined, intMode: IntMode) { this.#url = url; this.#authToken = authToken; + this.#intMode = intMode; this.#connState = this.#openConn(client); this.#futureConnState = undefined; this.closed = false; @@ -245,6 +247,7 @@ export class WsClient implements Client { } const stream = connState.client.openStream(); + stream.intMode = this.#intMode; const streamState = {conn: connState, stream}; connState.streamStates.add(streamState); return streamState;