Skip to content

Commit

Permalink
Implement IntMode
Browse files Browse the repository at this point in the history
IntMode allows the user to select how we convert SQLite integers to
JavaScript values: we can return integers as numbers, bigints or as
strings.
  • Loading branch information
honzasp committed Jun 22, 2023
1 parent 56dce9e commit 789c5b9
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 111 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
54 changes: 27 additions & 27 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -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"
}
Expand Down
94 changes: 59 additions & 35 deletions src/__tests__/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>): () => Promise<void> {
function withClient(
f: (c: libsql.Client) => Promise<void>,
extraConfig: Partial<libsql.Config> = {},
): () => Promise<void> {
return async () => {
const c = createClient(config);
const c = createClient({...config, ...extraConfig});
try {
await f(c);
} finally {
Expand Down Expand Up @@ -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()", () => {
Expand Down Expand Up @@ -172,24 +180,63 @@ 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");
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", 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) {
Expand All @@ -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]});
Expand Down
14 changes: 14 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down
12 changes: 11 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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,
};
}
10 changes: 5 additions & 5 deletions src/http.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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",
);
}
Expand All @@ -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;
Expand All @@ -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<ResultSet> {
Expand Down
Loading

0 comments on commit 789c5b9

Please sign in to comment.