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