turbine-orm 0.19.1 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +156 -24
  2. package/dist/adapters/cockroachdb.js +4 -9
  3. package/dist/cjs/adapters/cockroachdb.js +4 -9
  4. package/dist/cjs/cli/index.js +23 -12
  5. package/dist/cjs/cli/observe-ui.js +12 -2
  6. package/dist/cjs/cli/observe.js +6 -7
  7. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  8. package/dist/cjs/cli/studio.js +6 -37
  9. package/dist/cjs/client.js +45 -33
  10. package/dist/cjs/dialect.js +116 -0
  11. package/dist/cjs/errors.js +19 -1
  12. package/dist/cjs/generate.js +4 -0
  13. package/dist/cjs/index.js +3 -2
  14. package/dist/cjs/introspect.js +20 -0
  15. package/dist/cjs/mssql.js +1338 -0
  16. package/dist/cjs/mysql.js +1052 -0
  17. package/dist/cjs/query/builder.js +408 -122
  18. package/dist/cjs/query/utils.js +1 -0
  19. package/dist/cjs/sqlite.js +849 -0
  20. package/dist/cjs/typed-sql.js +9 -7
  21. package/dist/cli/index.js +23 -12
  22. package/dist/cli/migrate.d.ts +2 -2
  23. package/dist/cli/observe-ui.d.ts +1 -1
  24. package/dist/cli/observe-ui.js +12 -2
  25. package/dist/cli/observe.js +7 -8
  26. package/dist/cli/studio-ui.generated.js +1 -1
  27. package/dist/cli/studio.d.ts +0 -10
  28. package/dist/cli/studio.js +7 -37
  29. package/dist/client.d.ts +35 -14
  30. package/dist/client.js +46 -34
  31. package/dist/dialect.d.ts +258 -1
  32. package/dist/dialect.js +83 -0
  33. package/dist/errors.d.ts +12 -0
  34. package/dist/errors.js +17 -0
  35. package/dist/generate.js +4 -0
  36. package/dist/index.d.ts +4 -4
  37. package/dist/index.js +1 -1
  38. package/dist/introspect.d.ts +18 -0
  39. package/dist/introspect.js +19 -0
  40. package/dist/mssql.d.ts +233 -0
  41. package/dist/mssql.js +1298 -0
  42. package/dist/mysql.d.ts +174 -0
  43. package/dist/mysql.js +1012 -0
  44. package/dist/query/builder.d.ts +105 -6
  45. package/dist/query/builder.js +409 -123
  46. package/dist/query/index.d.ts +2 -2
  47. package/dist/query/types.d.ts +62 -12
  48. package/dist/query/utils.js +1 -0
  49. package/dist/sqlite.d.ts +144 -0
  50. package/dist/sqlite.js +842 -0
  51. package/dist/typed-sql.d.ts +7 -5
  52. package/dist/typed-sql.js +9 -7
  53. package/package.json +32 -3
@@ -0,0 +1,849 @@
1
+ "use strict";
2
+ /**
3
+ * turbine-orm/sqlite — zero-dependency SQLite engine
4
+ *
5
+ * Binds Turbine to SQLite via Node's built-in `node:sqlite` driver
6
+ * (`DatabaseSync`), so SQLite is a **zero new dependency** engine: the root
7
+ * package's runtime dependency stays exactly `pg`. This is the in-process
8
+ * test / edge / "try it in 10 seconds" engine — `:memory:` databases run
9
+ * entirely in-process with no service container.
10
+ *
11
+ * ## Driver
12
+ *
13
+ * - **Primary:** `node:sqlite` `DatabaseSync` (Node ≥ 22.5, experimental). Emits
14
+ * an `ExperimentalWarning` — harmless. No native build, no extra dependency.
15
+ * - **Fallback:** `better-sqlite3` for Node < 22.5. Not bundled and not required;
16
+ * wrap a `better-sqlite3` handle in the same `PgCompatPool` shape if needed.
17
+ *
18
+ * ## Capabilities & limits (vs PostgreSQL)
19
+ *
20
+ * - `RETURNING` + `ON CONFLICT … DO UPDATE` (SQLite ≥ 3.35) → create / upsert /
21
+ * update / delete return real rows in a single statement (`resultStrategy =
22
+ * 'returning'`, same as Postgres).
23
+ * - **Single-writer:** one connection / one write transaction at a time;
24
+ * concurrent writers get `SQLITE_BUSY` (treated as retryable). `journal_mode =
25
+ * WAL` is enabled for file databases to allow concurrent readers.
26
+ * - **Unsupported (throw `UnsupportedFeatureError`):** pgvector distance ops,
27
+ * LISTEN/NOTIFY (`$listen` / `$notify`), RLS `sessionContext`. Advisory-lock
28
+ * migration locking is unavailable — SQLite is single-writer, so migrations
29
+ * serialize naturally.
30
+ * - **Type affinity caveats:** SQLite has no native `BOOLEAN` (0/1 integers) or
31
+ * `DATE` (TEXT/INTEGER). Booleans bind as 1/0; `Date` values bind as ISO-8601
32
+ * text; columns declared `TIMESTAMP`/`DATETIME`/`DATE` are coerced back to
33
+ * `Date`. Integers wider than `Number.MAX_SAFE_INTEGER` come back as strings
34
+ * (the same safe-int policy Turbine uses for Postgres `int8`).
35
+ * - **Case-insensitive matching** uses `COLLATE NOCASE`, which is **ASCII-only**
36
+ * (no Unicode case folding).
37
+ *
38
+ * ## Example — `:memory:` database
39
+ *
40
+ * ```ts
41
+ * import { turbineSqlite } from 'turbine-orm/sqlite';
42
+ * import { SCHEMA } from './generated/turbine/metadata.js';
43
+ *
44
+ * const db = turbineSqlite(':memory:', SCHEMA);
45
+ * const users = await db.users.findMany({ with: { posts: true }, limit: 10 });
46
+ * await db.disconnect();
47
+ * ```
48
+ */
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.sqliteDialect = exports.SqlitePool = void 0;
51
+ exports.sqliteTypeToTs = sqliteTypeToTs;
52
+ exports.introspectSqliteDatabase = introspectSqliteDatabase;
53
+ exports.introspectSqlite = introspectSqlite;
54
+ exports.turbineSqlite = turbineSqlite;
55
+ const node_module_1 = require("node:module");
56
+ const client_js_1 = require("./client.js");
57
+ const dialect_js_1 = require("./dialect.js");
58
+ const errors_js_1 = require("./errors.js");
59
+ const schema_js_1 = require("./schema.js");
60
+ let cachedDatabaseSync;
61
+ /**
62
+ * Lazily load `node:sqlite`'s `DatabaseSync` constructor.
63
+ *
64
+ * `node:sqlite` is a built-in only on Node >= 22.5, so importing it at module
65
+ * top-level would make `import 'turbine-orm/sqlite'` — and any module that
66
+ * merely re-exports `sqliteDialect` (e.g. the dialect test suite) — throw
67
+ * `ERR_UNKNOWN_BUILTIN_MODULE` on Node 20. Deferring the require to the moment a
68
+ * connection is actually opened keeps the dialect (pure SQL generation) usable
69
+ * everywhere and scopes the Node-version requirement to `turbineSqlite()`.
70
+ *
71
+ * `createRequire` is anchored on `process.cwd()` so this works identically in
72
+ * the ESM and CJS builds (no `import.meta` / `__filename`, which differ between
73
+ * them); built-in module resolution ignores the anchor entirely.
74
+ */
75
+ function loadDatabaseSync() {
76
+ if (cachedDatabaseSync)
77
+ return cachedDatabaseSync;
78
+ let ctor;
79
+ try {
80
+ const req = (0, node_module_1.createRequire)(process.cwd());
81
+ ctor = req('node:sqlite').DatabaseSync;
82
+ }
83
+ catch (err) {
84
+ throw new errors_js_1.ConnectionError("[turbine] turbine-orm/sqlite requires Node's built-in 'node:sqlite' module (Node >= 22.5). " +
85
+ `Upgrade Node to >= 22.5, or pass an already-open better-sqlite3-compatible handle. (${err.message})`);
86
+ }
87
+ if (typeof ctor !== 'function') {
88
+ throw new errors_js_1.ConnectionError("[turbine] 'node:sqlite' loaded but did not export a DatabaseSync constructor — this Node build may lack SQLite support.");
89
+ }
90
+ cachedDatabaseSync = ctor;
91
+ return cachedDatabaseSync;
92
+ }
93
+ /**
94
+ * Coerce an arbitrary JS value into something `node:sqlite` can bind.
95
+ *
96
+ * `node:sqlite` throws on `boolean`, `undefined`, `Date`, and plain objects, so
97
+ * we normalize: booleans → 1/0, `undefined`/`null` → NULL, `Date` → ISO text,
98
+ * `Uint8Array`/`Buffer` → BLOB, and any remaining object/array → JSON text
99
+ * (matches how Turbine already pre-serializes JSON filter params).
100
+ */
101
+ function toSqliteParam(value) {
102
+ if (value === undefined || value === null)
103
+ return null;
104
+ switch (typeof value) {
105
+ case 'boolean':
106
+ return value ? 1 : 0;
107
+ case 'number':
108
+ case 'bigint':
109
+ case 'string':
110
+ return value;
111
+ }
112
+ if (value instanceof Date)
113
+ return value.toISOString();
114
+ if (value instanceof Uint8Array)
115
+ return value; // also covers Buffer
116
+ return JSON.stringify(value);
117
+ }
118
+ /**
119
+ * Normalize a single column value read from SQLite. With `setReadBigInts(true)`
120
+ * every integer column comes back as a `bigint`; apply the same safe-integer
121
+ * policy Turbine uses for Postgres `int8` — number when it fits in a JS safe
122
+ * integer, otherwise the decimal string to avoid precision loss. Never mutates
123
+ * any global parser state (the policy lives entirely in this shim).
124
+ */
125
+ function normalizeValue(value) {
126
+ if (typeof value === 'bigint') {
127
+ const asNumber = Number(value);
128
+ return Number.isSafeInteger(asNumber) ? asNumber : value.toString();
129
+ }
130
+ return value;
131
+ }
132
+ /** Convert a `node:sqlite` null-prototype row into a normalized plain object. */
133
+ function normalizeRow(row) {
134
+ const out = {};
135
+ for (const key of Object.keys(row)) {
136
+ out[key] = normalizeValue(row[key]);
137
+ }
138
+ return out;
139
+ }
140
+ // ---------------------------------------------------------------------------
141
+ // Statement classification + error translation
142
+ // ---------------------------------------------------------------------------
143
+ /** Statements whose first keyword yields a result set. */
144
+ const ROW_RETURNING_HEAD = /^\s*(?:select|with|pragma|explain|values)\b/i;
145
+ /**
146
+ * Decide whether a statement produces rows (use `.all()`) or only a change
147
+ * count (use `.run()` to capture `changes` for updateMany/deleteMany).
148
+ * A `RETURNING` clause turns any write into a row-producing statement.
149
+ */
150
+ function statementReturnsRows(sql) {
151
+ return ROW_RETURNING_HEAD.test(sql) || /\breturning\b/i.test(sql);
152
+ }
153
+ /**
154
+ * Augment a `node:sqlite` error with the Postgres-shaped fields that
155
+ * `wrapPgError` understands, so SQLite constraint failures surface as the same
156
+ * typed Turbine errors as Postgres (E008/E009/E010/E011) and `SQLITE_BUSY`
157
+ * becomes a retryable error. The original error (with its real SQLite message)
158
+ * is preserved as the `cause`. Returns the value unchanged when it is not a
159
+ * recognizable SQLite error.
160
+ *
161
+ * `wrapPgError` is invoked downstream (in the query executor and the
162
+ * transaction proxy), so we only annotate here — we never throw a `new`
163
+ * Turbine error from the driver itself.
164
+ */
165
+ function augmentSqliteError(err) {
166
+ if (!err || typeof err !== 'object')
167
+ return err;
168
+ const e = err;
169
+ if (typeof e.errcode !== 'number')
170
+ return err;
171
+ const target = e;
172
+ const message = e.message ?? '';
173
+ // Primary SQLite result code = errcode & 0xff; the rest are extended codes.
174
+ const primary = e.errcode & 0xff;
175
+ switch (e.errcode) {
176
+ // SQLITE_CONSTRAINT_UNIQUE (2067) / SQLITE_CONSTRAINT_PRIMARYKEY (1555)
177
+ case 2067:
178
+ case 1555: {
179
+ // "UNIQUE constraint failed: tbl.col[, tbl.col2]"
180
+ const m = /constraint failed:\s*(.+)$/i.exec(message);
181
+ const pairs = m ? m[1].split(',').map((s) => s.trim()) : [];
182
+ const cols = pairs.map((p) => p.split('.').pop() ?? p);
183
+ target.code = '23505';
184
+ target.table = pairs[0]?.split('.')[0];
185
+ target.detail = `Key (${cols.join(', ')})=() already exists.`;
186
+ return err;
187
+ }
188
+ // SQLITE_CONSTRAINT_FOREIGNKEY (787)
189
+ case 787:
190
+ target.code = '23503';
191
+ return err;
192
+ // SQLITE_CONSTRAINT_NOTNULL (1299)
193
+ case 1299: {
194
+ const m = /constraint failed:\s*([^.\s]+)\.(\S+)/i.exec(message);
195
+ target.code = '23502';
196
+ if (m) {
197
+ target.table = m[1];
198
+ target.column = m[2];
199
+ }
200
+ return err;
201
+ }
202
+ // SQLITE_CONSTRAINT_CHECK (275)
203
+ case 275:
204
+ target.code = '23514';
205
+ return err;
206
+ default:
207
+ // SQLITE_BUSY (5) / SQLITE_LOCKED (6) / SQLITE_BUSY_SNAPSHOT (261) etc.
208
+ if (primary === 5 || primary === 6) {
209
+ // Map to serialization_failure so withRetry()/$retry() retry it.
210
+ target.code = '40001';
211
+ }
212
+ return err;
213
+ }
214
+ }
215
+ function normalizeQueryArgs(arg, values) {
216
+ if (typeof arg === 'string')
217
+ return { text: arg, params: values ?? [] };
218
+ return { text: arg.text, params: arg.values ?? values ?? [] };
219
+ }
220
+ /**
221
+ * Execute one statement against a `DatabaseSync` handle and shape the result
222
+ * like a `pg.QueryResult` (`{ rows, rowCount }`). Row-returning statements use
223
+ * `.all()`; pure writes use `.run()` so `changes` populates `rowCount` (which
224
+ * `updateMany` / `deleteMany` read). `lastID` mirrors mysql2's insert-id for
225
+ * parity with the `reselect` strategy (unused by SQLite's `returning` path).
226
+ */
227
+ /**
228
+ * Bind the positional `params[]` (in 1-indexed generation order) to the named
229
+ * `:p1`, `:p2`, … placeholders the dialect emits. Mapping by NAME makes binding
230
+ * independent of where each placeholder lands in the SQL text — the same
231
+ * guarantee Postgres' numbered `$N` gives. Returns `undefined` when there are no
232
+ * params so parameter-less statements (BEGIN/COMMIT/DDL) bind nothing.
233
+ */
234
+ function toNamedBinding(values) {
235
+ if (values.length === 0)
236
+ return undefined;
237
+ const named = {};
238
+ for (let i = 0; i < values.length; i++) {
239
+ named[`p${i + 1}`] = toSqliteParam(values[i]);
240
+ }
241
+ return named;
242
+ }
243
+ function runStatement(db, sql, values) {
244
+ const binding = toNamedBinding(values);
245
+ let stmt;
246
+ try {
247
+ stmt = db.prepare(sql);
248
+ }
249
+ catch (err) {
250
+ throw augmentSqliteError(err);
251
+ }
252
+ stmt.setReadBigInts(true);
253
+ try {
254
+ if (statementReturnsRows(sql)) {
255
+ const rows = (binding ? stmt.all(binding) : stmt.all()).map(normalizeRow);
256
+ return { rows, rowCount: rows.length };
257
+ }
258
+ const info = binding ? stmt.run(binding) : stmt.run();
259
+ const changes = typeof info.changes === 'bigint' ? Number(info.changes) : info.changes;
260
+ const lastID = typeof info.lastInsertRowid === 'bigint' ? Number(info.lastInsertRowid) : info.lastInsertRowid;
261
+ return { rows: [], rowCount: changes, lastID, insertId: lastID };
262
+ }
263
+ catch (err) {
264
+ throw augmentSqliteError(err);
265
+ }
266
+ }
267
+ /**
268
+ * A `PgCompatPool` backed by a single `node:sqlite` `DatabaseSync` connection.
269
+ * SQLite is single-connection by nature (a `:memory:` database is per-handle),
270
+ * so `connect()` hands back a client over the **same** handle — transactions
271
+ * (`BEGIN`/`COMMIT`/`ROLLBACK`, `SAVEPOINT` nesting) just run on it. Queries are
272
+ * serialized; this is the documented single-writer model.
273
+ */
274
+ class SqlitePool {
275
+ /** The underlying `node:sqlite` handle — exposed as an escape hatch (seed/DDL). */
276
+ db;
277
+ closed = false;
278
+ constructor(db) {
279
+ this.db = db;
280
+ }
281
+ // biome-ignore lint/suspicious/noExplicitAny: pg-compat query is generic over the row shape; runStatement returns plain objects.
282
+ async query(text, values) {
283
+ const { text: sql, params } = normalizeQueryArgs(text, values);
284
+ return runStatement(this.db, sql, params);
285
+ }
286
+ async connect() {
287
+ const db = this.db;
288
+ return {
289
+ // biome-ignore lint/suspicious/noExplicitAny: see query() above.
290
+ query: async (text, values) => {
291
+ const { text: sql, params } = normalizeQueryArgs(text, values);
292
+ return runStatement(db, sql, params);
293
+ },
294
+ release: () => {
295
+ // Single shared connection — nothing to return to a pool.
296
+ },
297
+ };
298
+ }
299
+ async end() {
300
+ if (this.closed)
301
+ return;
302
+ this.closed = true;
303
+ this.db.close();
304
+ }
305
+ }
306
+ exports.SqlitePool = SqlitePool;
307
+ // ---------------------------------------------------------------------------
308
+ // Type mapping (SQLite declared type → TypeScript)
309
+ // ---------------------------------------------------------------------------
310
+ /**
311
+ * Map a SQLite declared column type (type affinity) to a TypeScript type.
312
+ * SQLite is dynamically typed; we read the declared `PRAGMA table_info` type.
313
+ */
314
+ function sqliteTypeToTs(declaredType, nullable) {
315
+ const t = declaredType.toLowerCase();
316
+ let base;
317
+ if (/int/.test(t))
318
+ base = 'number';
319
+ else if (/(real|floa|doub)/.test(t))
320
+ base = 'number';
321
+ else if (/bool/.test(t))
322
+ base = 'boolean';
323
+ else if (/(timestamp|datetime|date)/.test(t))
324
+ base = 'Date';
325
+ else if (/(char|clob|text)/.test(t))
326
+ base = 'string';
327
+ else if (/blob/.test(t))
328
+ base = 'Uint8Array';
329
+ else if (/(numeric|decimal)/.test(t))
330
+ base = 'string';
331
+ else if (t === '')
332
+ base = 'unknown'; // no declared affinity (BLOB affinity)
333
+ else
334
+ base = 'unknown';
335
+ return nullable ? `${base} | null` : base;
336
+ }
337
+ /** Is a SQLite declared type a date/time type (so values coerce back to `Date`)? */
338
+ function isSqliteDateType(declaredType) {
339
+ const t = declaredType.toLowerCase();
340
+ return (0, schema_js_1.isDateType)(t) || /(timestamp|datetime|date)/.test(t);
341
+ }
342
+ /** Map a schema-builder (Postgres-flavored) column type to a SQLite affinity. */
343
+ function sqliteColumnAffinity(type) {
344
+ const t = type.toUpperCase();
345
+ if (/SERIAL|INT/.test(t))
346
+ return 'INTEGER';
347
+ if (/REAL|FLOAT|DOUBLE/.test(t))
348
+ return 'REAL';
349
+ if (/NUMERIC|DECIMAL|MONEY/.test(t))
350
+ return 'NUMERIC';
351
+ if (/BLOB|BYTEA/.test(t))
352
+ return 'BLOB';
353
+ if (/BOOL/.test(t))
354
+ return 'INTEGER';
355
+ // TEXT, VARCHAR, CHAR, UUID, JSON(B), TIMESTAMP(TZ), DATE, TIME, ENUM, …
356
+ return 'TEXT';
357
+ }
358
+ // ---------------------------------------------------------------------------
359
+ // sqliteDialect — the full Dialect contract for SQLite
360
+ // ---------------------------------------------------------------------------
361
+ /**
362
+ * SQLite implementation of the {@link Dialect} contract. Standardizes on `"…"`
363
+ * identifier quoting and positional `?` placeholders, uses `json_object` /
364
+ * `json_group_array` for the single-query nested-relation engine (with the
365
+ * critical `json(...)` subresult wrap so nested objects are real JSON, not
366
+ * SQLite's strings-of-strings double-encoding), keeps `RETURNING` + `ON CONFLICT`
367
+ * (SQLite ≥ 3.35), and disables the Postgres-only capabilities (vector,
368
+ * LISTEN/NOTIFY, RLS, advisory locks).
369
+ */
370
+ exports.sqliteDialect = {
371
+ ...dialect_js_1.postgresDialect,
372
+ name: 'sqlite',
373
+ resultStrategy: 'returning', // SQLite ≥ 3.35 has RETURNING
374
+ supportsReturning: true,
375
+ supportsILike: false,
376
+ supportsVector: false,
377
+ supportsListenNotify: false,
378
+ supportsRLS: false,
379
+ supportsAdvisoryLock: false,
380
+ // json_group_array / json_object have no inline ORDER BY argument, so every
381
+ // ordered to-many relation is forced through the inner-subquery rewrite.
382
+ aggSupportsInlineOrderBy: false,
383
+ jsonPathSupport: 'function',
384
+ emptyJsonArrayLiteral: "json('[]')",
385
+ nullJsonLiteral: 'NULL',
386
+ // Named placeholders (`:p1`, `:p2`, …) — NOT positional `?`. Turbine pushes
387
+ // params in 1-indexed generation order but may EMIT them in a different SQL
388
+ // text position (e.g. a `with`-relation LIMIT lands in the SELECT list, ahead
389
+ // of the outer WHERE). Postgres reconciles this via numbered `$N`; positional
390
+ // `?` cannot. SQLite named parameters bind by name, so `:pN` ↔ `params[N-1]`
391
+ // exactly mirrors Postgres' `$N` semantics regardless of text order. The
392
+ // driver binds via a `{ p1, p2, … }` object built from the positional array.
393
+ paramPlaceholder(index) {
394
+ return `:p${index}`;
395
+ },
396
+ quoteIdentifier(name) {
397
+ return `"${name.replace(/"/g, '""')}"`;
398
+ },
399
+ buildJsonObject(pairs) {
400
+ const args = pairs.map(([key, expr]) => `'${this.escapeStringLiteral(key)}', ${expr}`);
401
+ return `json_object(${args.join(', ')})`;
402
+ },
403
+ buildJsonArrayAgg(jsonObjectExpr, orderBy) {
404
+ // json() wraps each aggregated object so SQLite stores real JSON, not an
405
+ // escaped string; COALESCE handles the empty-group NULL.
406
+ const suffix = orderBy ? ` ${orderBy}` : '';
407
+ return `COALESCE(json_group_array(json(${jsonObjectExpr}))${suffix}, json('[]'))`;
408
+ },
409
+ /**
410
+ * SQLite's `json_group_array` double-encodes nested objects unless each nested
411
+ * subresult is `json(...)`-wrapped. This is the whole point of the hook: it
412
+ * keeps `users[0].posts[0].author.name` a real parsed tree instead of a string
413
+ * containing a string containing JSON.
414
+ */
415
+ wrapJsonSubresult(subquery, fallback) {
416
+ return `COALESCE(json((${subquery})), ${fallback})`;
417
+ },
418
+ castAggregate(expr, target) {
419
+ return `CAST(${expr} AS ${target === 'int' ? 'INTEGER' : 'REAL'})`;
420
+ },
421
+ // No `= ANY(array)` in SQLite. `json_each` expands a single JSON-array param
422
+ // into a row set, keeping ONE placeholder (so the SQL cache stays valid
423
+ // regardless of list length) and handling the empty-list case correctly.
424
+ buildInClause(expr, paramRef, negated) {
425
+ return `${expr} ${negated ? 'NOT IN' : 'IN'} (SELECT value FROM json_each(${paramRef}))`;
426
+ },
427
+ inClauseParam(values) {
428
+ return JSON.stringify(values ?? []);
429
+ },
430
+ buildReturningClause(selection = '*') {
431
+ return ` RETURNING ${selection}`;
432
+ },
433
+ buildInsertStatement(input) {
434
+ return (`INSERT INTO ${input.table} (${input.columns.join(', ')}) ` +
435
+ `VALUES (${input.valuePlaceholders.join(', ')})${this.buildReturningClause(input.returning)}`);
436
+ },
437
+ buildBulkInsertStatement(input) {
438
+ // No UNNEST in SQLite — emit multi-row VALUES with flattened, named
439
+ // placeholders (`:p1`, `:p2`, …) matching the flat param order.
440
+ let n = 0;
441
+ const placeholders = input.rowValues
442
+ .map((row) => `(${row.map(() => this.paramPlaceholder(++n)).join(', ')})`)
443
+ .join(', ');
444
+ const conflict = input.skipDuplicates ? ' ON CONFLICT DO NOTHING' : '';
445
+ return {
446
+ sql: `INSERT INTO ${input.table} (${input.columns.join(', ')}) VALUES ${placeholders}` +
447
+ `${conflict}${this.buildReturningClause(input.returning)}`,
448
+ params: input.rowValues.flat(),
449
+ };
450
+ },
451
+ buildUpsertStatement(input) {
452
+ return (`INSERT INTO ${input.table} (${input.insertColumns.join(', ')}) VALUES (${input.valuePlaceholders.join(', ')})` +
453
+ ` ON CONFLICT (${input.conflictColumns.join(', ')}) DO UPDATE SET ${input.updateSetClauses.join(', ')}` +
454
+ this.buildReturningClause(input.returning));
455
+ },
456
+ buildInsensitiveLike(column, paramRef) {
457
+ // COLLATE NOCASE is ASCII-only (no Unicode case folding) — documented limit.
458
+ return `${column} LIKE ${paramRef} COLLATE NOCASE`;
459
+ },
460
+ buildJsonContains(column, paramRef) {
461
+ // Emulated containment: true when any top-level JSON value equals the param.
462
+ // Limited vs Postgres `@>` (no deep/object containment) — jsonPathSupport='function'.
463
+ return `EXISTS (SELECT 1 FROM json_each(${column}) WHERE json_each.value = ${paramRef})`;
464
+ },
465
+ buildJsonPathExtract(column, pathParamRef) {
466
+ return `json_extract(${column}, ${pathParamRef})`;
467
+ },
468
+ // ---- Type mapping -------------------------------------------------------
469
+ typeToTypeScript(dialectType, nullable) {
470
+ return sqliteTypeToTs(dialectType, nullable);
471
+ },
472
+ // SQLite has no true array columns; bulk insert uses multi-row VALUES, so no
473
+ // array cast is ever needed. Returning undefined disables the UNNEST path.
474
+ arrayType: undefined,
475
+ // ---- DDL ----------------------------------------------------------------
476
+ buildColumnType(input) {
477
+ return sqliteColumnAffinity(input.type);
478
+ },
479
+ buildColumnDefinition(input) {
480
+ const isSerial = /serial/i.test(input.type);
481
+ const parts = [input.name];
482
+ if (input.primaryKey && isSerial) {
483
+ // The canonical SQLite auto-increment PK (implicitly NOT NULL).
484
+ parts.push('INTEGER PRIMARY KEY AUTOINCREMENT');
485
+ }
486
+ else {
487
+ parts.push(this.buildColumnType(input));
488
+ if (input.primaryKey)
489
+ parts.push('PRIMARY KEY');
490
+ if (input.unique && !input.primaryKey)
491
+ parts.push('UNIQUE');
492
+ if (input.notNull)
493
+ parts.push('NOT NULL');
494
+ }
495
+ if (input.defaultValue != null)
496
+ parts.push(`DEFAULT ${input.defaultValue}`);
497
+ if (input.references)
498
+ parts.push(`REFERENCES ${input.references.table}(${input.references.column})`);
499
+ return parts.join(' ');
500
+ },
501
+ buildCreateIndexStatement(input) {
502
+ return `CREATE INDEX IF NOT EXISTS ${input.name} ON ${input.table}(${input.columns.join(', ')});`;
503
+ },
504
+ buildCreateTableStatement(input) {
505
+ const body = input.definitions.map((d) => ` ${d}`).join(',\n');
506
+ return `CREATE TABLE ${input.table} (\n${body}\n);`;
507
+ },
508
+ // ---- Migration tracking -------------------------------------------------
509
+ // buildMigrationSelectApplied / UpdateChecksum / InsertApplied / DeleteApplied
510
+ // are inherited from postgresDialect: they call `this.paramPlaceholder(n)`
511
+ // (→ `?`) and `ON CONFLICT (name) DO NOTHING`, both valid SQLite. Only the
512
+ // tracking-table DDL (SERIAL / TIMESTAMPTZ) needs a SQLite-specific override.
513
+ buildMigrationTrackingTable(table) {
514
+ return `
515
+ CREATE TABLE IF NOT EXISTS ${table} (
516
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
517
+ name TEXT NOT NULL UNIQUE,
518
+ checksum TEXT NOT NULL,
519
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
520
+ );
521
+ `;
522
+ },
523
+ // ---- Streaming ----------------------------------------------------------
524
+ /**
525
+ * SQLite is in-process: the whole result set already lives in memory, so the
526
+ * "stream" simply fetches once via the pooled connection and yields the rows
527
+ * in `batchSize` chunks. No server-side cursor (and none needed).
528
+ */
529
+ async *openStream(connection, sql, params, batchSize) {
530
+ const result = await connection.query(sql, params);
531
+ for (let i = 0; i < result.rows.length; i += batchSize) {
532
+ yield result.rows.slice(i, i + batchSize);
533
+ }
534
+ },
535
+ // ---- Introspection ------------------------------------------------------
536
+ introspector: {
537
+ async introspect(options) {
538
+ return introspectSqlite(options);
539
+ },
540
+ },
541
+ };
542
+ function pragma(db, sql) {
543
+ // PRAGMA / SELECT against sqlite_master — read-only, identifiers are SQLite
544
+ // catalog names (never user input here), values normalized for safe ints.
545
+ return db.prepare(sql).all().map(normalizeRow);
546
+ }
547
+ /**
548
+ * Read a live SQLite database (an open `DatabaseSync` handle) into the same
549
+ * {@link SchemaMetadata} shape the Postgres catalog introspector produces.
550
+ * Exposed directly so callers (and tests) can introspect an in-process
551
+ * `:memory:` database without round-tripping through a file path.
552
+ */
553
+ function introspectSqliteDatabase(db, options = {}) {
554
+ // ----- Tables (skip SQLite internal + the migration tracking table) -----
555
+ let tableNames = pragma(db, "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name").map((r) => r.name);
556
+ if (options.include?.length) {
557
+ const inc = new Set(options.include);
558
+ tableNames = tableNames.filter((t) => inc.has(t));
559
+ }
560
+ if (options.exclude?.length) {
561
+ const exc = new Set(options.exclude);
562
+ tableNames = tableNames.filter((t) => !exc.has(t));
563
+ }
564
+ const tableSet = new Set(tableNames);
565
+ const columnsByTable = new Map();
566
+ const pkByTable = new Map();
567
+ const uniqueByTable = new Map();
568
+ const indexesByTable = new Map();
569
+ const foreignKeys = [];
570
+ for (const tableName of tableNames) {
571
+ const q = (name) => `"${name.replace(/"/g, '""')}"`;
572
+ // ----- Columns + primary key -----
573
+ const cols = pragma(db, `PRAGMA table_info(${q(tableName)})`);
574
+ const colMeta = [];
575
+ const pkCols = [];
576
+ for (const c of cols) {
577
+ const declared = c.type || '';
578
+ colMeta.push({
579
+ name: c.name,
580
+ field: (0, schema_js_1.snakeToCamel)(c.name),
581
+ dialectType: declared,
582
+ pgType: declared,
583
+ tsType: sqliteTypeToTs(declared, c.notnull === 0 && c.pk === 0),
584
+ nullable: c.notnull === 0 && c.pk === 0,
585
+ hasDefault: c.dflt_value !== null || c.pk === 1,
586
+ isArray: false,
587
+ arrayType: undefined,
588
+ pgArrayType: 'text[]',
589
+ });
590
+ if (c.pk > 0)
591
+ pkCols.push({ name: c.name, order: c.pk });
592
+ }
593
+ columnsByTable.set(tableName, colMeta);
594
+ pkByTable.set(tableName, pkCols.sort((a, b) => a.order - b.order).map((p) => p.name));
595
+ // ----- Foreign keys -----
596
+ const fks = pragma(db, `PRAGMA foreign_key_list(${q(tableName)})`);
597
+ // Group by FK id (composite FKs share an id), ordered by seq.
598
+ const fkById = new Map();
599
+ for (const fk of fks.sort((a, b) => a.id - b.id || a.seq - b.seq)) {
600
+ let entry = fkById.get(fk.id);
601
+ if (!entry) {
602
+ entry = {
603
+ sourceTable: tableName,
604
+ sourceColumns: [],
605
+ targetTable: fk.table,
606
+ targetColumns: [],
607
+ constraintName: `fk_${tableName}_${fk.id}`,
608
+ };
609
+ fkById.set(fk.id, entry);
610
+ }
611
+ entry.sourceColumns.push(fk.from);
612
+ // `to` is null when the FK references the target's PK implicitly.
613
+ if (fk.to)
614
+ entry.targetColumns.push(fk.to);
615
+ }
616
+ for (const entry of fkById.values()) {
617
+ if (entry.targetColumns.length === 0) {
618
+ // Implicit reference → the target table's primary key.
619
+ const targetPk = pragma(db, `PRAGMA table_info(${q(entry.targetTable)})`)
620
+ .filter((c) => c.pk > 0)
621
+ .sort((a, b) => a.pk - b.pk)
622
+ .map((c) => c.name);
623
+ entry.targetColumns = targetPk.length > 0 ? targetPk : ['id'];
624
+ }
625
+ if (tableSet.has(entry.targetTable))
626
+ foreignKeys.push(entry);
627
+ }
628
+ // ----- Indexes + unique constraints -----
629
+ const idxList = pragma(db, `PRAGMA index_list(${q(tableName)})`);
630
+ const idxMeta = [];
631
+ const uniques = [];
632
+ for (const idx of idxList) {
633
+ const idxCols = pragma(db, `PRAGMA index_info(${q(idx.name)})`)
634
+ .sort((a, b) => a.seqno - b.seqno)
635
+ .map((c) => c.name);
636
+ idxMeta.push({
637
+ name: idx.name,
638
+ columns: idxCols,
639
+ unique: idx.unique === 1,
640
+ definition: `${idx.unique === 1 ? 'UNIQUE ' : ''}INDEX ${idx.name} ON ${tableName}(${idxCols.join(', ')})`,
641
+ });
642
+ if (idx.unique === 1 && idxCols.length > 0)
643
+ uniques.push(idxCols);
644
+ }
645
+ indexesByTable.set(tableName, idxMeta);
646
+ uniqueByTable.set(tableName, uniques);
647
+ }
648
+ // ----- Build relations from foreign keys (belongsTo + hasMany) -----
649
+ const fkCounts = new Map();
650
+ for (const fk of foreignKeys) {
651
+ const key = `${fk.sourceTable}->${fk.targetTable}`;
652
+ fkCounts.set(key, (fkCounts.get(key) ?? 0) + 1);
653
+ }
654
+ const relationsByTable = new Map();
655
+ for (const fk of foreignKeys) {
656
+ const needsDisambiguation = (fkCounts.get(`${fk.sourceTable}->${fk.targetTable}`) ?? 0) > 1;
657
+ const foreignKey = fk.sourceColumns.length === 1 ? fk.sourceColumns[0] : fk.sourceColumns;
658
+ const referenceKey = fk.targetColumns.length === 1 ? fk.targetColumns[0] : fk.targetColumns;
659
+ const belongsToName = needsDisambiguation && fk.sourceColumns.length === 1
660
+ ? (0, schema_js_1.snakeToCamel)(fk.sourceColumns[0].replace(/_id$/, ''))
661
+ : (0, schema_js_1.singularize)((0, schema_js_1.snakeToCamel)(fk.targetTable));
662
+ if (!relationsByTable.has(fk.sourceTable))
663
+ relationsByTable.set(fk.sourceTable, {});
664
+ relationsByTable.get(fk.sourceTable)[belongsToName] = {
665
+ type: 'belongsTo',
666
+ name: belongsToName,
667
+ from: fk.sourceTable,
668
+ to: fk.targetTable,
669
+ foreignKey,
670
+ referenceKey,
671
+ };
672
+ const hasManyName = needsDisambiguation && fk.sourceColumns.length === 1
673
+ ? (0, schema_js_1.snakeToCamel)(`${fk.sourceTable}_by_${fk.sourceColumns[0].replace(/_id$/, '')}`)
674
+ : (0, schema_js_1.snakeToCamel)(fk.sourceTable);
675
+ if (!relationsByTable.has(fk.targetTable))
676
+ relationsByTable.set(fk.targetTable, {});
677
+ relationsByTable.get(fk.targetTable)[hasManyName] = {
678
+ type: 'hasMany',
679
+ name: hasManyName,
680
+ from: fk.targetTable,
681
+ to: fk.sourceTable,
682
+ foreignKey,
683
+ referenceKey,
684
+ };
685
+ }
686
+ // ----- Conservative many-to-many auto-detection (additive) -----
687
+ // A table J is a pure junction iff: PK is exactly two columns, exactly two
688
+ // single-column FKs whose source columns ARE the PK, two distinct target
689
+ // tables, and no payload columns. Mirrors the Postgres introspector.
690
+ for (const tableName of tableNames) {
691
+ const pk = pkByTable.get(tableName) ?? [];
692
+ if (pk.length !== 2)
693
+ continue;
694
+ const tableFks = foreignKeys.filter((fk) => fk.sourceTable === tableName);
695
+ if (tableFks.length !== 2)
696
+ continue;
697
+ if (tableFks.some((fk) => fk.sourceColumns.length !== 1))
698
+ continue;
699
+ const fkCols = tableFks.map((fk) => fk.sourceColumns[0]);
700
+ const pkSet = new Set(pk);
701
+ if (!fkCols.every((c) => pkSet.has(c)))
702
+ continue;
703
+ if (new Set(fkCols).size !== 2)
704
+ continue;
705
+ const [fkA, fkB] = tableFks;
706
+ if (fkA.targetTable === fkB.targetTable)
707
+ continue;
708
+ const jCols = (columnsByTable.get(tableName) ?? []).map((c) => c.name);
709
+ if (jCols.length !== 2)
710
+ continue;
711
+ const addM2M = (self, other) => {
712
+ const sourceTbl = self.targetTable;
713
+ const targetTbl = other.targetTable;
714
+ const relName = (0, schema_js_1.snakeToCamel)(targetTbl);
715
+ if (!relationsByTable.has(sourceTbl))
716
+ relationsByTable.set(sourceTbl, {});
717
+ const existing = relationsByTable.get(sourceTbl);
718
+ if (existing[relName])
719
+ return;
720
+ existing[relName] = {
721
+ type: 'manyToMany',
722
+ name: relName,
723
+ from: sourceTbl,
724
+ to: targetTbl,
725
+ referenceKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
726
+ foreignKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
727
+ through: {
728
+ table: tableName,
729
+ sourceKey: self.sourceColumns[0],
730
+ targetKey: other.sourceColumns[0],
731
+ },
732
+ };
733
+ };
734
+ addM2M(fkA, fkB);
735
+ addM2M(fkB, fkA);
736
+ }
737
+ // ----- Assemble TableMetadata -----
738
+ const tables = {};
739
+ for (const tableName of tableNames) {
740
+ const columns = columnsByTable.get(tableName) ?? [];
741
+ const columnMap = {};
742
+ const reverseColumnMap = {};
743
+ const dateColumns = new Set();
744
+ const dialectTypes = {};
745
+ const pgTypes = {};
746
+ const allColumns = [];
747
+ for (const col of columns) {
748
+ columnMap[col.field] = col.name;
749
+ reverseColumnMap[col.name] = col.field;
750
+ allColumns.push(col.name);
751
+ dialectTypes[col.name] = col.dialectType ?? col.pgType;
752
+ pgTypes[col.name] = col.pgType;
753
+ if (isSqliteDateType(col.dialectType ?? col.pgType))
754
+ dateColumns.add(col.name);
755
+ }
756
+ tables[tableName] = {
757
+ name: tableName,
758
+ columns,
759
+ columnMap,
760
+ reverseColumnMap,
761
+ dateColumns,
762
+ dialectTypes,
763
+ pgTypes,
764
+ allColumns,
765
+ primaryKey: pkByTable.get(tableName) ?? [],
766
+ uniqueColumns: uniqueByTable.get(tableName) ?? [],
767
+ relations: relationsByTable.get(tableName) ?? {},
768
+ indexes: indexesByTable.get(tableName) ?? [],
769
+ };
770
+ }
771
+ return { tables, enums: {} };
772
+ }
773
+ /**
774
+ * Open the SQLite database named by `options.connectionString` (a file path or
775
+ * `':memory:'`), introspect it, and close it. Wraps
776
+ * {@link introspectSqliteDatabase} for the {@link DialectIntrospector} seam used
777
+ * by `introspect()` / `npx turbine generate`.
778
+ *
779
+ * Note: introspecting `':memory:'` opens a *fresh, empty* database (memory DBs
780
+ * are per-handle), so codegen should target a real file.
781
+ */
782
+ async function introspectSqlite(options) {
783
+ const DatabaseSync = loadDatabaseSync();
784
+ let db;
785
+ try {
786
+ db = new DatabaseSync(options.connectionString);
787
+ }
788
+ catch (err) {
789
+ throw new errors_js_1.ConnectionError(`[turbine] Failed to open SQLite database "${options.connectionString}": ${err.message}`);
790
+ }
791
+ try {
792
+ return introspectSqliteDatabase(db, { include: options.include, exclude: options.exclude });
793
+ }
794
+ finally {
795
+ db.close();
796
+ }
797
+ }
798
+ function openSqliteDatabase(target, options) {
799
+ const DatabaseSync = loadDatabaseSync();
800
+ let db;
801
+ try {
802
+ db = new DatabaseSync(target);
803
+ }
804
+ catch (err) {
805
+ throw new errors_js_1.ConnectionError(`[turbine] Failed to open SQLite database "${target}": ${err.message}`);
806
+ }
807
+ db.exec(`PRAGMA busy_timeout = ${Number(options.busyTimeoutMs ?? 5000)}`);
808
+ if (options.foreignKeys !== false)
809
+ db.exec('PRAGMA foreign_keys = ON');
810
+ if (target !== ':memory:' && options.wal !== false) {
811
+ try {
812
+ db.exec('PRAGMA journal_mode = WAL');
813
+ }
814
+ catch {
815
+ // Some filesystems (network mounts) reject WAL — fall back silently.
816
+ }
817
+ }
818
+ return db;
819
+ }
820
+ /**
821
+ * Create a {@link TurbineClient} bound to SQLite via `node:sqlite`.
822
+ *
823
+ * Pass a file path, `':memory:'`, or an already-open `DatabaseSync` handle (so
824
+ * you can seed / introspect it first and reuse the same connection). The
825
+ * returned client uses {@link sqliteDialect} and disables prepared-statement
826
+ * names (SQLite caches plans internally).
827
+ *
828
+ * @param target A SQLite file path, `':memory:'`, or an open `DatabaseSync`.
829
+ * @param schema Introspected or hand-written {@link SchemaMetadata}.
830
+ * @param options Optional pragmas + logging / defaultLimit / warnOnUnlimited.
831
+ *
832
+ * @example
833
+ * ```ts
834
+ * import { turbineSqlite } from 'turbine-orm/sqlite';
835
+ * const db = turbineSqlite(':memory:', SCHEMA);
836
+ * ```
837
+ */
838
+ function turbineSqlite(target, schema, options = {}) {
839
+ const db = typeof target === 'string' ? openSqliteDatabase(target, options) : target;
840
+ const pool = new SqlitePool(db);
841
+ return new client_js_1.TurbineClient({
842
+ pool,
843
+ dialect: exports.sqliteDialect,
844
+ preparedStatements: false,
845
+ logging: options.logging,
846
+ defaultLimit: options.defaultLimit,
847
+ warnOnUnlimited: options.warnOnUnlimited,
848
+ }, schema);
849
+ }