API index and search · Build metadata
Source snapshot
packages/sql-client/src/drizzle.ts
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