Rindle

API index and search · Build metadata

Source snapshot

packages/sql-client/src/drizzle.ts

Source revision 05d0bf2c2e56 · build details
Source revision: 05d0bf2c2e56.
TypeScript input SHA-256: aabe6cfcc4172b870d5e272142958e9ea8d8784c2aa23133156e5a7ee633318e
Generated 2026-09-04T23:58:25.590Z with TypeScript 6.0.3. Public TypeScript checks and declaration emit passed. Package runtime tests are separate.
1import { customType } from "drizzle-orm/sqlite-core";23import { createSqlClient } from "./client.ts";4import { RindleSqlError, valueUnsupported } from "./errors.ts";5import type {6  ClientOptions,7  SqlClient,8  SqlTransaction,9  SqlValue,10  Statement,11  StatementResult,12} from "./types.ts";1314/**15 * The exact-i64 column for Drizzle schemas (design 226 §7.2). Three promises,16 * lined up with the column contract: generated DDL says `BIGINT` (the opt-in17 * exact int64 declaration), bind/predicate values are JS `bigint`, and selected18 * values infer as JS `bigint`. Drizzle's own `integer()` remains the19 * number/date/boolean surface and still emits `INTEGER` (the safe-range f6420 * plane).21 *22 * Caveat (§7.2): decltype dispatch fires only for **direct column references**.23 * SQLite reports no decltype for expression results, so `max(big_id)` or24 * `big_id + 1` falls into the safe-number arm and is a typed25 * `VALUE_UNSUPPORTED` past the round-trip bound — never a rounded number. For26 * exact expression results, use the native client with `intMode: "bigint"`.27 */28export const rindleBigint = customType<{29  data: bigint;30  driverData: bigint;31}>({32  dataType: () => "BIGINT",33});3435/** The small, structural statement shape consumed by drizzle-orm/libsql. */36export interface DrizzleStatement {37  sql: string;38  args?: DrizzleArgs;39}4041export type DrizzleArgs = readonly unknown[] | Readonly<Record<string, unknown>>;42export type DrizzleInputStatement = string | DrizzleStatement | readonly [sql: string, args?: DrizzleArgs];43export type DrizzleTransactionMode = "write" | "read" | "deferred";4445// Booleans are a bind convenience only; SQLite result storage classes never decode to boolean.46// `bigint` appears ONLY in cells of a column whose decltype is exactly BIGINT/INT8 (the47// `rindleBigint` opt-in, design 226 §7.2); every other integer cell stays a safe number.48export type DrizzleValue = Exclude<SqlValue, boolean>;49export type DrizzleRow = DrizzleValue[] & Record<string, DrizzleValue>;5051export interface DrizzleResultSet {52  columns: string[];53  columnTypes: string[];54  rows: DrizzleRow[];55  rowsAffected: number;56  lastInsertRowid: bigint | undefined;57  toJSON(): unknown;58}5960export interface DrizzleTransaction {61  readonly closed: boolean;62  execute(statement: DrizzleInputStatement): Promise<DrizzleResultSet>;63  batch(statements: DrizzleInputStatement[]): Promise<DrizzleResultSet[]>;64  executeMultiple(sql: string): Promise<void>;65  commit(): Promise<void>;66  rollback(): Promise<void>;67  close(): void;68}6970export interface DrizzleClient {71  readonly protocol: string;72  readonly closed: boolean;73  execute(statement: DrizzleInputStatement, args?: DrizzleArgs): Promise<DrizzleResultSet>;74  batch(statements: DrizzleInputStatement[], mode?: DrizzleTransactionMode): Promise<DrizzleResultSet[]>;75  /** Present for structural Client typing; Drizzle's libSQL migrator is deliberately unsupported. */76  migrate(statements: DrizzleInputStatement[]): Promise<DrizzleResultSet[]>;77  transaction(mode?: DrizzleTransactionMode): Promise<DrizzleTransaction>;78  executeMultiple(sql: string): Promise<void>;79  /** Embedded-replica sync is deliberately unsupported. */80  sync(): Promise<never>;81  reconnect(): void;82  close(): void;83  /** Escape hatch for Rindle-specific operations and session-cursor persistence. */84  readonly rindle: SqlClient;85}8687function toNativeStatement(input: DrizzleInputStatement, args?: DrizzleArgs): Statement | string {88  if (typeof input === "string") return args === undefined ? input : { sql: input, args: args as Statement["args"] };89  if (Array.isArray(input)) return { sql: input[0], args: input[1] as Statement["args"] };90  return {91    sql: (input as DrizzleStatement).sql,92    // The native codec performs the deliberate runtime refusal for binary/unsupported values.93    args: (input as DrizzleStatement).args as Statement["args"],94  };95}9697function defineNamedCell(row: DrizzleRow, name: string, value: DrizzleValue): void {98  // Array length and numeric aliases overlap the positional row representation; positions win.99  const numericIndex = Number(name);100  if (101    name === "length" ||102    (Number.isInteger(numericIndex) && numericIndex >= 0 && numericIndex < 0xffff_ffff && String(numericIndex) === name)103  ) {104    return;105  }106  try {107    Object.defineProperty(row, name, { value, writable: true, enumerable: true, configurable: true });108  } catch {109    // An exotic/non-configurable property name cannot be represented in the hybrid row.110  }111}112113/** The exact whole-declaration match (design 226 §4.1): `BIGINT`/`INT8` and nothing114 *  else — `UNSIGNED BIGINT` and every decorated spelling keeps its safe-number115 *  INTEGER-affinity meaning, mirroring the daemon's `value_type_of`. */116function isInt64Decltype(decltype: string | null | undefined): boolean {117  if (!decltype) return false;118  const t = decltype.trim().toUpperCase();119  return t === "BIGINT" || t === "INT8";120}121122function toDrizzleValue(value: SqlValue, int64Column: boolean): DrizzleValue {123  if (typeof value === "bigint") {124    // A BIGINT/INT8-declared result column preserves the exact value as JS bigint125    // (the `rindleBigint` lane, design 226 §7.2). This dispatch must be per-column:126    // globally returning bigint would break Drizzle's integer/timestamp mappers,127    // globally returning number would defeat `rindleBigint`.128    if (int64Column) return value;129    const number = Number(value);130    if (!Number.isSafeInteger(number)) {131      throw valueUnsupported(`integer ${value.toString()} is outside Number's safe integer range required by Drizzle`);132    }133    return number;134  }135  if (int64Column && value !== null) {136    // Under the facade's lossless-bigint contract every non-NULL INTEGER cell arrives137    // as bigint, so a number or string here is a REAL/TEXT physical cell — SQLite138    // affinity permits them and schema admission never scans rows. Passing it through139    // would hand a `rindleBigint` field (statically `bigint`) the wrong runtime type;140    // §7.2's contract is a typed error, never a mistyped value.141    throw valueUnsupported(142      `BIGINT/INT8-declared column holds a ${typeof value} cell; exact int64 columns require INTEGER storage`,143    );144  }145  // The native decoder never produces booleans, but normalize a structurally supplied result too.146  if (typeof value === "boolean") return value ? 1 : 0;147  return value;148}149150/** Convert Rindle's lossless positional result into libSQL's positional + named hybrid rows. */151export function toDrizzleResultSet(result: StatementResult): DrizzleResultSet {152  const columns = result.columns.map((column) => column.name);153  const columnTypes = result.columns.map((column) => column.decltype ?? "");154  const int64Columns = result.columns.map((column) => isInt64Decltype(column.decltype));155  const rows = result.rows.map((cells) => {156    const row = cells.map((cell, index) => toDrizzleValue(cell, int64Columns[index] === true)) as DrizzleRow;157    for (let index = 0; index < columns.length; index += 1) {158      defineNamedCell(row, columns[index]!, row[index]!);159    }160    return row;161  });162  const converted: DrizzleResultSet = {163    columns,164    columnTypes,165    rows,166    rowsAffected: result.rowsAffected,167    lastInsertRowid: result.lastInsertRowid === null ? undefined : BigInt(result.lastInsertRowid),168    toJSON() {169      return {170        columns: this.columns,171        columnTypes: this.columnTypes,172        rows: this.rows,173        rowsAffected: this.rowsAffected,174        lastInsertRowid: this.lastInsertRowid?.toString(),175      };176    },177  };178  return converted;179}180181class TransactionFacade implements DrizzleTransaction {182  private isClosed = false;183  private readonly tx: SqlTransaction;184185  constructor(tx: SqlTransaction) {186    this.tx = tx;187  }188189  get closed(): boolean {190    return this.isClosed;191  }192193  async execute(statement: DrizzleInputStatement): Promise<DrizzleResultSet> {194    return toDrizzleResultSet(await this.tx.execute(toNativeStatement(statement)));195  }196197  async batch(statements: DrizzleInputStatement[]): Promise<DrizzleResultSet[]> {198    const native = statements.map((statement) => {199      const converted = toNativeStatement(statement);200      return typeof converted === "string" ? { sql: converted } : converted;201    });202    return (await this.tx.batch(native)).map(toDrizzleResultSet);203  }204205  async executeMultiple(_sql: string): Promise<void> {206    throw new RindleSqlError({207      code: "STATEMENT_UNSUPPORTED",208      message: "executeMultiple is not supported inside a Rindle interactive transaction; use batch()",209    });210  }211212  async commit(): Promise<void> {213    if (this.isClosed) return;214    await this.tx.commit();215    this.isClosed = true;216  }217218  async rollback(): Promise<void> {219    if (this.isClosed) return;220    await this.tx.rollback();221    this.isClosed = true;222  }223224  close(): void {225    if (this.isClosed) return;226    this.isClosed = true;227    void this.tx.rollback().catch(() => {});228  }229}230231class ClientFacade implements DrizzleClient {232  readonly rindle: SqlClient;233  readonly protocol = "http";234  private isClosed = false;235236  constructor(rindle: SqlClient) {237    this.rindle = rindle;238  }239240  get closed(): boolean {241    return this.isClosed;242  }243244  async execute(statement: DrizzleInputStatement, args?: DrizzleArgs): Promise<DrizzleResultSet> {245    return toDrizzleResultSet((await this.rindle.execute(toNativeStatement(statement, args))).result);246  }247248  async batch(statements: DrizzleInputStatement[], _mode?: DrizzleTransactionMode): Promise<DrizzleResultSet[]> {249    const native = statements.map((statement) => {250      const converted = toNativeStatement(statement);251      return typeof converted === "string" ? { sql: converted } : converted;252    });253    return (await this.rindle.batch(native)).results.map(toDrizzleResultSet);254  }255256  async transaction(mode: DrizzleTransactionMode = "write"): Promise<DrizzleTransaction> {257    if (mode !== "write" && mode !== "read" && mode !== "deferred") {258      throw new TypeError(`unsupported transaction mode: ${String(mode)}`);259    }260    return new TransactionFacade(await this.rindle.begin({ readOnly: mode === "read" }));261  }262263  executeMultiple(sql: string): Promise<void> {264    return this.rindle.executeMultiple(sql);265  }266267  async migrate(_statements: DrizzleInputStatement[]): Promise<DrizzleResultSet[]> {268    throw new RindleSqlError({269      code: "MIGRATOR_UNSUPPORTED",270      message: "drizzle-orm/libsql/migrator is not supported; apply declared migrations with SqlClient.migrate()",271    });272  }273274  async sync(): Promise<never> {275    throw new RindleSqlError({276      code: "SYNC_UNSUPPORTED",277      message: "embedded-replica sync is not supported by Rindle SQL",278    });279  }280281  reconnect(): void {282    throw new RindleSqlError({283      code: "RECONNECT_UNSUPPORTED",284      message: "a closed Rindle SQL client cannot be reopened; create a new client",285    });286  }287288  close(): void {289    if (this.isClosed) return;290    this.isClosed = true;291    this.rindle.close();292  }293}294295/** Create the structural libSQL-client facade used by drizzle-orm/libsql. */296export function createDrizzleClient(options: ClientOptions | SqlClient): DrizzleClient {297  // The internal client is LOSSLESS (`intMode: "bigint"`) so precision is not298  // discarded before result metadata is considered (design 226 §7.2):299  // toDrizzleResultSet then dispatches per column decltype — BIGINT/INT8 keeps300  // the exact bigint (the `rindleBigint` lane); every other column converts to301  // the safe number Drizzle's integer/timestamp mappers consume, with an unsafe302  // integer a typed VALUE_UNSUPPORTED, never a rounded number. An INJECTED303  // structural client used with `rindleBigint` must likewise expose lossless304  // bigint results, or exact values are lost before this facade sees them.305  const client = isSqlClient(options) ? options : createSqlClient({ ...options, intMode: "bigint" });306  return new ClientFacade(client);307}308309function isSqlClient(value: ClientOptions | SqlClient): value is SqlClient {310  const candidate = value as Partial<SqlClient>;311  return typeof candidate.execute === "function" && typeof candidate.close === "function";312}313