turbine-orm 0.40.1 → 0.41.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 (63) hide show
  1. package/README.md +22 -4
  2. package/dist/cjs/cli/config.js +3 -0
  3. package/dist/cjs/cli/index.js +179 -0
  4. package/dist/cjs/cli/prisma-report.js +216 -0
  5. package/dist/cjs/cli/prisma-resolve.js +335 -0
  6. package/dist/cjs/cli/prisma-schema.js +484 -0
  7. package/dist/cjs/client.js +1 -0
  8. package/dist/cjs/generate.js +279 -22
  9. package/dist/cjs/index.js +3 -2
  10. package/dist/cjs/introspect.js +203 -26
  11. package/dist/cjs/mssql.js +9 -10
  12. package/dist/cjs/mysql.js +3 -9
  13. package/dist/cjs/powdb-introspect.js +5 -10
  14. package/dist/cjs/powql.js +13 -0
  15. package/dist/cjs/prisma-compat.js +1147 -0
  16. package/dist/cjs/query/aggregates.js +67 -7
  17. package/dist/cjs/query/builder.js +388 -17
  18. package/dist/cjs/query/compound-unique.js +0 -0
  19. package/dist/cjs/query/relations.js +7 -5
  20. package/dist/cjs/query/warn-registry.js +98 -0
  21. package/dist/cjs/query/writes.js +13 -5
  22. package/dist/cjs/schema.js +47 -0
  23. package/dist/cjs/sqlite.js +4 -9
  24. package/dist/cli/config.d.ts +26 -0
  25. package/dist/cli/config.js +3 -0
  26. package/dist/cli/index.d.ts +11 -0
  27. package/dist/cli/index.js +180 -1
  28. package/dist/cli/prisma-report.d.ts +19 -0
  29. package/dist/cli/prisma-report.js +211 -0
  30. package/dist/cli/prisma-resolve.d.ts +87 -0
  31. package/dist/cli/prisma-resolve.js +330 -0
  32. package/dist/cli/prisma-schema.d.ts +116 -0
  33. package/dist/cli/prisma-schema.js +479 -0
  34. package/dist/cli/ui.d.ts +1 -1
  35. package/dist/client.d.ts +18 -2
  36. package/dist/client.js +1 -0
  37. package/dist/generate.d.ts +80 -1
  38. package/dist/generate.js +277 -25
  39. package/dist/index.d.ts +2 -2
  40. package/dist/index.js +1 -1
  41. package/dist/introspect.d.ts +92 -2
  42. package/dist/introspect.js +198 -26
  43. package/dist/mssql.js +10 -11
  44. package/dist/mysql.js +4 -10
  45. package/dist/powdb-introspect.js +5 -10
  46. package/dist/powql.js +13 -0
  47. package/dist/prisma-compat.d.ts +281 -0
  48. package/dist/prisma-compat.js +1143 -0
  49. package/dist/query/aggregates.js +67 -7
  50. package/dist/query/builder.d.ts +77 -4
  51. package/dist/query/builder.js +390 -19
  52. package/dist/query/compound-unique.d.ts +49 -0
  53. package/dist/query/compound-unique.js +0 -0
  54. package/dist/query/deferred.d.ts +18 -0
  55. package/dist/query/relations.js +7 -5
  56. package/dist/query/types.d.ts +70 -9
  57. package/dist/query/warn-registry.d.ts +57 -0
  58. package/dist/query/warn-registry.js +92 -0
  59. package/dist/query/writes.js +13 -5
  60. package/dist/schema.d.ts +75 -0
  61. package/dist/schema.js +46 -0
  62. package/dist/sqlite.js +5 -10
  63. package/package.json +6 -1
@@ -92,15 +92,37 @@ export function buildGroupBy(qi, args) {
92
92
  }
93
93
  }
94
94
  // _count
95
- const countSelected = args._count === true || args._count === undefined;
96
- if (countSelected) {
97
- // default: always include count
95
+ // - `true` / omitted → scalar `_count` column (COUNT(*)), result `_count: number`.
96
+ // - record form → one column per selection: `_all` → COUNT(*) AS "_count__all"
97
+ // (double underscore, collision-proof against a real column named `all`),
98
+ // each field → COUNT(col) AS "_count_<col>", result `_count: { _all, field }`.
99
+ const countArg = args._count;
100
+ const countIsRecord = countArg !== true && countArg !== undefined && typeof countArg === 'object';
101
+ const scalarCount = countArg === true || countArg === undefined;
102
+ if (scalarCount) {
103
+ // default: always include the scalar count
98
104
  selectExprs.push(`${qi.castAgg('COUNT(*)', 'int')} AS _count`);
99
105
  }
106
+ else if (countIsRecord) {
107
+ for (const [field, enabled] of Object.entries(countArg)) {
108
+ if (!enabled)
109
+ continue;
110
+ if (field === '_all') {
111
+ selectExprs.push(`${qi.castAgg('COUNT(*)', 'int')} AS ${qi.q('_count__all')}`);
112
+ }
113
+ else {
114
+ const col = qi.toColumn(field);
115
+ selectExprs.push(`${qi.castAgg(`COUNT(${qi.q(col)})`, 'int')} AS ${qi.q(`_count_${col}`)}`);
116
+ }
117
+ }
118
+ }
100
119
  // ORDER BY aggregate expressions, keyed `${aggKey}:${field}` (plus a bare
101
120
  // `_count`). Populated alongside the SELECT list below so `orderBy` can only
102
121
  // reference an aggregate that is actually requested. `COUNT(*)` (uncast) is
103
122
  // the ordering expression (the SELECT cast is only for the returned value).
123
+ // COUNT(*) is orderable whenever it is selected: scalar `_count`, OR the
124
+ // record form containing `_all`.
125
+ const countSelected = scalarCount || (countIsRecord && countArg._all === true);
104
126
  const aggOrderExprs = new Map();
105
127
  if (countSelected)
106
128
  aggOrderExprs.set('_count', 'COUNT(*)');
@@ -192,12 +214,34 @@ export function buildGroupBy(qi, args) {
192
214
  restructured[reader.resultKey] = reader.raw ? row[reader.rowKey] : parsed[reader.resultKey];
193
215
  }
194
216
  // _count
217
+ // scalar form → the plain `_count` (or driver-lowercased `count`) column.
218
+ // record form → assemble `{ _all, field, ... }` from the `_count__all`
219
+ // and `_count_<col>` columns. `_count__all` MUST be matched before the
220
+ // generic `_count_` prefix (its slice(7) would map through snakeToCamel).
195
221
  if ('_count' in row) {
196
222
  restructured._count = row._count;
197
223
  }
198
224
  else if ('count' in row) {
199
225
  restructured._count = row.count;
200
226
  }
227
+ else {
228
+ const countObj = {};
229
+ let hasCount = false;
230
+ for (const [rawKey, rawValue] of Object.entries(row)) {
231
+ if (rawKey === '_count__all') {
232
+ countObj._all = rawValue;
233
+ hasCount = true;
234
+ }
235
+ else if (rawKey.startsWith('_count_')) {
236
+ const col = rawKey.slice(7);
237
+ const field = qi.tableMeta.reverseColumnMap[col] ?? snakeToCamel(col);
238
+ countObj[field] = rawValue;
239
+ hasCount = true;
240
+ }
241
+ }
242
+ if (hasCount)
243
+ restructured._count = countObj;
244
+ }
201
245
  // Collect aggregates into nested objects
202
246
  const sumObj = {};
203
247
  const avgObj = {};
@@ -520,6 +564,9 @@ export function buildAggregate(qi, args) {
520
564
  }
521
565
  if (args._count && typeof args._count === 'object') {
522
566
  for (const key of Object.keys(args._count)) {
567
+ // `_all` is the reserved COUNT(*) selector, not a column.
568
+ if (key === '_all')
569
+ continue;
523
570
  if (!(key in meta.columnMap)) {
524
571
  throw new ValidationError(`Unknown column "${key}" in aggregate for table "${qi.table}"`);
525
572
  }
@@ -527,13 +574,20 @@ export function buildAggregate(qi, args) {
527
574
  }
528
575
  }
529
576
  const selectExprs = [];
530
- // _count
577
+ // _count. `true` → scalar COUNT(*). Record form: reserved `_all` → COUNT(*) AS
578
+ // "_count__all" (double underscore, collision-proof against a real column named
579
+ // `all`); each field → COUNT(col) AS "_count_<col>".
531
580
  if (args._count === true) {
532
581
  selectExprs.push(`${qi.castAgg('COUNT(*)', 'int')} AS _count`);
533
582
  }
534
583
  else if (args._count && typeof args._count === 'object') {
535
584
  for (const [field, enabled] of Object.entries(args._count)) {
536
- if (enabled) {
585
+ if (!enabled)
586
+ continue;
587
+ if (field === '_all') {
588
+ selectExprs.push(`${qi.castAgg('COUNT(*)', 'int')} AS ${qi.q('_count__all')}`);
589
+ }
590
+ else {
537
591
  const col = qi.toColumn(field);
538
592
  selectExprs.push(`${qi.castAgg(`COUNT(${qi.q(col)})`, 'int')} AS ${qi.q(`_count_${col}`)}`);
539
593
  }
@@ -590,11 +644,17 @@ export function buildAggregate(qi, args) {
590
644
  aggResult._count = row._count;
591
645
  }
592
646
  else {
593
- // Check for per-column counts
647
+ // Check for per-column counts. `_count__all` MUST be matched before the
648
+ // generic `_count_` prefix (its slice(7) is `_all`, which snakeToCamel
649
+ // would mangle to `All`).
594
650
  const countObj = {};
595
651
  let hasCountFields = false;
596
652
  for (const [key, val] of Object.entries(row)) {
597
- if (key.startsWith('_count_')) {
653
+ if (key === '_count__all') {
654
+ countObj._all = val;
655
+ hasCountFields = true;
656
+ }
657
+ else if (key.startsWith('_count_')) {
598
658
  const col = key.slice(7);
599
659
  const field = qi.tableMeta.reverseColumnMap[col] ?? snakeToCamel(col);
600
660
  countObj[field] = val;
@@ -47,8 +47,16 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
47
47
  */
48
48
  private sqlCacheEnabled;
49
49
  private readonly dialect;
50
- /** Client-level default relation-loading strategy ('join' unless configured). */
50
+ /**
51
+ * Client-level default relation-loading strategy. When nothing is configured
52
+ * this is `'auto'` (the implicit default): per-relation, keep the single-
53
+ * statement join unless the introspected metadata proves a probe is unindexed,
54
+ * in which case that relation falls back to the batched loader. An explicit
55
+ * `'join'`/`'batched'` (client or query level) always wins.
56
+ */
51
57
  private readonly relationLoadStrategy;
58
+ /** Client-level default for {@link applyStableRelationOrder} (off unless configured). */
59
+ private readonly stableRelationOrder;
52
60
  /** Nested-relation JSON encoding: 'object' (default) or 'positional'. */
53
61
  private readonly jsonEncoding;
54
62
  /**
@@ -79,8 +87,6 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
79
87
  * must never receive this schema's `::"enum"` cast (see enumTypeForColumn).
80
88
  */
81
89
  private readonly crossSchemaTypeColumns;
82
- /** Tracks tables that have already triggered a deep-with warning (one-time) */
83
- private readonly deepWithWarned;
84
90
  /**
85
91
  * Per-table memo of date columns keyed by their camelCase FIELD name.
86
92
  * `meta.dateColumns` is keyed by raw snake_case column name, which matches
@@ -95,6 +101,15 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
95
101
  private readonly options?;
96
102
  /** Set by executeWithMiddleware so queryWithTimeout can include it in events. */
97
103
  private currentAction;
104
+ /**
105
+ * Tags the query events of an in-flight `relationLoadStrategy: 'auto'` query
106
+ * that engaged the batched fallback (`'auto-batched'`), so observability sees
107
+ * which queries the auto default re-planned. Same transient-instance-state
108
+ * caveat as {@link currentAction}: set for the whole auto-split operation and
109
+ * cleared afterward; a concurrent unrelated query on the same accessor during
110
+ * that window could read it (a best-effort diagnostic tag, not load-bearing).
111
+ */
112
+ private currentStrategyTag;
98
113
  /**
99
114
  * The active query's `skipGlobalFilters` opt-out, set at the top of each
100
115
  * `build*` method and read deep in the (synchronous) SQL-build + param-collect
@@ -181,10 +196,68 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
181
196
  private inParam;
182
197
  /**
183
198
  * Resolve the effective relation-loading strategy for a query: the per-query
184
- * arg wins, then the client-level default, then `'join'`. Only meaningful when
199
+ * arg wins, then the client-level default, then `'auto'`. Only meaningful when
185
200
  * a `with` clause is present; the callers gate on that.
186
201
  */
187
202
  private resolveLoadStrategy;
203
+ /**
204
+ * The effective {@link QueryInterfaceOptions.stableRelationOrder} for a query:
205
+ * the per-query arg wins, then the client-level default (off).
206
+ */
207
+ private resolveStableOrder;
208
+ /**
209
+ * Fill a PK-ascending `orderBy` into every to-many `with` relation that has no
210
+ * explicit one, recursing into nested `with`. Returns a CLONED clause (user
211
+ * args are never mutated); when nothing needs filling it returns the input
212
+ * object unchanged, so the byte-identical fast path stays free. Only called
213
+ * when {@link resolveStableOrder} is true; runs BEFORE `withFingerprint`, so
214
+ * the two orderings get distinct SQL-cache entries automatically. To-one
215
+ * relations are single rows (no array to order) and PK-less targets have
216
+ * nothing stable to order by, so both are left untouched.
217
+ */
218
+ private applyStableRelationOrder;
219
+ /**
220
+ * Whether a relation can be served by the batched loader, i.e. all its
221
+ * correlation keys are single-column (the loader throws E017 on composite
222
+ * keys). Composite-key relations therefore always stay on the join plan under
223
+ * `'auto'` (and keep the existing unindexed-probe dev warning).
224
+ */
225
+ private relationBatchEligible;
226
+ /**
227
+ * The verdict for one relation SUBTREE under `'auto'`: is any probe in the
228
+ * subtree unindexed, is EVERY relation in the subtree batched-eligible, and
229
+ * the first unindexed probe found (for the dev note). Subtree-atomic: a whole
230
+ * top-level relation falls back only when its entire subtree is eligible,
231
+ * mirroring the batched loader recursing the same tree.
232
+ */
233
+ private autoSubtreeVerdict;
234
+ /** The `_count` verdict under `'auto'`: any counted probe unindexed + all single-key. */
235
+ private autoCountVerdict;
236
+ /**
237
+ * Partition a top-level `with` clause under `'auto'`: each relation whose
238
+ * subtree has a PROVEN unindexed probe AND is fully batched-eligible routes to
239
+ * `batchedWith`; everything else (indexed, composite-key, unknown) stays in
240
+ * `joinWith` (byte-identical join). The reserved `_count` key partitions the
241
+ * same way. Also returns the engaged relations for the dev note.
242
+ */
243
+ private partitionWithForAuto;
244
+ /**
245
+ * Plan the `'auto'` split for a query's `with` clause: normalize stable order,
246
+ * partition, and return the split ONLY when at least one relation falls back
247
+ * to batched. Returns `null` (→ run the plain join path, byte-identical, same
248
+ * cache keys) when there is no DB-backed index metadata or nothing qualifies.
249
+ */
250
+ private planAuto;
251
+ /** Dev-only once-per-relation note that `'auto'` engaged the batched fallback. */
252
+ private emitAutoNotes;
253
+ /**
254
+ * Execute a findMany/findUnique `'auto'` split: run the base query with the
255
+ * residual `joinWith` (plus any parent stitch keys the batched subset needs),
256
+ * then load `batchedWith` via the batched loader and stitch. `single` returns
257
+ * the first entity (findUnique) instead of the array. Output is identical in
258
+ * shape to the pure join plan.
259
+ */
260
+ private runAutoSplit;
188
261
  /**
189
262
  * Build the {@link RelationLoadContext} the batched loader needs, closing over
190
263
  * this interface's pool/dialect/executor. Child readers are constructed on the