turbine-orm 0.50.0 → 0.51.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 (186) hide show
  1. package/README.md +66 -66
  2. package/dist/adapters/cockroachdb.d.ts +5 -5
  3. package/dist/adapters/cockroachdb.js +10 -10
  4. package/dist/adapters/index.d.ts +5 -5
  5. package/dist/adapters/index.js +7 -7
  6. package/dist/adapters/yugabytedb.d.ts +7 -7
  7. package/dist/adapters/yugabytedb.js +10 -10
  8. package/dist/cjs/adapters/cockroachdb.d.ts +5 -5
  9. package/dist/cjs/adapters/cockroachdb.js +10 -10
  10. package/dist/cjs/adapters/index.d.ts +5 -5
  11. package/dist/cjs/adapters/index.js +7 -7
  12. package/dist/cjs/adapters/yugabytedb.d.ts +7 -7
  13. package/dist/cjs/adapters/yugabytedb.js +10 -10
  14. package/dist/cjs/cli/config.d.ts +13 -2
  15. package/dist/cjs/cli/config.js +3 -2
  16. package/dist/cjs/cli/destructive.d.ts +1 -1
  17. package/dist/cjs/cli/destructive.js +1 -1
  18. package/dist/cjs/cli/index.d.ts +10 -10
  19. package/dist/cjs/cli/index.js +49 -45
  20. package/dist/cjs/cli/loader.d.ts +7 -7
  21. package/dist/cjs/cli/loader.js +9 -9
  22. package/dist/cjs/cli/mcp.js +4 -4
  23. package/dist/cjs/cli/migrate.d.ts +5 -5
  24. package/dist/cjs/cli/migrate.js +11 -11
  25. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  26. package/dist/cjs/cli/ui.d.ts +2 -2
  27. package/dist/cjs/cli/ui.js +2 -2
  28. package/dist/cjs/client.d.ts +49 -38
  29. package/dist/cjs/client.js +57 -56
  30. package/dist/cjs/dialect.d.ts +62 -18
  31. package/dist/cjs/dialect.js +40 -2
  32. package/dist/cjs/errors.d.ts +5 -5
  33. package/dist/cjs/errors.js +11 -11
  34. package/dist/cjs/generate.d.ts +6 -6
  35. package/dist/cjs/generate.js +31 -29
  36. package/dist/cjs/index-advisor.d.ts +5 -5
  37. package/dist/cjs/index-advisor.js +0 -0
  38. package/dist/cjs/index.d.ts +1 -1
  39. package/dist/cjs/index.js +7 -7
  40. package/dist/cjs/introspect.d.ts +35 -9
  41. package/dist/cjs/introspect.js +83 -32
  42. package/dist/cjs/mssql.d.ts +11 -11
  43. package/dist/cjs/mssql.js +64 -29
  44. package/dist/cjs/mysql.d.ts +8 -8
  45. package/dist/cjs/mysql.js +61 -23
  46. package/dist/cjs/nested-write.d.ts +21 -2
  47. package/dist/cjs/nested-write.js +51 -14
  48. package/dist/cjs/optional-peer-import.cjs +7 -7
  49. package/dist/cjs/optional-peer-import.d.cts +7 -7
  50. package/dist/cjs/pipeline-submittable.d.ts +2 -2
  51. package/dist/cjs/pipeline-submittable.js +6 -6
  52. package/dist/cjs/pipeline.d.ts +1 -1
  53. package/dist/cjs/pipeline.js +4 -4
  54. package/dist/cjs/powdb-introspect.d.ts +1 -1
  55. package/dist/cjs/powdb-introspect.js +1 -1
  56. package/dist/cjs/powdb.d.ts +28 -28
  57. package/dist/cjs/powdb.js +66 -66
  58. package/dist/cjs/powql.d.ts +27 -27
  59. package/dist/cjs/powql.js +73 -52
  60. package/dist/cjs/query/aggregates.d.ts +1 -1
  61. package/dist/cjs/query/aggregates.js +5 -5
  62. package/dist/cjs/query/batched-loader.d.ts +11 -11
  63. package/dist/cjs/query/batched-loader.js +24 -24
  64. package/dist/cjs/query/builder.d.ts +39 -21
  65. package/dist/cjs/query/builder.js +99 -57
  66. package/dist/cjs/query/compound-unique.d.ts +1 -1
  67. package/dist/cjs/query/compound-unique.js +0 -0
  68. package/dist/cjs/query/deferred.d.ts +12 -6
  69. package/dist/cjs/query/deferred.js +1 -1
  70. package/dist/cjs/query/filters.d.ts +31 -11
  71. package/dist/cjs/query/filters.js +67 -14
  72. package/dist/cjs/query/index.d.ts +1 -1
  73. package/dist/cjs/query/index.js +1 -1
  74. package/dist/cjs/query/relations.d.ts +9 -9
  75. package/dist/cjs/query/relations.js +164 -57
  76. package/dist/cjs/query/types.d.ts +86 -35
  77. package/dist/cjs/query/types.js +1 -1
  78. package/dist/cjs/query/utils.d.ts +27 -10
  79. package/dist/cjs/query/utils.js +86 -14
  80. package/dist/cjs/query/where.d.ts +47 -28
  81. package/dist/cjs/query/where.js +130 -31
  82. package/dist/cjs/query/writes.d.ts +24 -5
  83. package/dist/cjs/query/writes.js +102 -13
  84. package/dist/cjs/realtime.d.ts +7 -7
  85. package/dist/cjs/realtime.js +9 -9
  86. package/dist/cjs/schema-builder.d.ts +18 -7
  87. package/dist/cjs/schema-builder.js +17 -10
  88. package/dist/cjs/schema-metadata.d.ts +3 -3
  89. package/dist/cjs/schema-metadata.js +9 -9
  90. package/dist/cjs/schema-sql.d.ts +9 -9
  91. package/dist/cjs/schema-sql.js +20 -20
  92. package/dist/cjs/schema.d.ts +19 -9
  93. package/dist/cjs/schema.js +6 -6
  94. package/dist/cjs/serverless.d.ts +15 -15
  95. package/dist/cjs/serverless.js +16 -16
  96. package/dist/cjs/sqlite.d.ts +8 -8
  97. package/dist/cjs/sqlite.js +53 -22
  98. package/dist/cjs/typed-sql.d.ts +4 -4
  99. package/dist/cjs/typed-sql.js +5 -5
  100. package/dist/cli/config.d.ts +13 -2
  101. package/dist/cli/config.js +3 -2
  102. package/dist/cli/destructive.d.ts +1 -1
  103. package/dist/cli/destructive.js +1 -1
  104. package/dist/cli/index.d.ts +10 -10
  105. package/dist/cli/index.js +49 -45
  106. package/dist/cli/loader.d.ts +7 -7
  107. package/dist/cli/loader.js +9 -9
  108. package/dist/cli/mcp.js +4 -4
  109. package/dist/cli/migrate.d.ts +5 -5
  110. package/dist/cli/migrate.js +11 -11
  111. package/dist/cli/studio-ui.generated.js +1 -1
  112. package/dist/cli/ui.d.ts +2 -2
  113. package/dist/cli/ui.js +2 -2
  114. package/dist/client.d.ts +49 -38
  115. package/dist/client.js +57 -56
  116. package/dist/dialect.d.ts +62 -18
  117. package/dist/dialect.js +40 -2
  118. package/dist/errors.d.ts +5 -5
  119. package/dist/errors.js +11 -11
  120. package/dist/generate.d.ts +6 -6
  121. package/dist/generate.js +31 -29
  122. package/dist/index-advisor.d.ts +5 -5
  123. package/dist/index-advisor.js +0 -0
  124. package/dist/index.d.ts +1 -1
  125. package/dist/index.js +7 -7
  126. package/dist/introspect.d.ts +35 -9
  127. package/dist/introspect.js +82 -32
  128. package/dist/mssql.d.ts +11 -11
  129. package/dist/mssql.js +64 -29
  130. package/dist/mysql.d.ts +8 -8
  131. package/dist/mysql.js +61 -23
  132. package/dist/nested-write.d.ts +21 -2
  133. package/dist/nested-write.js +51 -14
  134. package/dist/optional-peer-import.cjs +7 -7
  135. package/dist/optional-peer-import.d.cts +7 -7
  136. package/dist/pipeline-submittable.d.ts +2 -2
  137. package/dist/pipeline-submittable.js +6 -6
  138. package/dist/pipeline.d.ts +1 -1
  139. package/dist/pipeline.js +4 -4
  140. package/dist/powdb-introspect.d.ts +1 -1
  141. package/dist/powdb-introspect.js +1 -1
  142. package/dist/powdb.d.ts +28 -28
  143. package/dist/powdb.js +66 -66
  144. package/dist/powql.d.ts +27 -27
  145. package/dist/powql.js +73 -52
  146. package/dist/query/aggregates.d.ts +1 -1
  147. package/dist/query/aggregates.js +5 -5
  148. package/dist/query/batched-loader.d.ts +11 -11
  149. package/dist/query/batched-loader.js +24 -24
  150. package/dist/query/builder.d.ts +39 -21
  151. package/dist/query/builder.js +100 -58
  152. package/dist/query/compound-unique.d.ts +1 -1
  153. package/dist/query/compound-unique.js +0 -0
  154. package/dist/query/deferred.d.ts +12 -6
  155. package/dist/query/deferred.js +1 -1
  156. package/dist/query/filters.d.ts +31 -11
  157. package/dist/query/filters.js +66 -13
  158. package/dist/query/index.d.ts +1 -1
  159. package/dist/query/index.js +1 -1
  160. package/dist/query/relations.d.ts +9 -9
  161. package/dist/query/relations.js +165 -58
  162. package/dist/query/types.d.ts +86 -35
  163. package/dist/query/types.js +1 -1
  164. package/dist/query/utils.d.ts +27 -10
  165. package/dist/query/utils.js +84 -14
  166. package/dist/query/where.d.ts +47 -28
  167. package/dist/query/where.js +129 -32
  168. package/dist/query/writes.d.ts +24 -5
  169. package/dist/query/writes.js +101 -13
  170. package/dist/realtime.d.ts +7 -7
  171. package/dist/realtime.js +9 -9
  172. package/dist/schema-builder.d.ts +18 -7
  173. package/dist/schema-builder.js +17 -10
  174. package/dist/schema-metadata.d.ts +3 -3
  175. package/dist/schema-metadata.js +9 -9
  176. package/dist/schema-sql.d.ts +9 -9
  177. package/dist/schema-sql.js +20 -20
  178. package/dist/schema.d.ts +19 -9
  179. package/dist/schema.js +6 -6
  180. package/dist/serverless.d.ts +15 -15
  181. package/dist/serverless.js +16 -16
  182. package/dist/sqlite.d.ts +8 -8
  183. package/dist/sqlite.js +53 -22
  184. package/dist/typed-sql.d.ts +4 -4
  185. package/dist/typed-sql.js +5 -5
  186. package/package.json +2 -2
package/dist/cjs/mssql.js CHANGED
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  /**
3
- * turbine-orm/mssql — Microsoft SQL Server engine (driver-injected, optional peer)
3
+ * turbine-orm/mssql, Microsoft SQL Server engine (driver-injected, optional peer)
4
4
  *
5
5
  * Binds Turbine to SQL Server 2016+ via the `mssql` driver (which wraps
6
- * `tedious`). `mssql` is **not** a root dependency — it is an **optional peer**:
6
+ * `tedious`). `mssql` is **not** a root dependency, it is an **optional peer**:
7
7
  * `npm i turbine-orm` pulls nothing extra, and only consumers who
8
8
  * `import 'turbine-orm/mssql'` install `mssql` themselves. The factory loads it
9
9
  * through a dynamic `import('mssql')` so importing this module never crashes when
@@ -34,14 +34,14 @@
34
34
  * real JSON instead of being escaped as a string. `INCLUDE_NULL_VALUES`
35
35
  * keeps NULL columns present (matching PostgreSQL `json_build_object`).
36
36
  * 3. **No `LIMIT`.** Paging is `ORDER BY … OFFSET n ROWS FETCH NEXT m ROWS ONLY`,
37
- * which requires an ORDER BY — a stable `ORDER BY (SELECT NULL)` is injected
37
+ * which requires an ORDER BY, a stable `ORDER BY (SELECT NULL)` is injected
38
38
  * when the query has none (`Dialect.buildLimitOffset`).
39
39
  *
40
40
  * ## Named `@pN` placeholders (no positional `?`)
41
41
  *
42
42
  * `mssqlDialect.paramPlaceholder = (i) => '@p' + i`. The driver shim binds via
43
43
  * `request.input('p' + i, value)`, so binding is by NAME and independent of where
44
- * each placeholder lands in the SQL text — exactly the guarantee PostgreSQL's
44
+ * each placeholder lands in the SQL text, exactly the guarantee PostgreSQL's
45
45
  * numbered `$N` gives. (SQL Server is naturally named-param friendly, sidestepping
46
46
  * the positional-`?` mis-bind bug the SQLite/MySQL phases hit.)
47
47
  *
@@ -49,12 +49,12 @@
49
49
  *
50
50
  * - **Single query nested relations preserved** via `FOR JSON PATH` (SQL Server
51
51
  * 2016+). Ordered/limited to-many uses `ORDER BY … OFFSET/FETCH` inside the FOR
52
- * JSON subquery (no inner-subquery rewrite needed — FOR JSON aggregates AFTER
52
+ * JSON subquery (no inner-subquery rewrite needed, FOR JSON aggregates AFTER
53
53
  * the row selection).
54
54
  * - **Result strategy `'output'`:** create/update/delete/upsert return their rows
55
55
  * from the same statement. `createMany` returns the inserted rows via
56
56
  * `OUTPUT INSERTED.*` on the multi-row VALUES insert (≤ 1000 rows / 2100 params
57
- * per statement — exceeding either throws a clear `ValidationError`; chunk
57
+ * per statement, exceeding either throws a clear `ValidationError`; chunk
58
58
  * yourself or use single `create`s).
59
59
  * - **MERGE concurrency caveat:** `MERGE` is the upsert primitive; under high
60
60
  * concurrency a `MERGE` can still race (it is NOT a substitute for a unique
@@ -63,20 +63,20 @@
63
63
  * loser of a race.
64
64
  * - **Unsupported (throw `UnsupportedFeatureError`):** pgvector distance ops,
65
65
  * LISTEN/NOTIFY (`$listen`/`$notify`), RLS `sessionContext` (sp_set_session_context
66
- * exists but is connection-scoped, not transaction-local, so it is not wired —
66
+ * exists but is connection-scoped, not transaction-local, so it is not wired -
67
67
  * throws rather than silently leaking context across pooled connections).
68
68
  * - **Advisory-lock migration locking** is available in principle via
69
69
  * `sp_getapplock`/`sp_releaseapplock` (`supportsAdvisoryLock = true`); the
70
70
  * migrate CLI is still PostgreSQL-only, so this flag documents intent for a
71
71
  * future adapter.
72
- * - **Case-insensitive matching** uses `LOWER(col) LIKE LOWER(ref)` — deterministic
72
+ * - **Case-insensitive matching** uses `LOWER(col) LIKE LOWER(ref)`, deterministic
73
73
  * regardless of the column's collation (note this can defeat an index unless a
74
74
  * computed/persisted `LOWER()` index exists).
75
75
  * - **bignum:** the shim applies the same safe-int policy Turbine uses for Postgres
76
76
  * `int8` (number when it fits in 2^53, decimal string otherwise) WITHOUT mutating
77
77
  * any global driver state. `DECIMAL`/`NUMERIC`/`MONEY` come back as strings;
78
78
  * `BIT` binds/returns booleans.
79
- * - **`DISTINCT ON`** is PostgreSQL-only and is not translated — avoid `distinct`
79
+ * - **`DISTINCT ON`** is PostgreSQL-only and is not translated, avoid `distinct`
80
80
  * on SQL Server.
81
81
  *
82
82
  * ## Example
@@ -122,7 +122,7 @@ function sortedRelEntries(obj) {
122
122
  /**
123
123
  * Coerce an arbitrary JS value into something `mssql` can bind. `BIT` accepts JS
124
124
  * booleans directly; `undefined`/`null` → NULL; `bigint` follows the safe-int
125
- * policy (number when it fits in 2^53, else a decimal string — no precision
125
+ * policy (number when it fits in 2^53, else a decimal string, no precision
126
126
  * loss); `Date`/`Uint8Array` pass through; any remaining object/array → JSON text
127
127
  * (matches how Turbine already pre-serializes JSON filter / `IN`-list params).
128
128
  */
@@ -162,7 +162,7 @@ function shapeResult(result) {
162
162
  * driver returns BIGINT as a string to avoid precision loss, so a BIGINT IDENTITY
163
163
  * `id` would otherwise surface as `'1'` instead of `1`. Values outside the safe
164
164
  * range are left as strings (same as the Postgres path). Nested relations are
165
- * unaffected — `FOR JSON PATH` already renders BIGINT as a JSON number.
165
+ * unaffected, `FOR JSON PATH` already renders BIGINT as a JSON number.
166
166
  */
167
167
  function coerceBigIntColumns(rows, columns) {
168
168
  if (!columns || rows.length === 0)
@@ -285,12 +285,12 @@ class MssqlTxClient {
285
285
  const { text: rawSql, params } = normalizeQueryArgs(text, values);
286
286
  const sql = rawSql.trim();
287
287
  // RELEASE SAVEPOINT has no SQL Server equivalent (savepoints auto-persist until
288
- // the outer commit/rollback) — releaseSavepointStatement() returns '' → no-op.
288
+ // the outer commit/rollback), releaseSavepointStatement() returns '' → no-op.
289
289
  if (sql === '')
290
290
  return EMPTY_RESULT;
291
291
  // The dialect composes `SET TRANSACTION ISOLATION LEVEL …; BEGIN TRANSACTION`
292
292
  // (SQL Server cannot take an inline isolation level on BEGIN). Split and run
293
- // the parts in order — same pattern as the MySQL shim. Gated on the exact
293
+ // the parts in order, same pattern as the MySQL shim. Gated on the exact
294
294
  // transaction-control prefix so no builder/user SQL is ever split.
295
295
  if (/^SET TRANSACTION ISOLATION LEVEL /i.test(sql) && sql.includes('; ')) {
296
296
  let last = EMPTY_RESULT;
@@ -351,7 +351,7 @@ class MssqlTxClient {
351
351
  * physical connection.
352
352
  */
353
353
  class MssqlPool {
354
- /** The underlying `mssql` ConnectionPool — exposed as an escape hatch (seed / DDL / advanced ops). */
354
+ /** The underlying `mssql` ConnectionPool, exposed as an escape hatch (seed / DDL / advanced ops). */
355
355
  pool;
356
356
  sqlNS;
357
357
  closed = false;
@@ -457,7 +457,7 @@ function mssqlColumnType(type, maxLength) {
457
457
  return type;
458
458
  }
459
459
  // ---------------------------------------------------------------------------
460
- // mssqlDialect — the full Dialect contract for SQL Server 2016+
460
+ // mssqlDialect, the full Dialect contract for SQL Server 2016+
461
461
  // ---------------------------------------------------------------------------
462
462
  /**
463
463
  * Render the SQL Server `OUTPUT` clause for a write's returning selection.
@@ -530,6 +530,27 @@ exports.mssqlDialect = {
530
530
  return `[${name.replace(/]/g, ']]')}]`;
531
531
  },
532
532
  // SQL Server aggregate casts: COUNT → INT, AVG/float → FLOAT.
533
+ jsonWireRule(columnType) {
534
+ const t = columnType.toLowerCase();
535
+ // FOR JSON PATH renders BIGINT as a JSON number, which is an IEEE double:
536
+ // a stored 9007199254740993 came back 9007199254740992 through the join
537
+ // strategy, while a top-level read and the batched loader both returned
538
+ // the exact decimal string. The tedious driver returns BIGINT as a STRING
539
+ // unconditionally (verified for both small and large values), so carrying
540
+ // text and keeping it reproduces the driver exactly.
541
+ if (t === 'bigint') {
542
+ return { sql: (ref) => `CAST(${ref} AS NVARCHAR(50))`, decode: (value) => value };
543
+ }
544
+ // Binary columns come out of FOR JSON PATH as BASE64 text ("AQL/") rather
545
+ // than bytes. Style 2 converts to bare hex, which rebuilds exactly.
546
+ if (t === 'binary' || t === 'varbinary' || t === 'image') {
547
+ return {
548
+ sql: (ref) => `CONVERT(VARCHAR(MAX), ${ref}, 2)`,
549
+ decode: (value) => (typeof value === 'string' ? Uint8Array.from(Buffer.from(value, 'hex')) : value),
550
+ };
551
+ }
552
+ return undefined;
553
+ },
533
554
  castAggregate(expr, target) {
534
555
  return `CAST(${expr} AS ${target === 'int' ? 'INT' : 'FLOAT'})`;
535
556
  },
@@ -544,7 +565,7 @@ exports.mssqlDialect = {
544
565
  inClauseParam(values) {
545
566
  return JSON.stringify(values ?? []);
546
567
  },
547
- // OUTPUT replaces RETURNING — injected mid-statement by the statement builders,
568
+ // OUTPUT replaces RETURNING, injected mid-statement by the statement builders,
548
569
  // never as a trailing clause.
549
570
  buildReturningClause() {
550
571
  return '';
@@ -554,7 +575,7 @@ exports.mssqlDialect = {
554
575
  return `INSERT INTO ${input.table} (${input.columns.join(', ')})${out} VALUES (${input.valuePlaceholders.join(', ')})`;
555
576
  },
556
577
  buildBulkInsertStatement(input) {
557
- // No UNNEST in SQL Server — emit multi-row VALUES with flattened, named `@pN`
578
+ // No UNNEST in SQL Server, emit multi-row VALUES with flattened, named `@pN`
558
579
  // placeholders. Enforce the engine's 1000-row / 2100-param statement limits.
559
580
  const rowCount = input.rowValues.length;
560
581
  const paramCount = input.rowValues.reduce((n, row) => n + row.length, 0);
@@ -584,7 +605,7 @@ exports.mssqlDialect = {
584
605
  },
585
606
  buildUpsertStatement(input) {
586
607
  // MERGE is the SQL Server upsert. The MERGE statement MUST end with `;`.
587
- // CONCURRENCY CAVEAT: MERGE is not a substitute for a UNIQUE/PK constraint —
608
+ // CONCURRENCY CAVEAT: MERGE is not a substitute for a UNIQUE/PK constraint -
588
609
  // keep the conflict columns backed by one and rely on UniqueConstraintError
589
610
  // (2627/2601 → E008) for a concurrent loser.
590
611
  const on = input.conflictColumns.map((c) => `T.${c} = S.${c}`).join(' AND ');
@@ -599,7 +620,7 @@ exports.mssqlDialect = {
599
620
  `${out};`);
600
621
  },
601
622
  // UPDATE/DELETE inject OUTPUT mid-statement (between SET and WHERE / FROM and
602
- // WHERE) — a trailing clause would be invalid T-SQL.
623
+ // WHERE), a trailing clause would be invalid T-SQL.
603
624
  buildUpdateStatement(input) {
604
625
  const out = mssqlOutput(input.returning, 'INSERTED');
605
626
  return `UPDATE ${input.table} SET ${input.setClauses.join(', ')}${out}${input.whereSql}`;
@@ -608,7 +629,7 @@ exports.mssqlDialect = {
608
629
  const out = mssqlOutput(input.returning, 'DELETED');
609
630
  return `DELETE FROM ${input.table}${out}${input.whereSql}`;
610
631
  },
611
- // SQL Server has no LIMIT — emit OFFSET/FETCH, injecting a stable ORDER BY when
632
+ // SQL Server has no LIMIT, emit OFFSET/FETCH, injecting a stable ORDER BY when
612
633
  // the outer query has none (OFFSET/FETCH requires an ORDER BY).
613
634
  buildLimitOffset(input) {
614
635
  const { limitPlaceholder, offsetPlaceholder, hasOrderBy } = input;
@@ -634,7 +655,7 @@ exports.mssqlDialect = {
634
655
  },
635
656
  // SQL Server has no JSON_CONTAINS. Emulate "the JSON array column contains the
636
657
  // scalar value" via OPENJSON (documented `limited`: object-containment and deep
637
- // paths are not supported — use a generated column + index for those).
658
+ // paths are not supported, use a generated column + index for those).
638
659
  buildJsonContains(column, paramRef) {
639
660
  return `EXISTS (SELECT 1 FROM OPENJSON(${column}) WHERE [value] = ${paramRef})`;
640
661
  },
@@ -713,7 +734,7 @@ exports.mssqlDialect = {
713
734
  return `SAVE TRANSACTION ${name}`;
714
735
  },
715
736
  releaseSavepointStatement() {
716
- // SQL Server has no RELEASE SAVEPOINT — savepoints persist until the outer
737
+ // SQL Server has no RELEASE SAVEPOINT, savepoints persist until the outer
717
738
  // commit/rollback. The shim treats the empty statement as a no-op.
718
739
  return '';
719
740
  },
@@ -771,10 +792,21 @@ function buildForJsonSubquery(dialect, ctx) {
771
792
  const orderEntries = spec !== true && spec.orderBy ? Object.entries(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
772
793
  const hasOrder = orderEntries.length > 0;
773
794
  const hasLimit = spec !== true && spec.limit !== undefined;
774
- /** `<alias>.<col> AS [<field>]` selection for the FOR JSON object keys. */
795
+ /**
796
+ * `<alias>.<col> AS [<field>]` selection for the FOR JSON object keys.
797
+ *
798
+ * A column whose FOR JSON rendering diverges from the driver's own value is
799
+ * wrapped by the dialect's {@link Dialect.jsonWireRule} cast, exactly as the
800
+ * shared `json_build_object` path does, this generator is an override, so it
801
+ * has to opt in explicitly or BIGINT silently loses precision and a binary
802
+ * column arrives as base64 text.
803
+ */
775
804
  const colSelect = (a) => targetColumns.map((col) => {
776
805
  const field = targetMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col);
777
- return `${a}.${q(col)} AS ${q(field)}`;
806
+ const ref = `${a}.${q(col)}`;
807
+ const type = targetMeta.pgTypes?.[col];
808
+ const expr = (type && dialect.jsonWireRule?.(type)?.sql(ref)) ?? ref;
809
+ return `${expr} AS ${q(field)}`;
778
810
  });
779
811
  /** Build nested relations as `JSON_QUERY((<subquery>)) AS [<name>]` columns (pushes their params). */
780
812
  const buildNested = (parentAlias) => {
@@ -932,7 +964,7 @@ const num = (v) => (typeof v === 'string' ? Number(v) : (v ?? 0));
932
964
  * (`deriveEngineRelations` → `buildRelationsFromForeignKeys` +
933
965
  * `addAutoManyToManyRelations` in introspect.ts), so this engine derives
934
966
  * IDENTICAL relation names to `turbine generate` against Postgres for the
935
- * same logical schema — legacy-first naming, per-column disambiguation, and
967
+ * same logical schema, legacy-first naming, per-column disambiguation, and
936
968
  * collision resolution against scalar column fields included.
937
969
  */
938
970
  function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable) {
@@ -1120,7 +1152,7 @@ async function introspectMssqlWith(exec, schema = 'dbo', options = {}) {
1120
1152
  indexes: indexesByTable.get(tableName) ?? [],
1121
1153
  };
1122
1154
  }
1123
- // SQL Server has no first-class enum type — enums are CHECK constraints; left empty.
1155
+ // SQL Server has no first-class enum type, enums are CHECK constraints; left empty.
1124
1156
  return { tables, enums: {} };
1125
1157
  }
1126
1158
  /**
@@ -1136,7 +1168,10 @@ async function introspectMssql(options) {
1136
1168
  try {
1137
1169
  const mp = new MssqlPool(pool, sqlNS);
1138
1170
  const exec = async (sql, params) => (await mp.query(sql, params)).rows;
1139
- return introspectMssqlWith(exec, options.schema ?? 'dbo', {
1171
+ // `await` is LOAD-BEARING: `return somePromise` inside a try/finally runs
1172
+ // the finally BEFORE the promise settles, so `pool.close()` would tear the
1173
+ // connection down underneath the introspection queries.
1174
+ return await introspectMssqlWith(exec, options.schema ?? 'dbo', {
1140
1175
  include: options.include,
1141
1176
  exclude: options.exclude,
1142
1177
  });
@@ -1177,7 +1212,7 @@ async function loadMssql() {
1177
1212
  let mod;
1178
1213
  try {
1179
1214
  // `mssql` ships no bundled type declarations (it needs @types/mssql, which
1180
- // Turbine deliberately does not depend on) — the structural MssqlModule
1215
+ // Turbine deliberately does not depend on), the structural MssqlModule
1181
1216
  // above is our typed surface; the helper returns `unknown` so no TS7016.
1182
1217
  // Via the .cts helper so the CJS build keeps a path to a REAL dynamic
1183
1218
  // import() even if a future mssql major goes ESM-only (the CommonJS pass
@@ -1215,7 +1250,7 @@ function isMssqlPool(x) {
1215
1250
  * Pass one of:
1216
1251
  * - a connection string (`'mssql://sa:pass@host:1433/db'`),
1217
1252
  * - an `mssql` config object (`{ server, user, password, database, options }`),
1218
- * - an existing `MssqlPool` (injection — you own its lifecycle, `disconnect()` is
1253
+ * - an existing `MssqlPool` (injection, you own its lifecycle, `disconnect()` is
1219
1254
  * a no-op).
1220
1255
  *
1221
1256
  * When Turbine builds the pool (string/config), it probes
@@ -1,8 +1,8 @@
1
1
  /**
2
- * turbine-orm/mysql — MySQL 8 engine (driver-injected, optional peer)
2
+ * turbine-orm/mysql, MySQL 8 engine (driver-injected, optional peer)
3
3
  *
4
4
  * Binds Turbine to MySQL 8 via the `mysql2` driver. `mysql2` is **not** a root
5
- * dependency — it is an **optional peer**: `npm i turbine-orm` pulls nothing
5
+ * dependency, it is an **optional peer**: `npm i turbine-orm` pulls nothing
6
6
  * extra, and only consumers who `import 'turbine-orm/mysql'` install `mysql2`
7
7
  * themselves. The factory loads it through a dynamic `import('mysql2/promise')`
8
8
  * so importing this module never crashes when `mysql2` is absent for a consumer
@@ -21,7 +21,7 @@
21
21
  * (e.g. a `with`-relation `LIMIT` lands in the SELECT list, ahead of the
22
22
  * outer `WHERE`). Postgres reconciles this via numbered `$N`; positional `?`
23
23
  * silently mis-binds. So `mysqlDialect` uses **mysql2 named placeholders**
24
- * (`:p1`, `:p2`, …) and the driver shim binds via a `{ p1, p2, … }` object —
24
+ * (`:p1`, `:p2`, …) and the driver shim binds via a `{ p1, p2, … }` object -
25
25
  * exactly mirroring `$N` semantics regardless of text order. (See
26
26
  * `turbine-orm/sqlite` for the same fix.)
27
27
  *
@@ -42,10 +42,10 @@
42
42
  * - **Advisory-lock migration locking** is available in principle via
43
43
  * `GET_LOCK`/`RELEASE_LOCK` (`supportsAdvisoryLock = true`); the migrate CLI is
44
44
  * still PostgreSQL-only, so this flag documents intent for a future adapter.
45
- * - **Case-insensitive matching** uses `LOWER(col) LIKE LOWER(ref)` — note this
45
+ * - **Case-insensitive matching** uses `LOWER(col) LIKE LOWER(ref)`, note this
46
46
  * can defeat indexes unless a functional/generated index exists.
47
47
  * - **bignum:** mysql2 is configured `supportBigNumbers:true, bigNumberStrings:false`
48
- * — the same safe-int policy Turbine uses for Postgres `int8` (number when it
48
+ * - the same safe-int policy Turbine uses for Postgres `int8` (number when it
49
49
  * fits, decimal string otherwise). `DECIMAL` comes back as a string; `TINYINT(1)`
50
50
  * binds booleans as 1/0. No global parser state is mutated.
51
51
  * - **Version:** MySQL **8.0+** required (5.7 lacks `JSON_ARRAYAGG`); MariaDB is
@@ -92,7 +92,7 @@ type QueryArg = string | {
92
92
  * `SAVEPOINT` nesting all run on the same physical connection.
93
93
  */
94
94
  export declare class MysqlPool implements PgCompatPool {
95
- /** The underlying mysql2 pool — exposed as an escape hatch (seed / DDL / advanced ops). */
95
+ /** The underlying mysql2 pool, exposed as an escape hatch (seed / DDL / advanced ops). */
96
96
  readonly pool: Mysql2Pool;
97
97
  private closed;
98
98
  constructor(pool: Mysql2Pool);
@@ -109,7 +109,7 @@ export declare class MysqlPool implements PgCompatPool {
109
109
  export declare function mysqlTypeToTs(dialectType: string, nullable: boolean, columnType?: string): string;
110
110
  /**
111
111
  * MySQL 8 implementation of the {@link Dialect} contract. Backtick identifier
112
- * quoting, named `:pN` placeholders (NOT positional `?` — see the module
112
+ * quoting, named `:pN` placeholders (NOT positional `?`, see the module
113
113
  * docstring), `JSON_OBJECT` / `JSON_ARRAYAGG` for the single-query nested
114
114
  * relation engine (`CAST(… AS JSON)`-wrapped nested subresults), no `RETURNING`
115
115
  * (`resultStrategy = 'reselect'`), `INSERT … ON DUPLICATE KEY UPDATE` upserts,
@@ -156,7 +156,7 @@ export interface TurbineMysqlOptions extends Pick<TurbineConfig, 'logging' | 'de
156
156
  * Pass one of:
157
157
  * - a connection string (`'mysql://user:pass@host:3306/db'`),
158
158
  * - a mysql2 connection config object (`{ host, user, password, database }`), or
159
- * - an existing mysql2 pool / {@link MysqlPool} (injection — you own its lifecycle,
159
+ * - an existing mysql2 pool / {@link MysqlPool} (injection, you own its lifecycle,
160
160
  * `disconnect()` is a no-op, advanced config like SSL lives here).
161
161
  *
162
162
  * When Turbine builds the pool (string/config), it pins the correct mysql2 flags
package/dist/cjs/mysql.js CHANGED
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  /**
3
- * turbine-orm/mysql — MySQL 8 engine (driver-injected, optional peer)
3
+ * turbine-orm/mysql, MySQL 8 engine (driver-injected, optional peer)
4
4
  *
5
5
  * Binds Turbine to MySQL 8 via the `mysql2` driver. `mysql2` is **not** a root
6
- * dependency — it is an **optional peer**: `npm i turbine-orm` pulls nothing
6
+ * dependency, it is an **optional peer**: `npm i turbine-orm` pulls nothing
7
7
  * extra, and only consumers who `import 'turbine-orm/mysql'` install `mysql2`
8
8
  * themselves. The factory loads it through a dynamic `import('mysql2/promise')`
9
9
  * so importing this module never crashes when `mysql2` is absent for a consumer
@@ -22,7 +22,7 @@
22
22
  * (e.g. a `with`-relation `LIMIT` lands in the SELECT list, ahead of the
23
23
  * outer `WHERE`). Postgres reconciles this via numbered `$N`; positional `?`
24
24
  * silently mis-binds. So `mysqlDialect` uses **mysql2 named placeholders**
25
- * (`:p1`, `:p2`, …) and the driver shim binds via a `{ p1, p2, … }` object —
25
+ * (`:p1`, `:p2`, …) and the driver shim binds via a `{ p1, p2, … }` object -
26
26
  * exactly mirroring `$N` semantics regardless of text order. (See
27
27
  * `turbine-orm/sqlite` for the same fix.)
28
28
  *
@@ -43,10 +43,10 @@
43
43
  * - **Advisory-lock migration locking** is available in principle via
44
44
  * `GET_LOCK`/`RELEASE_LOCK` (`supportsAdvisoryLock = true`); the migrate CLI is
45
45
  * still PostgreSQL-only, so this flag documents intent for a future adapter.
46
- * - **Case-insensitive matching** uses `LOWER(col) LIKE LOWER(ref)` — note this
46
+ * - **Case-insensitive matching** uses `LOWER(col) LIKE LOWER(ref)`, note this
47
47
  * can defeat indexes unless a functional/generated index exists.
48
48
  * - **bignum:** mysql2 is configured `supportBigNumbers:true, bigNumberStrings:false`
49
- * — the same safe-int policy Turbine uses for Postgres `int8` (number when it
49
+ * - the same safe-int policy Turbine uses for Postgres `int8` (number when it
50
50
  * fits, decimal string otherwise). `DECIMAL` comes back as a string; `TINYINT(1)`
51
51
  * binds booleans as 1/0. No global parser state is mutated.
52
52
  * - **Version:** MySQL **8.0+** required (5.7 lacks `JSON_ARRAYAGG`); MariaDB is
@@ -108,7 +108,7 @@ function toMysqlParam(value) {
108
108
  /**
109
109
  * Bind the positional `params[]` (in 1-indexed generation order) to the named
110
110
  * `:p1`, `:p2`, … placeholders the dialect emits. Mapping by NAME makes binding
111
- * independent of where each placeholder lands in the SQL text — the same
111
+ * independent of where each placeholder lands in the SQL text, the same
112
112
  * guarantee Postgres' numbered `$N` gives. Returns `undefined` for parameter-less
113
113
  * statements (BEGIN/COMMIT/DDL) so they bind nothing.
114
114
  */
@@ -142,7 +142,7 @@ function shapeResult(result) {
142
142
  * deadlock / lock-wait-timeout become retryable (E012/E013). The original mysql2
143
143
  * error (with its real message) is preserved as the wrapped error's `.cause`
144
144
  * downstream. Returns the value unchanged when it is not a recognizable mysql2
145
- * error. We only annotate here — `wrapPgError` (invoked downstream in the query
145
+ * error. We only annotate here, `wrapPgError` (invoked downstream in the query
146
146
  * executor / transaction proxy) does the actual translation.
147
147
  */
148
148
  function augmentMysqlError(err) {
@@ -225,7 +225,7 @@ async function execOne(runner, sql, params) {
225
225
  * emits `SET TRANSACTION ISOLATION LEVEL <level>; START TRANSACTION`. mysql2 runs
226
226
  * one statement per call (multi-statements stay OFF by design), so split exactly
227
227
  * that dialect-generated compound and run the parts in order. Gated on the precise
228
- * transaction-control prefix — the query builder never emits `SET TRANSACTION
228
+ * transaction-control prefix, the query builder never emits `SET TRANSACTION
229
229
  * ISOLATION LEVEL`, so no builder/user SQL is ever split.
230
230
  */
231
231
  async function runOnConnection(conn, sql, params) {
@@ -247,7 +247,7 @@ async function runOnConnection(conn, sql, params) {
247
247
  * `SAVEPOINT` nesting all run on the same physical connection.
248
248
  */
249
249
  class MysqlPool {
250
- /** The underlying mysql2 pool — exposed as an escape hatch (seed / DDL / advanced ops). */
250
+ /** The underlying mysql2 pool, exposed as an escape hatch (seed / DDL / advanced ops). */
251
251
  pool;
252
252
  closed = false;
253
253
  constructor(pool) {
@@ -359,11 +359,11 @@ function mysqlColumnType(type, maxLength) {
359
359
  return type;
360
360
  }
361
361
  // ---------------------------------------------------------------------------
362
- // mysqlDialect — the full Dialect contract for MySQL 8
362
+ // mysqlDialect, the full Dialect contract for MySQL 8
363
363
  // ---------------------------------------------------------------------------
364
364
  /**
365
365
  * MySQL 8 implementation of the {@link Dialect} contract. Backtick identifier
366
- * quoting, named `:pN` placeholders (NOT positional `?` — see the module
366
+ * quoting, named `:pN` placeholders (NOT positional `?`, see the module
367
367
  * docstring), `JSON_OBJECT` / `JSON_ARRAYAGG` for the single-query nested
368
368
  * relation engine (`CAST(… AS JSON)`-wrapped nested subresults), no `RETURNING`
369
369
  * (`resultStrategy = 'reselect'`), `INSERT … ON DUPLICATE KEY UPDATE` upserts,
@@ -407,7 +407,7 @@ exports.mysqlDialect = {
407
407
  jsonPathSupport: 'function',
408
408
  emptyJsonArrayLiteral: 'JSON_ARRAY()',
409
409
  nullJsonLiteral: 'NULL',
410
- // Named placeholders (`:p1`, `:p2`, …) — NOT positional `?`. Turbine pushes
410
+ // Named placeholders (`:p1`, `:p2`, …), NOT positional `?`. Turbine pushes
411
411
  // params in 1-indexed generation order but may EMIT them in a different SQL
412
412
  // text position; positional `?` mis-binds. mysql2 named parameters bind by
413
413
  // name, so `:pN` ↔ `params[N-1]` mirrors Postgres' `$N` regardless of text
@@ -444,7 +444,7 @@ exports.mysqlDialect = {
444
444
  * Wrap a nested correlated subquery for embedding inside a parent `JSON_OBJECT`.
445
445
  * MySQL double-encodes a scalar subquery result as a JSON *string* unless it is
446
446
  * explicitly typed JSON, so `CAST((subquery) AS JSON)` forces the nested value
447
- * to be embedded as real JSON (objects/arrays), not a quoted string — the same
447
+ * to be embedded as real JSON (objects/arrays), not a quoted string, the same
448
448
  * intent as SQLite's `json(...)` wrap. The fallback (`JSON_ARRAY()` / `NULL`) is
449
449
  * already JSON-typed.
450
450
  */
@@ -453,6 +453,40 @@ exports.mysqlDialect = {
453
453
  },
454
454
  // MySQL aggregate casts: COUNT → SIGNED (BIGINT, comes back as a JS number when
455
455
  // safe), AVG/float → DECIMAL (string, the aggregate transform Number()-coerces).
456
+ jsonWireRule(columnType) {
457
+ const t = columnType.toLowerCase();
458
+ // JSON_OBJECT renders these as JSON numbers, which are IEEE doubles, so a
459
+ // relation read through the join strategy disagreed with a top-level read
460
+ // and with the batched loader, LOSSILY. Measured: BIGINT 9007199254740993
461
+ // came back 9007199254740992, and DECIMAL '1000.50' came back 1000.5.
462
+ // mysql2 hands both back as STRINGS at top level (supportBigNumbers, and
463
+ // DECIMAL is always a string), so carry the text and keep it.
464
+ if (t === 'bigint') {
465
+ return {
466
+ sql: (ref) => `CAST(${ref} AS CHAR)`,
467
+ decode: (value) => {
468
+ // Same policy as the driver's supportBigNumbers/bigNumberStrings:false.
469
+ if (typeof value !== 'string' || !/^-?\d+$/.test(value))
470
+ return value;
471
+ const asNumber = Number(value);
472
+ return Number.isSafeInteger(asNumber) ? asNumber : value;
473
+ },
474
+ };
475
+ }
476
+ if (t === 'decimal' || t === 'numeric') {
477
+ return { sql: (ref) => `CAST(${ref} AS CHAR)`, decode: (value) => value };
478
+ }
479
+ // Binary columns are worse than lossy: JSON_OBJECT emits MySQL's internal
480
+ // `base64:type15:…` marker string, so the caller got that text instead of
481
+ // bytes. Carry hex and rebuild the buffer.
482
+ if (t === 'binary' || t === 'varbinary' || t.endsWith('blob')) {
483
+ return {
484
+ sql: (ref) => `HEX(${ref})`,
485
+ decode: (value) => (typeof value === 'string' ? Uint8Array.from(Buffer.from(value, 'hex')) : value),
486
+ };
487
+ }
488
+ return undefined;
489
+ },
456
490
  castAggregate(expr, target) {
457
491
  return `CAST(${expr} AS ${target === 'int' ? 'SIGNED' : 'DECIMAL(65,30)'})`;
458
492
  },
@@ -468,7 +502,7 @@ exports.mysqlDialect = {
468
502
  inClauseParam(values) {
469
503
  return JSON.stringify(values ?? []);
470
504
  },
471
- // No RETURNING — resultStrategy 'reselect' re-fetches the row.
505
+ // No RETURNING, resultStrategy 'reselect' re-fetches the row.
472
506
  buildReturningClause() {
473
507
  return '';
474
508
  },
@@ -476,7 +510,7 @@ exports.mysqlDialect = {
476
510
  return `INSERT INTO ${input.table} (${input.columns.join(', ')}) VALUES (${input.valuePlaceholders.join(', ')})`;
477
511
  },
478
512
  buildBulkInsertStatement(input) {
479
- // No UNNEST in MySQL — emit multi-row VALUES with flattened, named
513
+ // No UNNEST in MySQL, emit multi-row VALUES with flattened, named
480
514
  // placeholders (`:p1`, `:p2`, …) matching the flat param order.
481
515
  let n = 0;
482
516
  const placeholders = input.rowValues
@@ -491,7 +525,7 @@ exports.mysqlDialect = {
491
525
  };
492
526
  },
493
527
  buildUpsertStatement(input) {
494
- // MySQL ignores the explicit conflict target — ON DUPLICATE KEY UPDATE keys
528
+ // MySQL ignores the explicit conflict target, ON DUPLICATE KEY UPDATE keys
495
529
  // off the table's PK/unique indexes. The `where`-derived conflictColumns
496
530
  // (input.conflictColumns) must therefore correspond to a real unique/PK index
497
531
  // for the upsert to target the intended row (plan §4 / R9).
@@ -600,7 +634,7 @@ const num = (v) => (typeof v === 'string' ? Number(v) : (v ?? 0));
600
634
  * (`deriveEngineRelations` → `buildRelationsFromForeignKeys` +
601
635
  * `addAutoManyToManyRelations` in introspect.ts), so this engine derives
602
636
  * IDENTICAL relation names to `turbine generate` against Postgres for the
603
- * same logical schema — legacy-first naming, per-column disambiguation, and
637
+ * same logical schema, legacy-first naming, per-column disambiguation, and
604
638
  * collision resolution against scalar column fields included.
605
639
  */
606
640
  function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable) {
@@ -787,9 +821,13 @@ async function introspectMysql(options) {
787
821
  schemaName = rows[0]?.db ?? '';
788
822
  }
789
823
  if (!schemaName) {
790
- throw new errors_js_1.ConnectionError('[turbine] Could not determine the MySQL database to introspect — pass a database in the connection string or a `schema` option.');
824
+ throw new errors_js_1.ConnectionError('[turbine] Could not determine the MySQL database to introspect, pass a database in the connection string or a `schema` option.');
791
825
  }
792
- return introspectMysqlWith(exec, schemaName, { include: options.include, exclude: options.exclude });
826
+ // `await` is LOAD-BEARING, not stylistic: `return somePromise` inside a
827
+ // try/finally runs the finally BEFORE the promise settles, so `pool.end()`
828
+ // closed the pool out from under every introspection query and the whole
829
+ // call failed with mysql2's "Pool is closed".
830
+ return await introspectMysqlWith(exec, schemaName, { include: options.include, exclude: options.exclude });
793
831
  }
794
832
  finally {
795
833
  await pool.end();
@@ -800,7 +838,7 @@ async function introspectMysql(options) {
800
838
  // ---------------------------------------------------------------------------
801
839
  /** mysql2 connection flags Turbine pins for correct behavior. */
802
840
  const MYSQL_DRIVER_FLAGS = {
803
- // Named placeholders (`:pN`) — see paramPlaceholder + toNamedBinding.
841
+ // Named placeholders (`:pN`), see paramPlaceholder + toNamedBinding.
804
842
  namedPlaceholders: true,
805
843
  // Safe-int policy matching Postgres int8: number when it fits, decimal string
806
844
  // otherwise. DECIMAL always comes back as a string. No global state mutated.
@@ -886,7 +924,7 @@ function isMysql2Pool(x) {
886
924
  * Pass one of:
887
925
  * - a connection string (`'mysql://user:pass@host:3306/db'`),
888
926
  * - a mysql2 connection config object (`{ host, user, password, database }`), or
889
- * - an existing mysql2 pool / {@link MysqlPool} (injection — you own its lifecycle,
927
+ * - an existing mysql2 pool / {@link MysqlPool} (injection, you own its lifecycle,
890
928
  * `disconnect()` is a no-op, advanced config like SSL lives here).
891
929
  *
892
930
  * When Turbine builds the pool (string/config), it pins the correct mysql2 flags
@@ -918,12 +956,12 @@ async function turbineMysql(target, schema, options = {}) {
918
956
  });
919
957
  // Every NEW physical connection: disable backslash string escaping so the
920
958
  // builder's `LIKE … ESCAPE '\'` clause (a single literal backslash) is valid
921
- // MySQL — by default MySQL would parse `'\'` as an escaped quote (syntax
959
+ // MySQL, by default MySQL would parse `'\'` as an escaped quote (syntax
922
960
  // error). This also makes string-literal semantics match Postgres. Turbine
923
961
  // parameterizes every value, so no behavior depends on backslash escaping.
924
962
  //
925
963
  // NOTE: even on a `mysql2/promise` pool, the 'connection' event yields the
926
- // *raw* (callback-style) connection — its `.query()` returns a non-thenable
964
+ // *raw* (callback-style) connection, its `.query()` returns a non-thenable
927
965
  // `Query`, so `.catch()`/`await` on it throws "result of query that is not a
928
966
  // promise". Use the callback form here (fire-and-forget): if the SET fails we
929
967
  // fall back to MySQL's default escaping, harmless since every value is a param.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * turbine-orm — Nested write engine
2
+ * turbine-orm, Nested write engine
3
3
  *
4
4
  * Tree-walking create/update that resolves relation fields in `data` into
5
5
  * batched SQL operations within a transaction. Supports create, connect,
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * This module is imported by `query/builder.ts` when the `data` argument
10
10
  * of `create()` or `update()` contains relation fields. It never imports
11
- * `client.ts` directly — the transaction handle is passed in via
11
+ * `client.ts` directly, the transaction handle is passed in via
12
12
  * `NestedWriteContext`.
13
13
  */
14
14
  import type { RelationDef, SchemaMetadata, TableMetadata } from './schema.js';
@@ -22,6 +22,25 @@ export interface ExtractedFields {
22
22
  */
23
23
  export interface NestedWriteContext {
24
24
  schema: SchemaMetadata;
25
+ /**
26
+ * Refuse a `connect` / `connectOrCreate` that would RE-PARENT a to-many
27
+ * child already owned by a different parent. Off by default.
28
+ *
29
+ * `connect: { id: 42 }` on a to-many relation means "make row 42 mine", and
30
+ * it does that unconditionally: if another parent owns row 42, the row is
31
+ * silently taken from them. A handler that forwards a client-supplied id
32
+ * into a nested connect therefore hands any caller a cross-tenant write
33
+ * primitive, and the only defense is a hand-rolled ownership check at every
34
+ * call site. With this on, connecting a child whose foreign key already
35
+ * points at a DIFFERENT parent raises E003; connecting an unowned child
36
+ * (null FK) or one this parent already owns still succeeds, so the
37
+ * legitimate uses are untouched.
38
+ *
39
+ * Applies to `hasMany` / `hasOne` only. A `belongsTo` connect points the row
40
+ * being written at a parent, which takes nothing from anyone, and a
41
+ * many-to-many connect adds a junction row rather than moving one.
42
+ */
43
+ scopedConnect?: boolean;
25
44
  tx: {
26
45
  table<T extends object>(name: string): {
27
46
  create(args: {