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
@@ -106,7 +106,7 @@ function resolveColumns(qi, select, omit, includePii) {
106
106
  if (select) {
107
107
  // An array here means a caller wrote `select: ['id', 'name']` (Drizzle/SQL
108
108
  // style) instead of the object shape. Object.entries() would iterate the
109
- // numeric indices and throw a cryptic `Unknown field "0"` — catch it early
109
+ // numeric indices and throw a cryptic `Unknown field "0"`, catch it early
110
110
  // with an actionable message.
111
111
  if (Array.isArray(select)) {
112
112
  throw new errors_js_1.ValidationError(`[turbine] "select" must be an object mapping field names to true ` +
@@ -156,7 +156,7 @@ function withFingerprint(qi, withClause, table, depth = 0) {
156
156
  const spec = withClause[relName];
157
157
  if (!spec)
158
158
  continue;
159
- // Reserved `_count` key — fingerprint by the selected relation set so
159
+ // Reserved `_count` key, fingerprint by the selected relation set so
160
160
  // `_count: true` and `_count: { posts: true }` never share a cache entry.
161
161
  if (relName === '_count') {
162
162
  const c = spec;
@@ -242,7 +242,7 @@ function collectWithParams(qi, withClause, params, table, flattenPlan) {
242
242
  }
243
243
  collectRelationSubqueryParams(qi, relDef, relSpec, params, table ?? qi.table);
244
244
  }
245
- // `_count` global-filter params — mirror buildSelectWithRelations, which
245
+ // `_count` global-filter params, mirror buildSelectWithRelations, which
246
246
  // appends the count subqueries (and any target-filter params) AFTER every
247
247
  // relation subquery, in resolveCountRelations order.
248
248
  const countSpec = withClause._count;
@@ -312,14 +312,14 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
312
312
  if (nativeOrderPath && hasOrder) {
313
313
  collectRelationOrderParams(qi, targetTable, targetMeta, relOrderEntries, params);
314
314
  }
315
- // where params — mirrors buildAliasWhere push order
315
+ // where params, mirrors buildAliasWhere push order
316
316
  if (spec.where) {
317
317
  whereMod.collectAliasWhereParams(qi, targetTable, targetMeta, spec.where, params);
318
318
  }
319
- // Global filter on the target — mirrors targetGlobalFilterAlias in
319
+ // Global filter on the target, mirrors targetGlobalFilterAlias in
320
320
  // buildRelationSubquery (pushed after spec.where, before limit).
321
321
  whereMod.collectTargetGlobalFilterAlias(qi, targetTable, params);
322
- // limit param — only hasMany parameterizes its limit (mirrors
322
+ // limit param, only hasMany parameterizes its limit (mirrors
323
323
  // buildRelationSubquery). belongsTo/hasOne ignore limit (always LIMIT 1), so
324
324
  // pushing one here would orphan a param and desync the collect path.
325
325
  // `limit: 0` pushes (LIMIT 0 is honored), so check !== undefined.
@@ -340,7 +340,7 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
340
340
  * Value-shape fingerprint for a single orderBy entry, so two queries whose
341
341
  * ORDER BY differs only in nulls placement, vector metric, or relation-count
342
342
  * vs relation-column never collide on one cached SQL string. Captures the
343
- * SQL-shaping bits (direction, nulls, metric, relation keys) — never values.
343
+ * SQL-shaping bits (direction, nulls, metric, relation keys), never values.
344
344
  */
345
345
  function orderByEntryFingerprint(qi, d, targetTable) {
346
346
  // Vector KNN ordering changes the emitted operator by metric and adds a
@@ -386,7 +386,7 @@ function orderByEntryFingerprint(qi, d, targetTable) {
386
386
  // emits one ORDER BY term per entry in Object.entries order, so entry
387
387
  // order is SQL-shaping precedence. A sorted fingerprint made
388
388
  // `{ name: 'asc', email: 'desc' }` and the swapped literal share one
389
- // cached SQL string — silently mis-ordered results on a warm cache.
389
+ // cached SQL string, silently mis-ordered results on a warm cache.
390
390
  return `rel(${Object.entries(d)
391
391
  .map(([k, v]) => `${k}=${orderByEntryFingerprint(qi, v)}`)
392
392
  .join(',')})`;
@@ -438,7 +438,7 @@ function buildOrderBy(qi, orderBy, params, lateralSink) {
438
438
  if (isRelationOrderByValue(qi, value)) {
439
439
  return buildRelationOrderBy(qi, key, value, `ord${relOrdCounter++}`, params, undefined, lateralSink);
440
440
  }
441
- // Scalar column ordering — a plain direction or an OrderBySpec (nulls).
441
+ // Scalar column ordering, a plain direction or an OrderBySpec (nulls).
442
442
  if (meta && !(key in meta.columnMap)) {
443
443
  throw new errors_js_1.ValidationError(`[turbine] Unknown field "${key}" in orderBy on table "${qi.table}". ` +
444
444
  `Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`);
@@ -463,7 +463,7 @@ function isRelationOrderByValue(_qi, value) {
463
463
  }
464
464
  /**
465
465
  * Render the ` NULLS FIRST` / ` NULLS LAST` suffix for a column ordering.
466
- * Only PostgreSQL and SQLite support the `NULLS FIRST/LAST` grammar — on any
466
+ * Only PostgreSQL and SQLite support the `NULLS FIRST/LAST` grammar, on any
467
467
  * other engine a caller asking for explicit nulls placement gets a clear
468
468
  * {@link UnsupportedFeatureError} (E017) instead of broken SQL.
469
469
  */
@@ -543,6 +543,100 @@ function buildJsonPathOrderEntry(qi, table, meta, field, spec, prefix, params) {
543
543
  : '';
544
544
  return `${lhs} ${dir}${nullsSql}`;
545
545
  }
546
+ /**
547
+ * Order by a column reached through TWO OR MORE to-one relation hops, e.g.
548
+ * `orderBy: { model: { category: { name: 'asc' } } }`.
549
+ *
550
+ * One hop already compiled to a correlated scalar subquery; each additional
551
+ * hop is added to that same subquery as an INNER JOIN, so the whole chain is
552
+ * one subquery with one `LIMIT 1` regardless of depth:
553
+ *
554
+ * (SELECT t1."name"
555
+ * FROM "models" t0
556
+ * JOIN "categories" t1 ON t1."id" = t0."category_id"
557
+ * WHERE t0."id" = "versions"."model_id"
558
+ * LIMIT 1) ASC
559
+ *
560
+ * Every hop must be to-one. A to-many hop has no single value to order by, so
561
+ * it is refused rather than silently picking an arbitrary row (`{ pick, by }`
562
+ * exists for that, deliberately, because it forces the caller to say WHICH
563
+ * row). Each hop's target global filter is applied to its join condition, so
564
+ * ordering never keys off a soft-deleted or other-tenant row.
565
+ */
566
+ function buildChainedToOneOrderBy(qi, head, nextRelName, nextValue, params) {
567
+ const joins = [];
568
+ let currentMeta = qi.schema.tables[head.relDef.to];
569
+ let currentAlias = head.alias;
570
+ let hop = 0;
571
+ let relName = nextRelName;
572
+ let value = nextValue;
573
+ const path = [head.relName];
574
+ // Walk the chain, emitting one JOIN per hop, until the value stops being a
575
+ // relation object. Bounded by the same depth cap as nested `with`.
576
+ for (;;) {
577
+ if (!currentMeta)
578
+ throw new errors_js_1.RelationError(`[turbine] Unknown relation target in orderBy chain "${path.join('.')}"`);
579
+ const relDef = (0, utils_js_1.ownLookup)(currentMeta.relations, relName);
580
+ if (!relDef) {
581
+ throw new errors_js_1.ValidationError(`[turbine] Unknown relation "${relName}" in orderBy on relation "${path.join('.')}" ` +
582
+ `(table "${currentMeta.name}"). Available: ${Object.keys(currentMeta.relations).join(', ') || '(none)'}.`);
583
+ }
584
+ if (relDef.type !== 'belongsTo' && relDef.type !== 'hasOne') {
585
+ throw new errors_js_1.ValidationError(`[turbine] orderBy cannot traverse the to-many relation "${relName}" on "${currentMeta.name}" ` +
586
+ `(path "${path.concat(relName).join('.')}"): a to-many relation has no single value to order by. ` +
587
+ `Use a pick-row ordering ({ pick, by }) at the top level, or order by "_count".`);
588
+ }
589
+ if (++hop > MAX_ORDER_BY_RELATION_HOPS) {
590
+ throw new errors_js_1.CircularRelationError(path.concat(relName));
591
+ }
592
+ const nextMeta = qi.schema.tables[relDef.to];
593
+ if (!nextMeta)
594
+ throw new errors_js_1.RelationError(`[turbine] Unknown relation target "${relDef.to}" in orderBy`);
595
+ const nextAlias = `${head.alias}c${hop}`;
596
+ const on = relDef.type === 'belongsTo'
597
+ ? qi.dialect.buildCorrelation(nextAlias, relDef.referenceKey, currentAlias, relDef.foreignKey)
598
+ : qi.dialect.buildCorrelation(nextAlias, relDef.foreignKey, currentAlias, relDef.referenceKey);
599
+ let onSql = on;
600
+ if (params) {
601
+ const gf = whereMod.targetGlobalFilterAlias(qi, relDef.to, nextAlias, params);
602
+ if (gf)
603
+ onSql += ` AND ${gf}`;
604
+ }
605
+ joins.push(`JOIN ${qi.q(relDef.to)} ${nextAlias} ON ${onSql}`);
606
+ path.push(relName);
607
+ currentMeta = nextMeta;
608
+ currentAlias = nextAlias;
609
+ // Exactly one key per level, as with the single-hop form.
610
+ const entries = Object.entries(value);
611
+ if (entries.length !== 1) {
612
+ throw new errors_js_1.ValidationError(`[turbine] orderBy on relation "${path.join('.')}" needs exactly one key per level ` +
613
+ `(got: ${entries.map(([k]) => k).join(', ') || '(empty)'}).`);
614
+ }
615
+ const [key, entryValue] = entries[0];
616
+ if ((0, utils_js_1.ownLookup)(currentMeta.relations, key) && isRelationOrderByValue(qi, entryValue)) {
617
+ relName = key;
618
+ value = entryValue;
619
+ continue;
620
+ }
621
+ // Terminal: a column on the last table in the chain.
622
+ const snakeCol = (0, utils_js_1.ownLookup)(currentMeta.columnMap, key) ?? (0, schema_js_1.camelToSnake)(key);
623
+ if (!currentMeta.allColumns.includes(snakeCol)) {
624
+ throw new errors_js_1.ValidationError(`[turbine] Unknown column "${key}" in orderBy on relation "${path.join('.')}" (table "${currentMeta.name}").`);
625
+ }
626
+ const { dir, nulls } = (0, filters_js_1.normalizeOrderBy)(entryValue);
627
+ let where = head.correlation;
628
+ if (params) {
629
+ const gf = whereMod.targetGlobalFilterAlias(qi, head.relDef.to, head.alias, params);
630
+ if (gf)
631
+ where += ` AND ${gf}`;
632
+ }
633
+ const from = `${qi.q(head.relDef.to)} ${head.alias} ${joins.join(' ')}`;
634
+ return (`(SELECT ${currentAlias}.${qi.q(snakeCol)} FROM ${from} WHERE ${where}${qi.limitOneClause()}) ` +
635
+ `${dir}${nullsSuffix(qi, nulls)}`);
636
+ }
637
+ }
638
+ /** Depth cap for a chained relation orderBy, mirroring the nested-`with` cap. */
639
+ const MAX_ORDER_BY_RELATION_HOPS = 10;
546
640
  /**
547
641
  * Compile a relation ordering term. For a to-many relation the only allowed
548
642
  * key is `_count`, which becomes a correlated `COUNT(*)` subquery. For a
@@ -610,14 +704,26 @@ function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lateralSin
610
704
  }
611
705
  return entries
612
706
  .map(([col, dirValue]) => {
707
+ // A nested relation: `orderBy: { model: { category: { name: 'asc' } } }`.
708
+ // Each further to-one hop becomes a JOIN inside the SAME correlated
709
+ // subquery rather than another level of nesting, so an N-hop chain still
710
+ // costs one subquery.
711
+ const nestedRel = (0, utils_js_1.ownLookup)(targetMeta.relations, col);
712
+ if (nestedRel && isRelationOrderByValue(qi, dirValue)) {
713
+ return buildChainedToOneOrderBy(qi, { relName, relDef, alias, correlation }, col, dirValue, params);
714
+ }
613
715
  // columnMap-first resolution (camelToSnake fallback): mirrors the
614
716
  // scalar orderBy path so camelCase-named DB columns resolve here too.
615
717
  const snakeCol = (0, utils_js_1.ownLookup)(targetMeta.columnMap, col) ?? (0, schema_js_1.camelToSnake)(col);
616
718
  if (!targetMeta.allColumns.includes(snakeCol)) {
617
- throw new errors_js_1.ValidationError(`[turbine] Unknown column "${col}" in orderBy on relation "${relName}" (table "${relDef.to}").`);
719
+ const relationHint = (0, utils_js_1.ownLookup)(targetMeta.relations, col)
720
+ ? ` "${col}" is a relation on "${relDef.to}": order by one of ITS columns, e.g. ` +
721
+ `{ ${relName}: { ${col}: { <column>: 'asc' } } }.`
722
+ : '';
723
+ throw new errors_js_1.ValidationError(`[turbine] Unknown column "${col}" in orderBy on relation "${relName}" (table "${relDef.to}").${relationHint}`);
618
724
  }
619
725
  const { dir, nulls } = (0, filters_js_1.normalizeOrderBy)(dirValue);
620
- // Target's global filter applies here too — otherwise ordering keys off
726
+ // Target's global filter applies here too, otherwise ordering keys off
621
727
  // a soft-deleted / other-tenant related row's value (matches the with
622
728
  // subquery semantics for belongsTo/hasOne).
623
729
  let where = correlation;
@@ -974,7 +1080,7 @@ function collectManyToManyTargetGlobalFilter(qi, relDef, params) {
974
1080
  /**
975
1081
  * Param-collect mirror of {@link buildRelationCountExpr}'s global-filter
976
1082
  * params (hasMany direct filter, or manyToMany EXISTS-on-target). Only pushes
977
- * when a filter applies — no-op otherwise.
1083
+ * when a filter applies, no-op otherwise.
978
1084
  */
979
1085
  function collectRelationCountParams(qi, relDef, params) {
980
1086
  if (relDef.type === 'manyToMany') {
@@ -1128,7 +1234,7 @@ function buildJsonRow(qi, jsonPairs) {
1128
1234
  // `json_build_object` renders a handful of Postgres types as something other
1129
1235
  // than the value the driver hands back for the same column, so the 'join'
1130
1236
  // strategy used to disagree with a top-level read, with 'batched', and with
1131
- // 'flatten' — losing precision outright on numeric and int8. The two halves of
1237
+ // 'flatten', losing precision outright on numeric and int8. The two halves of
1132
1238
  // the fix live here and must stay in lockstep:
1133
1239
  //
1134
1240
  // SQL side jsonScalarPairs() emits `alias."col"::text` for a divergent
@@ -1141,9 +1247,17 @@ function buildJsonRow(qi, jsonPairs) {
1141
1247
  // decode (or vice versa). Postgres-only: other engines neither use
1142
1248
  // json_build_object nor share this divergence set.
1143
1249
  // ---------------------------------------------------------------------------
1144
- /** True when this query's engine gets the JSON-wire cast/decode treatment. */
1145
- function usesJsonWireCoercion(qi) {
1146
- return qi.dialect.name === 'postgresql';
1250
+ /**
1251
+ * The JSON-wire rule for one column, or undefined when its JSON rendering
1252
+ * already matches the driver. Routed entirely through the dialect, so each
1253
+ * engine states its own divergence set (see {@link Dialect.jsonWireRule}); a
1254
+ * dialect that omits the hook keeps the pre-0.51 no-cast behavior.
1255
+ */
1256
+ function jsonWireRuleFor(qi, meta, col) {
1257
+ const type = meta.pgTypes?.[col];
1258
+ if (!type)
1259
+ return undefined;
1260
+ return qi.dialect.jsonWireRule?.(type);
1147
1261
  }
1148
1262
  /**
1149
1263
  * The JSON value expression for one scalar relation column: the plain column
@@ -1152,9 +1266,7 @@ function usesJsonWireCoercion(qi) {
1152
1266
  */
1153
1267
  function jsonScalarExpr(qi, targetMeta, col, ref) {
1154
1268
  const expr = `${ref}.${qi.q(col)}`;
1155
- if (!usesJsonWireCoercion(qi))
1156
- return expr;
1157
- return (0, utils_js_1.jsonWireCoercionOid)(targetMeta.pgTypes?.[col]) === undefined ? expr : `${expr}::text`;
1269
+ return jsonWireRuleFor(qi, targetMeta, col)?.sql(expr) ?? expr;
1158
1270
  }
1159
1271
  /**
1160
1272
  * The `[camelKey, valueExpr]` pairs for a relation's scalar columns. Single
@@ -1188,16 +1300,13 @@ function jsonWireFields(qi, table, meta) {
1188
1300
  if (cached !== undefined)
1189
1301
  return cached;
1190
1302
  let fields = null;
1191
- const pgTypes = meta.pgTypes;
1192
- if (pgTypes) {
1193
- for (const col of meta.allColumns) {
1194
- const oid = (0, utils_js_1.jsonWireCoercionOid)(pgTypes[col]);
1195
- if (oid === undefined)
1196
- continue;
1197
- if (fields === null)
1198
- fields = new Map();
1199
- fields.set(meta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col), oid);
1200
- }
1303
+ for (const col of meta.allColumns) {
1304
+ const rule = jsonWireRuleFor(qi, meta, col);
1305
+ if (rule === undefined)
1306
+ continue;
1307
+ if (fields === null)
1308
+ fields = new Map();
1309
+ fields.set(meta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col), rule);
1201
1310
  }
1202
1311
  byTable.set(table, fields);
1203
1312
  return fields;
@@ -1208,8 +1317,6 @@ function jsonWireFields(qi, table, meta) {
1208
1317
  * so the ordinary relation pays nothing beyond the memo lookup.
1209
1318
  */
1210
1319
  function decodeJsonWireRow(qi, row, table, meta) {
1211
- if (!usesJsonWireCoercion(qi))
1212
- return row;
1213
1320
  const fields = jsonWireFields(qi, table, meta);
1214
1321
  if (fields === null)
1215
1322
  return row;
@@ -1218,13 +1325,13 @@ function decodeJsonWireRow(qi, row, table, meta) {
1218
1325
  // be holding. Measured against an in-place variant on a 20K-child-row join the
1219
1326
  // two were indistinguishable, so the copy is free insurance.
1220
1327
  let decoded;
1221
- for (const [field, oid] of fields) {
1328
+ for (const [field, rule] of fields) {
1222
1329
  const raw = row[field];
1223
1330
  if (typeof raw !== 'string')
1224
1331
  continue;
1225
1332
  if (decoded === undefined)
1226
1333
  decoded = { ...row };
1227
- decoded[field] = (0, utils_js_1.coerceJsonWireValue)(oid, raw);
1334
+ decoded[field] = rule.decode(raw);
1228
1335
  }
1229
1336
  return decoded ?? row;
1230
1337
  }
@@ -1293,7 +1400,7 @@ function makeNestedParser(qi, withClause, includePii, flattenPlan) {
1293
1400
  /**
1294
1401
  * Return a shallow copy of a top-level row with each relation column decoded
1295
1402
  * from its positional array(s) into the object representation. Only relation
1296
- * columns are positional — base scalar columns stay object-keyed — so the
1403
+ * columns are positional, base scalar columns stay object-keyed, so the
1297
1404
  * result is exactly what the object encoding would have handed parseNestedRow.
1298
1405
  */
1299
1406
  function decodePositionalRelations(qi, row, shapes) {
@@ -1343,7 +1450,7 @@ function decodePositionalObject(qi, arr, shape) {
1343
1450
  return obj;
1344
1451
  }
1345
1452
  // ---------------------------------------------------------------------------
1346
- // relationLoadStrategy: 'flatten' — to-one relations compiled as LEFT JOINs
1453
+ // relationLoadStrategy: 'flatten', to-one relations compiled as LEFT JOINs
1347
1454
  // ---------------------------------------------------------------------------
1348
1455
  /**
1349
1456
  * Alias prefix for a flattened relation's join. Deliberately distinct from the
@@ -1358,7 +1465,7 @@ const FLATTEN_ALIAS_PREFIX = 'f';
1358
1465
  * The top-level `WHERE` and `ORDER BY` reference the parent's columns
1359
1466
  * UNQUALIFIED (`WHERE "name" = $1`). A bare `LEFT JOIN "orgs" f0` puts a second
1360
1467
  * table in scope, so any column name the two tables share ("id", "name",
1361
- * "created_at" — i.e. most of them) turns those references ambiguous and
1468
+ * "created_at", i.e. most of them) turns those references ambiguous and
1362
1469
  * Postgres rejects the statement. Qualifying the parent's references is not an
1363
1470
  * option here: they are compiled by the shared WHERE walk, which is scope-blind
1364
1471
  * by design.
@@ -1461,13 +1568,13 @@ function provableUniqueTargetKey(relDef, targetMeta) {
1461
1568
  *
1462
1569
  * A relation is ELIGIBLE when all of the following hold:
1463
1570
  * 1. it is `belongsTo` or `hasOne` AND its target-side correlation columns are
1464
- * provably unique ({@link provableUniqueTargetKey}) — the row-multiplication
1571
+ * provably unique ({@link provableUniqueTargetKey}), the row-multiplication
1465
1572
  * guard;
1466
1573
  * 2. its spec declares no `limit` and no `orderBy` (both are no-ops over a
1467
1574
  * single matching row, but refusing them keeps the emitted SQL and the
1468
1575
  * param stream trivially equivalent);
1469
1576
  * 3. its nested `with` names only real relations and no reserved `_count`
1470
- * (nested `_count` is unsupported on every strategy — falling back lets the
1577
+ * (nested `_count` is unsupported on every strategy, falling back lets the
1471
1578
  * subquery path raise the same error);
1472
1579
  * 4. the depth cap is not reached, so a too-deep chain still raises
1473
1580
  * {@link CircularRelationError} from the subquery path instead of silently
@@ -1602,7 +1709,7 @@ function warnFlattenFallback(table, rejects) {
1602
1709
  * it emits today, down to the cache key).
1603
1710
  *
1604
1711
  * The plan is a pure function of the schema, the `with` clause shape and
1605
- * `includePii` — never of any bound value — so the build path, the cache-hit
1712
+ * `includePii`, never of any bound value, so the build path, the cache-hit
1606
1713
  * param-collect path and the row assembler can each recompute it and agree.
1607
1714
  */
1608
1715
  function planFlattenWith(qi, table, withClause, includePii) {
@@ -1905,8 +2012,8 @@ function buildSelectWithRelations(qi, table, withClause, params, columnsList, de
1905
2012
  const meta = qi.schema.tables[table];
1906
2013
  if (!meta)
1907
2014
  throw new errors_js_1.ValidationError(`[turbine] Unknown table "${table}"`);
1908
- // Positional JSON encoding is Postgres-only in v1. Gate here — the single
1909
- // entry point for every `with` clause — so no engine ever emits the
2015
+ // Positional JSON encoding is Postgres-only in v1. Gate here, the single
2016
+ // entry point for every `with` clause, so no engine ever emits the
1910
2017
  // json_build_array shape its dialect can't produce (and mssql's FOR JSON
1911
2018
  // override path is never reached with positional active).
1912
2019
  if (qi.jsonEncoding === 'positional' && qi.dialect.name !== 'postgresql') {
@@ -2048,7 +2155,7 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
2048
2155
  const currentDepth = depth ?? 0;
2049
2156
  const currentPath = path ?? [qi.table];
2050
2157
  const targetTable = relDef.to;
2051
- // Hard depth cap — the `with` clause is a finite JSON structure so users can't
2158
+ // Hard depth cap, the `with` clause is a finite JSON structure so users can't
2052
2159
  // create true infinite recursion, but extremely deep nesting (10+ levels) produces
2053
2160
  // unmanageably large SQL. Back-references (e.g. posts → user → posts) are allowed
2054
2161
  // since they are legitimate queries (Prisma supports the same pattern).
@@ -2073,7 +2180,7 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
2073
2180
  const miss = (0, index_advisor_js_1.missingIndexForRelation)(qi.schema, relDef);
2074
2181
  if (miss && (0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.unindexedRelation, warnKey)) {
2075
2182
  console.warn(`[turbine] Relation "${relDef.name}" on "${relDef.from}" probes ` +
2076
- `"${miss.table}"(${miss.columns.join(', ')}) which has no covering index — ` +
2183
+ `"${miss.table}"(${miss.columns.join(', ')}) which has no covering index, ` +
2077
2184
  `each parent row scans the full table. Fix: ${miss.createSql}; ` +
2078
2185
  'or run `npx turbine doctor` for a full report.');
2079
2186
  }
@@ -2117,7 +2224,7 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
2117
2224
  // Determine if this hasMany will take the wrapped subquery path (LIMIT or ORDER BY).
2118
2225
  // When wrapping, nested relations are built in the wrapped path referencing innerAlias,
2119
2226
  // so we must NOT build them here (they would push orphaned params).
2120
- // An orderBy with no defined entries (`orderBy: {}`) is treated as absent —
2227
+ // An orderBy with no defined entries (`orderBy: {}`) is treated as absent -
2121
2228
  // it must neither trigger the wrap (dropping nested relations) nor render a
2122
2229
  // dangling `ORDER BY `. `limit: 0` is meaningful (LIMIT 0) and DOES wrap.
2123
2230
  const relOrderEntries = spec !== true && spec.orderBy ? (0, filters_js_1.orderByEntries)(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
@@ -2128,7 +2235,7 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
2128
2235
  if (relDef.type === 'manyToMany') {
2129
2236
  return buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCounter, currentDepth, currentPath, alias, targetMeta, targetColumns, includePii);
2130
2237
  }
2131
- // Nested relations — only in the non-wrapped path (wrapped path builds them separately)
2238
+ // Nested relations, only in the non-wrapped path (wrapped path builds them separately)
2132
2239
  if (!willWrap && spec !== true && spec.with) {
2133
2240
  for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(spec.with)) {
2134
2241
  const nestedRelDef = (0, utils_js_1.ownLookup)(targetMeta.relations, nestedRelName);
@@ -2144,7 +2251,7 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
2144
2251
  }
2145
2252
  }
2146
2253
  const jsonObj = buildJsonRow(qi, jsonPairs);
2147
- // Quote parent ref — can be a table name or auto-generated alias
2254
+ // Quote parent ref, can be a table name or auto-generated alias
2148
2255
  const qParent = qi.q(parentRef);
2149
2256
  const qTarget = qi.q(targetTable);
2150
2257
  // Build ORDER BY for json_agg: unified with the top-level orderBy surface
@@ -2155,10 +2262,10 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
2155
2262
  if (relOrderEntries.length > 0) {
2156
2263
  orderClause = buildRelationOrderClause(qi, targetTable, targetMeta, alias, relOrderEntries, params);
2157
2264
  }
2158
- // Build WHERE — correlate to parent via parentRef (alias or table name).
2265
+ // Build WHERE, correlate to parent via parentRef (alias or table name).
2159
2266
  // For hasMany/hasOne: TARGET has the FK (RelationDef.foreignKey is always
2160
2267
  // the child-side column), so alias.fk = parentRef.pk. hasOne is just
2161
- // hasMany with a unique FK — treating it like belongsTo here silently
2268
+ // hasMany with a unique FK, treating it like belongsTo here silently
2162
2269
  // correlated the wrong columns (caught dogfooding: uuid = varchar).
2163
2270
  // For belongsTo: SOURCE has the FK, so alias.pk = parentRef.fk (reversed).
2164
2271
  // Supports composite foreign keys (string[]) via buildCorrelation.
@@ -2169,20 +2276,20 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
2169
2276
  else {
2170
2277
  whereClause = qi.dialect.buildCorrelation(alias, relDef.foreignKey, qParent, relDef.referenceKey);
2171
2278
  }
2172
- // Additional filters — full scalar where surface (equality, null, operator
2279
+ // Additional filters, full scalar where surface (equality, null, operator
2173
2280
  // objects, OR/AND/NOT), properly parameterized against this alias.
2174
2281
  if (spec !== true && spec.where) {
2175
2282
  const extra = whereMod.buildAliasWhere(qi, targetTable, targetMeta, alias, spec.where, params);
2176
2283
  if (extra)
2177
2284
  whereClause += ` AND ${extra}`;
2178
2285
  }
2179
- // Global filter on the target table (soft-delete / tenancy) — AND-merged so
2286
+ // Global filter on the target table (soft-delete / tenancy), AND-merged so
2180
2287
  // a `with` never surfaces filtered-out child rows. Pushed AFTER spec.where,
2181
2288
  // mirrored by collectRelationSubqueryParams.
2182
2289
  const gfExtra = whereMod.targetGlobalFilterAlias(qi, targetTable, alias, params);
2183
2290
  if (gfExtra)
2184
2291
  whereClause += ` AND ${gfExtra}`;
2185
- // LIMIT — only meaningful for hasMany. A belongsTo / hasOne subquery returns
2292
+ // LIMIT, only meaningful for hasMany. A belongsTo / hasOne subquery returns
2186
2293
  // a single row (literal `LIMIT 1` below), so a `spec.limit` here must NOT push
2187
2294
  // a parameter: doing so orphans an untyped `$N` that the SQL never references,
2188
2295
  // which Postgres rejects with "could not determine data type of parameter $N"
@@ -2200,7 +2307,7 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
2200
2307
  // Rewrite: SELECT json_agg(json_build_object(...)) FROM (SELECT * FROM table WHERE ... ORDER BY ... LIMIT N) AS alias
2201
2308
  // Inner SELECT always needs all columns for WHERE/ORDER to work; json_build_object filters later
2202
2309
  const innerSql = `SELECT ${targetMeta.allColumns.map((c) => `${alias}.${qi.q(c)}`).join(', ')} FROM ${qTarget} ${alias} WHERE ${whereClause}${orderClause}${limitClause}`;
2203
- // For the json_build_object, reference the inner alias — only include resolved columns
2310
+ // For the json_build_object, reference the inner alias, only include resolved columns
2204
2311
  const innerJsonPairs = jsonScalarPairs(qi, targetMeta, targetColumns, innerAlias);
2205
2312
  // Build nested relation subqueries referencing innerAlias
2206
2313
  if (spec !== true && spec.with) {
@@ -2220,7 +2327,7 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
2220
2327
  }
2221
2328
  // Inline ORDER BY only when the dialect's array-agg supports it (PG). For
2222
2329
  // hasMany this path is reached only when there is no orderClause, so the
2223
- // argument is `undefined` either way — keeping PG output byte-identical.
2330
+ // argument is `undefined` either way, keeping PG output byte-identical.
2224
2331
  const inlineOrder = qi.dialect.aggSupportsInlineOrderBy ? orderClause.trim() || undefined : undefined;
2225
2332
  return `SELECT ${qi.dialect.buildJsonArrayAgg(jsonObj, inlineOrder)} FROM ${qTarget} ${alias} WHERE ${whereClause}`;
2226
2333
  }
@@ -2261,7 +2368,7 @@ function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCount
2261
2368
  // JOIN: junction.targetKey = target.<targetPK>. Composite keys pair positionally.
2262
2369
  const targetKeys = (0, schema_js_1.normalizeKeyColumns)(relDef.through.targetKey);
2263
2370
  // The target PK is the column(s) the junction's targetKey references. An empty
2264
- // introspected PK means we cannot know what to JOIN on — fail loudly rather than
2371
+ // introspected PK means we cannot know what to JOIN on, fail loudly rather than
2265
2372
  // silently guessing `id` and generating a wrong JOIN.
2266
2373
  if (targetMeta.primaryKey.length === 0) {
2267
2374
  throw new errors_js_1.ValidationError(`[turbine] manyToMany relation "${relDef.name}" targets table "${targetTable}" which has no primary key; ` +
@@ -2293,7 +2400,7 @@ function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCount
2293
2400
  if (relOrderEntries.length > 0) {
2294
2401
  orderClause = buildRelationOrderClause(qi, targetTable, targetMeta, talias, relOrderEntries, params);
2295
2402
  }
2296
- // Additional WHERE filters on the target — full scalar where surface,
2403
+ // Additional WHERE filters on the target, full scalar where surface,
2297
2404
  // properly parameterized against the target alias.
2298
2405
  if (spec !== true && spec.where) {
2299
2406
  const extra = whereMod.buildAliasWhere(qi, targetTable, targetMeta, talias, spec.where, params);
@@ -2305,7 +2412,7 @@ function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCount
2305
2412
  const gfExtra = whereMod.targetGlobalFilterAlias(qi, targetTable, talias, params);
2306
2413
  if (gfExtra)
2307
2414
  whereClause += ` AND ${gfExtra}`;
2308
- // LIMIT — `limit: 0` is honored (LIMIT 0 → empty array)
2415
+ // LIMIT, `limit: 0` is honored (LIMIT 0 → empty array)
2309
2416
  let limitClause = '';
2310
2417
  if (spec !== true && spec.limit !== undefined) {
2311
2418
  limitClause = ` LIMIT ${qi.paginationRef(spec.limit, params, 'relation limit')}`;