From ac71b638867cb6f4548b1b11df6083a04b6f4d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0pa=C4=8Dek?= Date: Mon, 10 Jul 2023 09:10:56 +0200 Subject: [PATCH] Add `ResultSet.toJSON()` --- src/__tests__/client.test.ts | 44 +++++++++++++++++++++++++++++++++++ src/api.ts | 6 +++++ src/hrana.ts | 15 ++++++------ src/sqlite3.ts | 6 ++--- src/util.ts | 45 +++++++++++++++++++++++++++++++++++- 5 files changed, 104 insertions(+), 12 deletions(-) diff --git a/src/__tests__/client.test.ts b/src/__tests__/client.test.ts index f9f75f4..ac915c3 100644 --- a/src/__tests__/client.test.ts +++ b/src/__tests__/client.test.ts @@ -269,6 +269,50 @@ describe("values", () => { })); }); +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": "12345", + }); + })); + + 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"]]); + })); + + test("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({ diff --git a/src/api.ts b/src/api.ts index 71effa6..eae40a9 100644 --- a/src/api.ts +++ b/src/api.ts @@ -355,6 +355,12 @@ export interface ResultSet { * table. */ lastInsertRowid: bigint | undefined; + + /** Converts the result set to JSON. + * + * This is used automatically by `JSON.stringify()`, but you can also call it explicitly. + */ + toJSON(): any; } /** Row returned from an SQL statement. diff --git a/src/hrana.ts b/src/hrana.ts index 01c1002..b97cabf 100644 --- a/src/hrana.ts +++ b/src/hrana.ts @@ -2,7 +2,7 @@ import * as hrana from "@libsql/hrana-client"; import type { InStatement, ResultSet, Transaction, TransactionMode } from "./api.js"; import { LibsqlError } from "./api.js"; import type { SqlCache } from "./sql_cache.js"; -import { transactionModeToBegin } from "./util.js"; +import { transactionModeToBegin, ResultSetImpl } from "./util.js"; export abstract class HranaTransaction implements Transaction { #mode: TransactionMode; @@ -316,13 +316,12 @@ export function stmtToHrana(stmt: InStatement): hrana.Stmt { } export function resultSetFromHrana(hranaRows: hrana.RowsResult): ResultSet { - return { - columns: hranaRows.columnNames.map(c => c ?? ""), - rows: hranaRows.rows, - rowsAffected: hranaRows.affectedRowCount, - lastInsertRowid: hranaRows.lastInsertRowid !== undefined - ? BigInt(hranaRows.lastInsertRowid) : undefined, - }; + const columns = hranaRows.columnNames.map(c => c ?? ""); + const rows = hranaRows.rows; + const rowsAffected = hranaRows.affectedRowCount; + const lastInsertRowid = hranaRows.lastInsertRowid !== undefined + ? BigInt(hranaRows.lastInsertRowid) : undefined; + return new ResultSetImpl(columns, rows, rowsAffected, lastInsertRowid); } export function mapHranaError(e: unknown): unknown { diff --git a/src/sqlite3.ts b/src/sqlite3.ts index 848e0b7..1293912 100644 --- a/src/sqlite3.ts +++ b/src/sqlite3.ts @@ -8,7 +8,7 @@ import type { import { LibsqlError } from "./api.js"; import type { ExpandedConfig } from "./config.js"; import { expandConfig } from "./config.js"; -import { supportedUrlLink, transactionModeToBegin } from "./util.js"; +import { supportedUrlLink, transactionModeToBegin, ResultSetImpl } from "./util.js"; export * from "./api.js"; @@ -226,12 +226,12 @@ function executeStmt(db: Database.Database, stmt: InStatement, intMode: IntMode) // TODO: can we get this info from better-sqlite3? const rowsAffected = 0; const lastInsertRowid = undefined; - return { columns, rows, rowsAffected, lastInsertRowid }; + return new ResultSetImpl(columns, rows, rowsAffected, lastInsertRowid); } else { const info = sqlStmt.run(args); const rowsAffected = info.changes; const lastInsertRowid = BigInt(info.lastInsertRowid); - return { columns: [], rows: [], rowsAffected, lastInsertRowid }; + return new ResultSetImpl([], [], rowsAffected, lastInsertRowid); } } catch (e) { throw mapSqliteError(e); diff --git a/src/util.ts b/src/util.ts index 542619f..521b35e 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,4 +1,5 @@ -import { TransactionMode, InStatement, LibsqlError } from "./api.js"; +import { Base64 } from "js-base64"; +import { ResultSet, Row, Value, TransactionMode, InStatement, LibsqlError } from "./api.js"; export const supportedUrlLink = "https://github.com/libsql/libsql-client-ts#supported-urls"; @@ -13,3 +14,45 @@ export function transactionModeToBegin(mode: TransactionMode): string { throw RangeError('Unknown transaction mode, supported values are "write", "read" and "deferred"'); } } + +export class ResultSetImpl implements ResultSet { + 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; + this.lastInsertRowid = lastInsertRowid; + } + + toJSON(): any { + return { + "columns": this.columns, + "rows": this.rows.map(rowToJson), + "rowsAffected": this.rowsAffected, + "lastInsertRowid": this.lastInsertRowid !== undefined ? ""+this.lastInsertRowid : null, + }; + } +} + +function rowToJson(row: Row): unknown { + return Array.prototype.map.call(row, valueToJson); +} + +function valueToJson(value: Value): unknown { + if (typeof value === "bigint") { + return ""+value; + } else if (value instanceof ArrayBuffer) { + return Base64.fromUint8Array(new Uint8Array(value)); + } else { + return value; + } +}