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/powql.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * PowqlInterface — Turbine's PowQL query generator (the PowDB analogue of
2
+ * PowqlInterface, Turbine's PowQL query generator (the PowDB analogue of
3
3
  * {@link QueryInterface}). It exposes the same public method surface as the SQL
4
- * `QueryInterface` (`findMany`, `create`, `update`, …) but emits **PowQL** — a
5
- * pipeline language, not SQL — executed through {@link PowdbPool}.
4
+ * `QueryInterface` (`findMany`, `create`, `update`, …) but emits **PowQL**, a
5
+ * pipeline language, not SQL, executed through {@link PowdbPool}.
6
6
  *
7
7
  * It is a *parallel* implementation rather than a `Dialect` of the SQL builder:
8
8
  * PowQL's grammar (`T filter <e> order <k> { .col }`) shares no surface with
@@ -14,14 +14,14 @@
14
14
  * server):
15
15
  * - `create`/`createMany`/`update`/`delete` use PowDB 0.7.0's trailing
16
16
  * `returning` keyword (`RETURNING *`, all columns) to surface affected rows
17
- * in one round-trip. `upsert` is the lone exception — its statement does not
17
+ * in one round-trip. `upsert` is the lone exception, its statement does not
18
18
  * accept `returning`, so it reselects the row by PK (a composite-PK upsert
19
19
  * reselects-or-writes inside one flat transaction).
20
20
  * - The PK is server-assigned when the column is `isGenerated` (PowDB's `auto`
21
- * int — read back via `returning`); otherwise a defaulted **string** PK is
21
+ * int, read back via `returning`); otherwise a defaulted **string** PK is
22
22
  * generated client-side (UUID).
23
- * - `with` (nested relations) uses **batched N+1 loaders** — D round-trips for
24
- * depth D, not one query — including manyToMany (junction → targets).
23
+ * - `with` (nested relations) uses **batched N+1 loaders**, D round-trips for
24
+ * depth D, not one query, including manyToMany (junction → targets).
25
25
  * - **Relation filters** (`some`/`none`/`every`, all cardinalities incl. m2m)
26
26
  * are resolved client-side to a literal `in (…)` list, never an IN-subquery:
27
27
  * PowDB's executor caches a subquery's result by plan shape and would return
@@ -30,7 +30,7 @@
30
30
  * shared nested-write engine as one flat top-level transaction (PowDB is
31
31
  * single-writer, no savepoints).
32
32
  * - pgvector / JSON / array filters and cursor streaming throw
33
- * {@link UnsupportedFeatureError} (E017) — they have no PowDB equivalent.
33
+ * {@link UnsupportedFeatureError} (E017), they have no PowDB equivalent.
34
34
  *
35
35
  * @module
36
36
  */
@@ -42,6 +42,7 @@ import { assertAggregatePiiOptIn } from './query/aggregates.js';
42
42
  import { expandCompoundUniqueWhere } from './query/compound-unique.js';
43
43
  import { isJsonFilter, isRelationPickOrderBy, orderByEntries } from './query/filters.js';
44
44
  import { escapeLike } from './query/utils.js';
45
+ import { assertJsonFilterKeys, jsonStringEntries } from './query/where.js';
45
46
  import { normalizeKeyColumns, snakeToCamel, } from './schema.js';
46
47
  /**
47
48
  * Max parent keys per relation-loader `in (…)` query. A `with` over a large
@@ -88,7 +89,7 @@ const POWQL_WRITE_ACTIONS = new Set([
88
89
  * relation before compiling to it. Keyed on the pool object identity so every
89
90
  * table interface over the same connection shares one snapshot; a WeakMap lets a
90
91
  * discarded pool's snapshot be collected. A fetch failure caches `[]` (a missing
91
- * listing means "no verifiable links" — a silent fallback to loaders, never an
92
+ * listing means "no verifiable links", a silent fallback to loaders, never an
92
93
  * error).
93
94
  */
94
95
  const LINK_SNAPSHOT_CACHE = new WeakMap();
@@ -227,8 +228,8 @@ export class PowqlInterface {
227
228
  return `$${params.length}`;
228
229
  }
229
230
  /**
230
- * Render a value for a write *assignment* (`col := …`). Every value — float
231
- * columns included — is sent as a positional `$N` param. PowDB ≥ 0.7.0 fixed
231
+ * Render a value for a write *assignment* (`col := …`). Every value, float
232
+ * columns included, is sent as a positional `$N` param. PowDB ≥ 0.7.0 fixed
232
233
  * the int→float UPDATE coercion bug (`score := $n` with an integer param now
233
234
  * reads back the integer value, not the raw i64 bits), so the float-literal
234
235
  * inlining workaround Turbine carried for ≤ 0.6.2 is gone. Marks the column as
@@ -259,7 +260,7 @@ export class PowqlInterface {
259
260
  get capabilities() {
260
261
  return this.pool.capabilities ?? ALL_POWDB_CAPABILITIES;
261
262
  }
262
- /** A predicate that is always false — the empty-`in` / contradiction sentinel. */
263
+ /** A predicate that is always false, the empty-`in` / contradiction sentinel. */
263
264
  alwaysFalse() {
264
265
  const pk = this.meta.primaryKey[0] ?? this.meta.columns[0]?.name;
265
266
  return `(.${pk} is null and .${pk} is not null)`;
@@ -302,7 +303,7 @@ export class PowqlInterface {
302
303
  }
303
304
  else if (this.meta.relations[key]) {
304
305
  // Relation filters are pre-resolved to scalar in/notIn by
305
- // resolveRelationFilters() before buildWhere runs — reaching here means
306
+ // resolveRelationFilters() before buildWhere runs, reaching here means
306
307
  // a caller skipped that step (an internal bug, not user error).
307
308
  throw new ValidationError(`[turbine] internal: relation filter "${key}" reached buildWhere unresolved (missing resolveRelationFilters()).`);
308
309
  }
@@ -336,7 +337,7 @@ export class PowqlInterface {
336
337
  }
337
338
  rejectUnsupportedFilter(op, field);
338
339
  if (!Object.keys(op).some((k) => OPERATOR_KEYS.has(k))) {
339
- // A bare object that is not an operator set — equality by value.
340
+ // A bare object that is not an operator set, equality by value.
340
341
  return `${ref} = ${this.param(value, params)}`;
341
342
  }
342
343
  const insensitive = op.mode === 'insensitive';
@@ -434,6 +435,10 @@ export class PowqlInterface {
434
435
  * by the empty-where guard.
435
436
  */
436
437
  buildJsonPathCondition(col, filter, params, alias) {
438
+ // Same strict-key check as the SQL path. Without it an unrecognized
439
+ // operator compiled to zero conditions and the predicate silently
440
+ // disappeared, returning every row (see assertJsonFilterKeys).
441
+ assertJsonFilterKeys(filter, col.name);
437
442
  const conds = [];
438
443
  // Bind the path segments at most once and reuse the expression string across
439
444
  // equals + range comparisons (they share the same `path`).
@@ -479,8 +484,20 @@ export class PowqlInterface {
479
484
  }
480
485
  conds.push(`${pathP()} ${powOp} ${this.param(v, params)}`);
481
486
  }
482
- if (!conds.length)
483
- return '';
487
+ // Substring comparisons against the text at `path`, in the same fixed
488
+ // order as the SQL path. PowQL has `like`, so these compile natively.
489
+ for (const { pattern, value } of jsonStringEntries(filter, col.name)) {
490
+ const insensitive = filter.mode === 'insensitive';
491
+ const lhs = insensitive ? `lower(${pathP()})` : pathP();
492
+ conds.push(`${lhs} like ${this.bindLike(pattern(escapeLike(value)), params, insensitive)}`);
493
+ }
494
+ // Every reachable shape now emits a condition or throws: assertJsonFilterKeys
495
+ // rejects a filter that compares nothing, so an empty list here would be a
496
+ // generator bug rather than a user error, and must not silently widen the
497
+ // query the way it used to.
498
+ if (!conds.length) {
499
+ throw new ValidationError(`[turbine] internal: JSON filter on "${col.name}" compiled to no condition. This is a bug in turbine-orm.`);
500
+ }
484
501
  return conds.length > 1 ? `(${conds.join(' and ')})` : conds[0];
485
502
  }
486
503
  /** Bind a value, lowercasing for case-insensitive comparisons. */
@@ -493,7 +510,7 @@ export class PowqlInterface {
493
510
  const ph = this.param(pattern, params);
494
511
  return insensitive ? `lower(${ph})` : ph;
495
512
  }
496
- /** `lhs [not] in ($1, $2, …)` — empty list collapses to a constant. */
513
+ /** `lhs [not] in ($1, $2, …)`, empty list collapses to a constant. */
497
514
  buildInList(lhs, values, params, insensitive, negate) {
498
515
  if (!Array.isArray(values) || values.length === 0) {
499
516
  // `in []` matches nothing; `not in []` matches everything (SQL parity requires a
@@ -520,7 +537,7 @@ export class PowqlInterface {
520
537
  * so a second subquery of the same shape with a different value returns the
521
538
  * first one's stale rows (reproduced live on the embedded engine; the
522
539
  * single-statement literal `in (list)` form is always correct). Resolving
523
- * client-side trades extra round-trips for correctness, and recurses — nested
540
+ * client-side trades extra round-trips for correctness, and recurses, nested
524
541
  * relation filters in the inner predicate resolve when the target query runs.
525
542
  */
526
543
  async resolveRelationFilters(where, timeout) {
@@ -562,7 +579,7 @@ export class PowqlInterface {
562
579
  const fk = normalizeKeyColumns(rel.foreignKey);
563
580
  const rk = normalizeKeyColumns(rel.referenceKey);
564
581
  if (fk.length > 1 || rk.length > 1) {
565
- throw new UnsupportedFeatureError('composite-key relation filters', 'PowDB', `relation "${rel.name}" uses a composite key — PowQL has no tuple-\`in\` to express it`);
582
+ throw new UnsupportedFeatureError('composite-key relation filters', 'PowDB', `relation "${rel.name}" uses a composite key, PowQL has no tuple-\`in\` to express it`);
566
583
  }
567
584
  const targetMeta = this.schema.tables[rel.to];
568
585
  if (!targetMeta)
@@ -603,7 +620,7 @@ export class PowqlInterface {
603
620
  if (!targetMeta)
604
621
  throw new ValidationError(`[turbine] Relation "${rel.name}" targets unknown table "${rel.to}".`);
605
622
  if (sourceJ.length > 1 || targetJ.length > 1 || sourceRef.length > 1 || targetMeta.primaryKey.length > 1) {
606
- throw new UnsupportedFeatureError('composite-key manyToMany filters', 'PowDB', `relation "${rel.name}" — PowQL has no tuple-\`in\` for composite junction/target keys`);
623
+ throw new UnsupportedFeatureError('composite-key manyToMany filters', 'PowDB', `relation "${rel.name}", PowQL has no tuple-\`in\` for composite junction/target keys`);
607
624
  }
608
625
  const sourceJCol = sourceJ[0];
609
626
  const targetJCol = targetJ[0];
@@ -621,7 +638,7 @@ export class PowqlInterface {
621
638
  });
622
639
  return [...new Set(rows.map((r) => r[targetPkField]).filter((v) => v != null))];
623
640
  };
624
- // Junction source keys linking any of `targetPks` (literal IN-list — never a subquery).
641
+ // Junction source keys linking any of `targetPks` (literal IN-list, never a subquery).
625
642
  const sourcesForTargets = async (targetPks) => {
626
643
  if (!targetPks.length)
627
644
  return [];
@@ -769,7 +786,7 @@ export class PowqlInterface {
769
786
  }
770
787
  return `${this.ref(field, alias)} ${spec.sort === 'desc' ? 'desc' : 'asc'}`;
771
788
  }
772
- // Name the actual feature in the refusal — a pick-row ordering
789
+ // Name the actual feature in the refusal, a pick-row ordering
773
790
  // reported as "vector / distance ordering" sends users hunting for
774
791
  // pgvector docs. Everything else stays E017 on PowDB.
775
792
  const feature = isRelationPickOrderBy(dir)
@@ -951,8 +968,8 @@ export class PowqlInterface {
951
968
  *
952
969
  * When the engine supports nested projections (>= 0.18) and the strategy
953
970
  * does not opt out, eligible `with` relations compile INTO this statement as
954
- * nested-projection blocks (`nestedPlans`) — one round-trip for the whole
955
- * shape — and only the ineligible remainder (`residualWith`) goes to the
971
+ * nested-projection blocks (`nestedPlans`), one round-trip for the whole
972
+ * shape, and only the ineligible remainder (`residualWith`) goes to the
956
973
  * post-execution loaders. Without nesting the emitted PowQL is byte-identical
957
974
  * to the pre-0.18 output (no alias, `.col` refs).
958
975
  */
@@ -1110,7 +1127,7 @@ export class PowqlInterface {
1110
1127
  return row;
1111
1128
  }
1112
1129
  // -------------------------------------------------------------------------
1113
- // Nested relations — batched N+1 loaders (hasMany / hasOne / belongsTo)
1130
+ // Nested relations, batched N+1 loaders (hasMany / hasOne / belongsTo)
1114
1131
  // -------------------------------------------------------------------------
1115
1132
  /**
1116
1133
  * Load each requested relation for `parents` and attach it onto each row.
@@ -1242,7 +1259,7 @@ export class PowqlInterface {
1242
1259
  }
1243
1260
  }
1244
1261
  /**
1245
- * manyToMany nested read — a three-hop batched loader (no `json_agg`/join
1262
+ * manyToMany nested read, a three-hop batched loader (no `json_agg`/join
1246
1263
  * pushdown): (1) read the junction rows for all parents in `sourceKey in (…)`
1247
1264
  * chunks, (2) read the target rows for the collected `targetKey`s, (3) stitch
1248
1265
  * each parent → its junction rows → its targets in memory. Mirrors the
@@ -1260,7 +1277,7 @@ export class PowqlInterface {
1260
1277
  if (!targetMeta)
1261
1278
  throw new ValidationError(`[turbine] Relation "${relName}" targets unknown table "${rel.to}".`);
1262
1279
  if (sourceJ.length > 1 || targetJ.length > 1 || sourceRef.length > 1 || targetMeta.primaryKey.length > 1) {
1263
- throw new UnsupportedFeatureError('composite-key manyToMany', 'PowDB', `relation "${relName}" — PowQL has no tuple-\`in\`, so composite junction/target keys can't be loaded`);
1280
+ throw new UnsupportedFeatureError('composite-key manyToMany', 'PowDB', `relation "${relName}", PowQL has no tuple-\`in\`, so composite junction/target keys can't be loaded`);
1264
1281
  }
1265
1282
  const sourceJCol = sourceJ[0];
1266
1283
  const targetJCol = targetJ[0];
@@ -1706,7 +1723,7 @@ export class PowqlInterface {
1706
1723
  /**
1707
1724
  * Shape the nested JSON children back into typed entities on every parent
1708
1725
  * row. The nested field arrives as a decoded JSON array on the native wire
1709
- * (or JSON text on the legacy wire — parsed here); its values are real JSON
1726
+ * (or JSON text on the legacy wire, parsed here); its values are real JSON
1710
1727
  * types, so each child object goes through the NATIVE coercion policy
1711
1728
  * (`rowToEntity(…, true)`: a date column's micros number becomes a `Date`, a
1712
1729
  * json column's document passes through, a str `"null"` stays a string).
@@ -1803,7 +1820,7 @@ export class PowqlInterface {
1803
1820
  * must stay on the loaders (ALWAYS a silent fallback with identical output).
1804
1821
  *
1805
1822
  * SCOPED TIGHT: this fires ONLY for a to-one relation whose child projection
1806
- * includes a bigint/bytes column — exactly the case a JSON nested block cannot
1823
+ * includes a bigint/bytes column, exactly the case a JSON nested block cannot
1807
1824
  * carry, so nested projections have already fallen back to a per-relation loader
1808
1825
  * (`planNestedRelation` returned `null` for the same shape). Cases nested
1809
1826
  * projections DO serve keep nested projections: link-bearing statements are
@@ -1811,9 +1828,9 @@ export class PowqlInterface {
1811
1828
  * link path would regress a hot path for no gain. Requires: single-column
1812
1829
  * belongsTo; no relation `with` / `where` / `distinct` / `orderBy` /
1813
1830
  * `limit` / `offset` (a scalar path has no per-hop filter/order and cannot
1814
- * reproduce those — such inputs stay on the loader for exact parity); a link
1831
+ * reproduce those, such inputs stay on the loader for exact parity); a link
1815
1832
  * name and all projected columns that are bare identifiers (a quoted segment in
1816
- * a dotted link path is outside the verified spelling — fall back); and a
1833
+ * a dotted link path is outside the verified spelling, fall back); and a
1817
1834
  * DECLARED link that verifiably matches (`findMatchingLink`).
1818
1835
  */
1819
1836
  async planLinkPathRelation(relName, rel, opt, includePii, parentCols, index) {
@@ -1844,7 +1861,7 @@ export class PowqlInterface {
1844
1861
  });
1845
1862
  if (!hasCarrierBlockedCol)
1846
1863
  return null;
1847
- // All cheap checks passed: NOW fetch (and cache) the link snapshot — never
1864
+ // All cheap checks passed: NOW fetch (and cache) the link snapshot, never
1848
1865
  // before, so a query with no link-path candidate issues no `schema links`.
1849
1866
  const link = this.findMatchingLink(await this.linksSnapshot(), rel);
1850
1867
  if (!link)
@@ -1875,7 +1892,7 @@ export class PowqlInterface {
1875
1892
  }
1876
1893
  /**
1877
1894
  * Reconstruct each link-path relation's child entity from its flat hop fields
1878
- * and attach it under the relation name — output indistinguishable from the
1895
+ * and attach it under the relation name, output indistinguishable from the
1879
1896
  * loader (same keys, same coercions). Presence: the target PK cell arriving
1880
1897
  * Empty (a null/dangling FK at the hop) means no linked row → `null`, matching
1881
1898
  * the loader's `matches[0] ?? null`. Otherwise the gathered snake cells go
@@ -1905,7 +1922,7 @@ export class PowqlInterface {
1905
1922
  }
1906
1923
  }
1907
1924
  // -------------------------------------------------------------------------
1908
- // Writes (reselect — PowDB has no RETURNING)
1925
+ // Writes (reselect, PowDB has no RETURNING)
1909
1926
  // -------------------------------------------------------------------------
1910
1927
  /** Split `data` into scalar assignments; reject relation (nested-write) keys. */
1911
1928
  scalarData(data) {
@@ -1914,7 +1931,7 @@ export class PowqlInterface {
1914
1931
  if (value === undefined)
1915
1932
  continue;
1916
1933
  if (this.meta.relations[field]) {
1917
- throw new UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}" — nested writes need create()/update(), not createMany()/upsert()`);
1934
+ throw new UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update(), not createMany()/upsert()`);
1918
1935
  }
1919
1936
  out.push({ col: this.column(field), value });
1920
1937
  }
@@ -1923,8 +1940,8 @@ export class PowqlInterface {
1923
1940
  /**
1924
1941
  * Fill in a client-generated UUID for a defaulted **string** PK that wasn't
1925
1942
  * supplied. A server-generated PK ({@link ColumnMetadata.isGenerated}, e.g. an
1926
- * `int` column with PowDB's `auto` modifier) is left untouched — PowDB assigns
1927
- * it and the trailing `returning` reads it back — as is any non-string PK.
1943
+ * `int` column with PowDB's `auto` modifier) is left untouched, PowDB assigns
1944
+ * it and the trailing `returning` reads it back, as is any non-string PK.
1928
1945
  */
1929
1946
  applyPkDefault(data) {
1930
1947
  const out = { ...data };
@@ -1941,7 +1958,7 @@ export class PowqlInterface {
1941
1958
  return out;
1942
1959
  }
1943
1960
  /**
1944
- * The table name as a PowQL type reference — backtick-quoted when it is a
1961
+ * The table name as a PowQL type reference, backtick-quoted when it is a
1945
1962
  * reserved word (e.g. a table named `order`). Used in every emitted
1946
1963
  * statement; plain `this.table` stays in error messages.
1947
1964
  */
@@ -2024,7 +2041,7 @@ export class PowqlInterface {
2024
2041
  if (value === undefined)
2025
2042
  continue;
2026
2043
  if (this.meta.relations[field]) {
2027
- throw new UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}" — nested writes need create()/update(), not updateMany()/upsert()`);
2044
+ throw new UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update(), not updateMany()/upsert()`);
2028
2045
  }
2029
2046
  const colMeta = this.column(field);
2030
2047
  const ref = this.ref(field);
@@ -2053,11 +2070,11 @@ export class PowqlInterface {
2053
2070
  return parts.join(', ');
2054
2071
  }
2055
2072
  // -------------------------------------------------------------------------
2056
- // Nested writes — create/update whose `data` carries relation ops (create,
2073
+ // Nested writes, create/update whose `data` carries relation ops (create,
2057
2074
  // connect, connectOrCreate, disconnect, set, delete, update, upsert). Reuses
2058
2075
  // the engine-agnostic nested-write engine (it only needs ctx.schema + the
2059
2076
  // table accessors PowqlInterface already provides). Runs as ONE flat top-level
2060
- // PowDB transaction — single global write lock, no savepoints, so the whole
2077
+ // PowDB transaction, single global write lock, no savepoints, so the whole
2061
2078
  // tree commits or rolls back together (mirrors the SQL path's coverage:
2062
2079
  // hasMany / hasOne / belongsTo; manyToMany nested writes are not handled by
2063
2080
  // the shared engine on any backend).
@@ -2100,9 +2117,13 @@ export class PowqlInterface {
2100
2117
  // Pass the PowDB pool so its read-only guard + capabilities carry into
2101
2118
  // the transaction-scoped proxy pool (see createTxPool).
2102
2119
  this.pool);
2103
- const ctx = { schema: this.schema, tx: tx };
2120
+ const ctx = {
2121
+ schema: this.schema,
2122
+ tx: tx,
2123
+ scopedConnect: this.options?.scopedConnect === true,
2124
+ };
2104
2125
  // Plant the single-writer re-entrancy marker for the implicit tx's
2105
- // subtree (same seam TurbineClient.$transaction uses) — user code that
2126
+ // subtree (same seam TurbineClient.$transaction uses), user code that
2106
2127
  // fires db.$transaction from inside (e.g. $use middleware around a
2107
2128
  // nested-write child op) must fast-fail E017, not queue into deadlock.
2108
2129
  const wrap = client
@@ -2112,7 +2133,7 @@ export class PowqlInterface {
2112
2133
  return result;
2113
2134
  }
2114
2135
  catch (err) {
2115
- // Only roll back a transaction we actually opened — a failed BEGIN
2136
+ // Only roll back a transaction we actually opened, a failed BEGIN
2116
2137
  // (queue timeout, re-entrancy E017) must not emit a stray ROLLBACK that
2117
2138
  // could land inside another transaction on a shared engine handle.
2118
2139
  if (began) {
@@ -2120,7 +2141,7 @@ export class PowqlInterface {
2120
2141
  await client.query(d?.rollbackStatement?.() ?? 'rollback');
2121
2142
  }
2122
2143
  catch {
2123
- /* best-effort — the connection may be gone */
2144
+ /* best-effort, the connection may be gone */
2124
2145
  }
2125
2146
  }
2126
2147
  throw err;
@@ -2143,7 +2164,7 @@ export class PowqlInterface {
2143
2164
  const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
2144
2165
  const where = this.buildWhere(resolvedWhere, params);
2145
2166
  this.assertCompiledWhere(where, false, 'delete');
2146
- // `returning` hands back the deleted row(s) — no separate pre-image reselect needed.
2167
+ // `returning` hands back the deleted row(s), no separate pre-image reselect needed.
2147
2168
  const { rows, native } = await this.exec(`${this.qt} filter ${where} delete returning`, params, args.timeout, 'delete');
2148
2169
  const row = rows.length ? this.stripWritePii(this.shape(rows, native)[0]) : null;
2149
2170
  if (!row)
@@ -2231,7 +2252,7 @@ export class PowqlInterface {
2231
2252
  }
2232
2253
  async aggregate(args) {
2233
2254
  return this.withMiddleware('aggregate', args, async () => {
2234
- // One scalar query per aggregate — PowDB's bare-projection aggregate is broken.
2255
+ // One scalar query per aggregate, PowDB's bare-projection aggregate is broken.
2235
2256
  const result = {};
2236
2257
  const filterParams = [];
2237
2258
  const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
@@ -2600,12 +2621,12 @@ export class PowqlInterface {
2600
2621
  // -------------------------------------------------------------------------
2601
2622
  // Streaming / unsupported
2602
2623
  // -------------------------------------------------------------------------
2603
- // biome-ignore lint/correctness/useYield: intentionally throws before yielding — PowDB has no server cursor.
2624
+ // biome-ignore lint/correctness/useYield: intentionally throws before yielding, PowDB has no server cursor.
2604
2625
  async *findManyStream() {
2605
2626
  throw new UnsupportedFeatureError('cursor streaming (findManyStream)', 'PowDB', 'PowDB has no server-side cursor; page with findMany({ limit, offset }) instead');
2606
2627
  }
2607
2628
  // -------------------------------------------------------------------------
2608
- // Reselect helper (upsert only — PowDB's upsert has no `returning`)
2629
+ // Reselect helper (upsert only, PowDB's upsert has no `returning`)
2609
2630
  // -------------------------------------------------------------------------
2610
2631
  /** Reselect a single row by its single-column primary key value. */
2611
2632
  async reselectByPk(pkValue, timeout) {
@@ -2618,12 +2639,12 @@ export class PowqlInterface {
2618
2639
  return rows.length ? this.shape(rows, native)[0] : null;
2619
2640
  }
2620
2641
  /**
2621
- * Empty-where guard — blocks accidental whole-table writes. Mirrors the SQL
2642
+ * Empty-where guard, blocks accidental whole-table writes. Mirrors the SQL
2622
2643
  * path's `assertMutationHasPredicate` (query/builder.ts): it gates on the
2623
2644
  * *compiled* PowQL filter fragment, NOT the shape of the `where` object. A
2624
- * `where` whose conditions all evaporate during compilation — `{}`,
2645
+ * `where` whose conditions all evaporate during compilation, `{}`,
2625
2646
  * `{ id: undefined }`, `{ OR: [] }`, `{ AND: [] }`, `{ NOT: {} }`,
2626
- * `{ OR: [{ f: undefined }] }` — compiles to the empty string and is refused,
2647
+ * `{ OR: [{ f: undefined }] }`, compiles to the empty string and is refused,
2627
2648
  * because emitting a filter-less write would hit every row.
2628
2649
  */
2629
2650
  assertCompiledWhere(compiledWhere, allow, action) {
@@ -73,7 +73,7 @@ export declare function buildDistinctOnSource<T extends object>(qi: BuilderCtx,
73
73
  * throws {@link ValidationError} for unknown fields and `qi.q()` quotes via
74
74
  * the dialect, so no unvalidated identifier ever reaches the SQL string. Every
75
75
  * comparison value is pushed onto the shared `params` array and referenced by
76
- * a `$N` placeholder via {@link buildHavingNumericClauses} — there is no string
76
+ * a `$N` placeholder via {@link buildHavingNumericClauses}, there is no string
77
77
  * interpolation of user values.
78
78
  *
79
79
  * `jsonAggExprs` (from {@link buildGroupBy}) maps `alias:aggKey` to the
@@ -64,7 +64,7 @@ export function buildGroupBy(qi, args) {
64
64
  ? buildDistinctOnSource(qi, args.distinctOn, whereSql, params)
65
65
  : `${qi.q(qi.table)}${whereSql}`;
66
66
  // Group keys: plain columns and/or JSON-path keys. Output-name collisions
67
- // are rejected up front — and the check runs over the EMITTED SQL output
67
+ // are rejected up front, and the check runs over the EMITTED SQL output
68
68
  // column names (snake_case column / JSON alias / `_agg_key` aggregate
69
69
  // alias), not just the given arg keys: the driver keeps only the LAST
70
70
  // duplicate field per row object, so a JSON alias equal to another key's
@@ -211,7 +211,7 @@ export function buildGroupBy(qi, args) {
211
211
  buildAggregates('_min', 'MIN', args._min);
212
212
  buildAggregates('_max', 'MAX', args._max);
213
213
  let sql = `SELECT ${selectExprs.join(', ')} FROM ${fromSql} GROUP BY ${groupExprs.join(', ')}`;
214
- // HAVING — filter whole groups by their aggregate values.
214
+ // HAVING, filter whole groups by their aggregate values.
215
215
  // Appends to the same `params` array, so placeholders continue from the
216
216
  // WHERE clause's parameter positions (qi.p(params.length) below).
217
217
  if (args.having) {
@@ -467,7 +467,7 @@ export function buildDistinctOnSource(qi, distinctOn, whereSql, params) {
467
467
  * throws {@link ValidationError} for unknown fields and `qi.q()` quotes via
468
468
  * the dialect, so no unvalidated identifier ever reaches the SQL string. Every
469
469
  * comparison value is pushed onto the shared `params` array and referenced by
470
- * a `$N` placeholder via {@link buildHavingNumericClauses} — there is no string
470
+ * a `$N` placeholder via {@link buildHavingNumericClauses}, there is no string
471
471
  * interpolation of user values.
472
472
  *
473
473
  * `jsonAggExprs` (from {@link buildGroupBy}) maps `alias:aggKey` to the
@@ -479,7 +479,7 @@ export function buildDistinctOnSource(qi, distinctOn, whereSql, params) {
479
479
  export function buildHavingClauses(qi, having, params, jsonAggExprs) {
480
480
  const clauses = [];
481
481
  // Maps the per-field aggregate key to its SQL function name. The set of
482
- // allowed keys is fixed here — any other key on a field's filter object is
482
+ // allowed keys is fixed here, any other key on a field's filter object is
483
483
  // rejected by ValidationError below (never interpolated).
484
484
  const aggFnByKey = {
485
485
  _sum: 'SUM',
@@ -502,7 +502,7 @@ export function buildHavingClauses(qi, having, params, jsonAggExprs) {
502
502
  `expected an aggregate object like { _sum: { gt: 100 } }.`);
503
503
  }
504
504
  // toColumn validates the field against schema metadata (throws
505
- // ValidationError on unknown columns) and q() quotes the identifier — no
505
+ // ValidationError on unknown columns) and q() quotes the identifier, no
506
506
  // unvalidated identifier ever reaches the SQL string. Resolution is lazy:
507
507
  // a JSON-path aggregate alias is not a column, so it must not hit
508
508
  // toColumn when every aggregate under it resolves via `jsonAggExprs`.
@@ -1,19 +1,19 @@
1
1
  /**
2
- * turbine-orm — Batched relation loader (the `relationLoadStrategy: 'batched'` path)
2
+ * turbine-orm, Batched relation loader (the `relationLoadStrategy: 'batched'` path)
3
3
  *
4
4
  * ## Why this exists
5
5
  *
6
6
  * Turbine's default `with`-clause strategy resolves nested relations in ONE SQL
7
- * statement using correlated `json_agg(json_build_object(...))` subqueries — one
7
+ * statement using correlated `json_agg(json_build_object(...))` subqueries, one
8
8
  * probe per parent row (see `buildRelationSubquery` in builder.ts). That is the
9
9
  * right default: a single round-trip, and when the child FK columns are indexed
10
10
  * each probe is an index seek. But it degrades in two situations:
11
11
  *
12
- * 1. **Missing FK index** — a correlated probe per parent row becomes
12
+ * 1. **Missing FK index**, a correlated probe per parent row becomes
13
13
  * N-parents × full-table-scan. A batched-loader ORM pays that missing index
14
14
  * only ONCE (a single `WHERE fk = ANY($1)` seq-scan), which is why schemas
15
15
  * migrated from those ORMs often lack the index the json_agg path needs.
16
- * 2. **Huge unpaginated result sets** — the JSON wire format
16
+ * 2. **Huge unpaginated result sets**, the JSON wire format
17
17
  * (`json_build_object` per row, re-serialized inside `json_agg`) is heavy to
18
18
  * encode/decode compared with flat rows.
19
19
  *
@@ -29,19 +29,19 @@
29
29
  * - **Same executor / connection path.** Every follow-up query runs through the
30
30
  * caller's own executor ({@link RelationLoadContext.exec}) and child query
31
31
  * interfaces built on the caller's pool. Inside a `$transaction` that pool is
32
- * the pinned-connection `txPool`, so batched loads join the transaction — no
32
+ * the pinned-connection `txPool`, so batched loads join the transaction, no
33
33
  * separate pool checkout per query.
34
34
  * - **Identical output shape.** The stitched result is byte-for-byte the same
35
35
  * shape the join strategy produces: relation arrays for hasMany/manyToMany
36
36
  * (`[]` when empty), single-or-null for hasOne/belongsTo, with the same
37
- * camelCase keys and Date coercion — because the child rows are parsed by the
37
+ * camelCase keys and Date coercion, because the child rows are parsed by the
38
38
  * very same `parseRow`/`buildFindMany` machinery via a child QueryInterface.
39
39
  * - **Stitch keys never leak.** To stitch, the follow-up query must select the
40
40
  * FK/PK it joins on even when the caller's `select`/`omit` excluded it; the
41
41
  * loader adds those columns for the query and strips them from the returned
42
42
  * entities afterwards ({@link includeKeysForBatching}).
43
43
  *
44
- * PowDB (powql.ts) has its own batched loaders for the same reasons — this is the
44
+ * PowDB (powql.ts) has its own batched loaders for the same reasons, this is the
45
45
  * clean Postgres/SQL implementation, deliberately NOT shared with PowQL.
46
46
  *
47
47
  * @module
@@ -132,7 +132,7 @@ export declare function defaultProjectionFields(meta: TableMetadata, includePii:
132
132
  *
133
133
  * Used both for the base query (parent keys) and each follow-up query (child
134
134
  * keys) so a caller's `select: { title: true }` on a relation still stitches even
135
- * though the FK was not requested — and the FK never appears in the output.
135
+ * though the FK was not requested, and the FK never appears in the output.
136
136
  */
137
137
  export declare function includeKeysForBatching(select: Record<string, boolean> | undefined, omit: Record<string, boolean> | undefined, fields: string[],
138
138
  /**
@@ -173,13 +173,13 @@ export declare function neededParentKeyFields(parentMeta: TableMetadata, withCla
173
173
  */
174
174
  export declare function resolveCountRelations(parentMeta: TableMetadata, countSpec: WithCount): RelationDef[];
175
175
  /**
176
- * Reject pick-row relation ordering anywhere inside a `with` tree's orderBy —
176
+ * Reject pick-row relation ordering anywhere inside a `with` tree's orderBy -
177
177
  * strategy parity with the join path, which throws this exact E003 at SQL
178
178
  * build time (`pickOrderNestedError` in builder.ts). Without this guard the
179
179
  * loaders would forward `options.orderBy` as the child reader's TOP-LEVEL
180
- * findMany orderBy, where the pick shape compiles fine — so the same query
180
+ * findMany orderBy, where the pick shape compiles fine, so the same query
181
181
  * would execute on 'batched' but throw on 'join'. Walks the whole tree up
182
- * front so acceptance never depends on which levels have rows — the batched
182
+ * front so acceptance never depends on which levels have rows, the batched
183
183
  * runners in builder.ts call this BEFORE the base query (a zero-row base
184
184
  * result must still reject, exactly like the join strategy's build-time throw).
185
185
  */