API index and search · Build metadata
Source snapshot
packages/sql-client/src/value.ts
1import { protocolError, valueUnsupported } from "./errors.ts";2import type { IntMode, SqlArgs, SqlValue, Statement, WireArgs, WireSqlValue, WireStatement } from "./types.ts";34const I64_MIN = -(1n << 63n);5const I64_MAX = (1n << 63n) - 1n;6const DECIMAL_I64 = /^(?:0|-[1-9][0-9]*|[1-9][0-9]*)$/;78function binaryKind(value: unknown): string | null {9 if (typeof ArrayBuffer !== "undefined") {10 if (value instanceof ArrayBuffer) return "ArrayBuffer";11 if (ArrayBuffer.isView(value)) return value?.constructor?.name ?? "binary view";12 }13 if (typeof Blob !== "undefined" && value instanceof Blob) return "Blob";14 return null;15}1617function assertI64(value: bigint, source: string): void {18 if (value < I64_MIN || value > I64_MAX) {19 throw valueUnsupported(`${source} is outside SQLite's signed 64-bit integer range`);20 }21}2223/** Encode one public SDK value into the frozen v1 tagged JSON representation. */24export function encodeSqlValue(value: SqlValue): WireSqlValue {25 if (value === null || typeof value === "string") return value;26 if (typeof value === "boolean") return { $rindle: "i64", value: value ? "1" : "0" };27 if (typeof value === "bigint") {28 assertI64(value, `integer ${value.toString()}`);29 return { $rindle: "i64", value: value.toString() };30 }31 if (typeof value === "number") {32 if (Number.isNaN(value)) throw valueUnsupported("NaN is not a supported SQLite value");33 if (value === Infinity) return { $rindle: "float", value: "Infinity" };34 if (value === -Infinity) return { $rindle: "float", value: "-Infinity" };35 // JavaScript has one number type, but SQLite distinguishes INTEGER and REAL storage classes.36 // Safe integral numbers carry the exact i64 tag; non-integral and larger numeric values remain37 // REALs. Callers that need a larger integer must use bigint so intent and precision are explicit.38 if (Number.isSafeInteger(value)) {39 return { $rindle: "i64", value: BigInt(value).toString() };40 }41 return value;42 }4344 const kind = binaryKind(value);45 if (kind !== null) throw valueUnsupported(`${kind} bind values are not supported in Rindle SQL v1`);46 throw valueUnsupported(`unsupported bind value of type ${typeof value}`);47}4849function parseWireI64(decimal: unknown): bigint {50 if (typeof decimal !== "string" || !DECIMAL_I64.test(decimal)) {51 throw protocolError("server returned a malformed tagged i64 value");52 }53 let value: bigint;54 try {55 value = BigInt(decimal);56 } catch (cause) {57 throw protocolError("server returned a malformed tagged i64 value", cause);58 }59 if (value < I64_MIN || value > I64_MAX) {60 throw protocolError("server returned an out-of-range tagged i64 value");61 }62 return value;63}6465/** Decode one frozen v1 tagged JSON value using the client's integer policy. */66export function decodeSqlValue(value: unknown, intMode: IntMode = "bigint"): SqlValue {67 if (value === null || typeof value === "string") return value;68 if (typeof value === "number") {69 if (!Number.isFinite(value)) throw protocolError("server returned a non-finite untagged JSON number");70 return value;71 }72 if (typeof value !== "object" || Array.isArray(value)) {73 throw protocolError("server returned a malformed SQL value");74 }7576 const tagged = value as { $rindle?: unknown; value?: unknown };77 if (tagged.$rindle === "float") {78 if (tagged.value === "Infinity") return Infinity;79 if (tagged.value === "-Infinity") return -Infinity;80 throw protocolError("server returned a malformed tagged float value");81 }82 if (tagged.$rindle !== "i64") throw protocolError("server returned an unknown tagged SQL value");8384 const integer = parseWireI64(tagged.value);85 if (intMode === "bigint") return integer;86 if (intMode === "string") return integer.toString();87 const number = Number(integer);88 if (!Number.isSafeInteger(number)) {89 throw valueUnsupported(`integer ${integer.toString()} is outside Number's safe integer range`);90 }91 return number;92}9394export function encodeArgs(args: SqlArgs): WireArgs {95 if (Array.isArray(args)) return args.map((value) => encodeSqlValue(value));96 if (args === null || typeof args !== "object") {97 throw valueUnsupported("statement args must be a positional array or named object");98 }99 return Object.fromEntries(Object.entries(args).map(([name, value]) => [name, encodeSqlValue(value)]));100}101102export function encodeStatement(statement: Statement | string): WireStatement {103 const normalized: Statement = typeof statement === "string" ? { sql: statement } : statement;104 if (normalized === null || typeof normalized !== "object" || typeof normalized.sql !== "string") {105 throw new TypeError("statement must be a SQL string or an object with a string sql field");106 }107 if (normalized.sql.length === 0) throw new TypeError("statement sql must not be empty");108 const wire: WireStatement = { sql: normalized.sql };109 if (normalized.args !== undefined) wire.args = encodeArgs(normalized.args);110 if (normalized.wantRows !== undefined) wire.want_rows = normalized.wantRows;111 return wire;112}113