turbine-orm 0.49.0 → 0.50.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 (157) hide show
  1. package/README.md +122 -39
  2. package/dist/cjs/adapters/cockroachdb.d.ts +39 -0
  3. package/dist/cjs/adapters/index.d.ts +110 -0
  4. package/dist/cjs/adapters/yugabytedb.d.ts +51 -0
  5. package/dist/cjs/cli/config.d.ts +181 -0
  6. package/dist/cjs/cli/config.js +32 -6
  7. package/dist/cjs/cli/destructive.d.ts +38 -0
  8. package/dist/cjs/cli/index.d.ts +359 -0
  9. package/dist/cjs/cli/index.js +228 -56
  10. package/dist/cjs/cli/loader.d.ts +61 -0
  11. package/dist/cjs/cli/mcp.d.ts +42 -0
  12. package/dist/cjs/cli/migrate.d.ts +356 -0
  13. package/dist/cjs/cli/migrate.js +131 -40
  14. package/dist/cjs/cli/observe-ui.d.ts +1 -0
  15. package/dist/cjs/cli/observe-ui.js +14 -5
  16. package/dist/cjs/cli/observe.d.ts +25 -0
  17. package/dist/cjs/cli/observe.js +49 -12
  18. package/dist/cjs/cli/pii-tags.d.ts +53 -0
  19. package/dist/cjs/cli/prisma-report.d.ts +33 -0
  20. package/dist/cjs/cli/prisma-report.js +73 -0
  21. package/dist/cjs/cli/prisma-resolve.d.ts +106 -0
  22. package/dist/cjs/cli/prisma-resolve.js +1 -0
  23. package/dist/cjs/cli/prisma-schema.d.ts +176 -0
  24. package/dist/cjs/cli/prisma-schema.js +82 -4
  25. package/dist/cjs/cli/rate-limit.d.ts +32 -0
  26. package/dist/cjs/cli/rate-limit.js +45 -0
  27. package/dist/cjs/cli/studio-demo.d.ts +43 -0
  28. package/dist/cjs/cli/studio-ui.generated.d.ts +1 -0
  29. package/dist/cjs/cli/studio.d.ts +207 -0
  30. package/dist/cjs/cli/studio.js +136 -71
  31. package/dist/cjs/cli/ui.d.ts +73 -0
  32. package/dist/cjs/cli/ui.js +51 -9
  33. package/dist/cjs/client.d.ts +837 -0
  34. package/dist/cjs/client.js +3 -0
  35. package/dist/cjs/dialect.d.ts +516 -0
  36. package/dist/cjs/dialect.js +37 -12
  37. package/dist/cjs/errors.d.ts +370 -0
  38. package/dist/cjs/generate.d.ts +137 -0
  39. package/dist/cjs/generate.js +39 -6
  40. package/dist/cjs/index-advisor.d.ts +153 -0
  41. package/dist/cjs/index-stats.d.ts +384 -0
  42. package/dist/cjs/index.d.ts +55 -0
  43. package/dist/cjs/index.js +7 -2
  44. package/dist/cjs/introspect.d.ts +269 -0
  45. package/dist/cjs/mssql.d.ts +232 -0
  46. package/dist/cjs/mssql.js +6 -0
  47. package/dist/cjs/mysql.d.ts +173 -0
  48. package/dist/cjs/mysql.js +16 -0
  49. package/dist/cjs/nested-write.d.ts +96 -0
  50. package/dist/cjs/nested-write.js +414 -24
  51. package/dist/cjs/observe.d.ts +115 -0
  52. package/dist/cjs/optional-peer-import.d.cts +72 -0
  53. package/dist/cjs/pipeline-submittable.d.ts +93 -0
  54. package/dist/cjs/pipeline.d.ts +71 -0
  55. package/dist/cjs/powdb-introspect.d.ts +84 -0
  56. package/dist/cjs/powdb.d.ts +931 -0
  57. package/dist/cjs/powdb.js +106 -21
  58. package/dist/cjs/powql.d.ts +592 -0
  59. package/dist/cjs/powql.js +42 -6
  60. package/dist/cjs/prisma-compat.d.ts +283 -0
  61. package/dist/cjs/prisma-compat.js +167 -9
  62. package/dist/cjs/query/aggregates.d.ts +92 -0
  63. package/dist/cjs/query/aggregates.js +7 -3
  64. package/dist/cjs/query/batched-loader.d.ts +193 -0
  65. package/dist/cjs/query/builder.d.ts +849 -0
  66. package/dist/cjs/query/builder.js +571 -65
  67. package/dist/cjs/query/compound-unique.d.ts +51 -0
  68. package/dist/cjs/query/deferred.d.ts +223 -0
  69. package/dist/cjs/query/filters.d.ts +201 -0
  70. package/dist/cjs/query/index.d.ts +14 -0
  71. package/dist/cjs/query/index.js +6 -1
  72. package/dist/cjs/query/relations.d.ts +609 -0
  73. package/dist/cjs/query/relations.js +693 -46
  74. package/dist/cjs/query/types.d.ts +1300 -0
  75. package/dist/cjs/query/utils.d.ts +209 -0
  76. package/dist/cjs/query/utils.js +208 -1
  77. package/dist/cjs/query/warn-registry.d.ts +68 -0
  78. package/dist/cjs/query/warn-registry.js +9 -0
  79. package/dist/cjs/query/where-compile.d.ts +139 -0
  80. package/dist/cjs/query/where.d.ts +548 -0
  81. package/dist/cjs/query/where.js +58 -22
  82. package/dist/cjs/query/writes.d.ts +172 -0
  83. package/dist/cjs/query/writes.js +105 -12
  84. package/dist/cjs/realtime.d.ts +70 -0
  85. package/dist/cjs/schema-builder.d.ts +354 -0
  86. package/dist/cjs/schema-metadata.d.ts +83 -0
  87. package/dist/cjs/schema-sql.d.ts +217 -0
  88. package/dist/cjs/schema-sql.js +23 -5
  89. package/dist/cjs/schema.d.ts +356 -0
  90. package/dist/cjs/schema.js +125 -0
  91. package/dist/cjs/seed.d.ts +15 -0
  92. package/dist/cjs/serverless.d.ts +142 -0
  93. package/dist/cjs/sqlite.d.ts +143 -0
  94. package/dist/cjs/sqlite.js +4 -0
  95. package/dist/cjs/typed-sql.d.ts +102 -0
  96. package/dist/cli/config.d.ts +18 -4
  97. package/dist/cli/config.js +31 -6
  98. package/dist/cli/index.d.ts +123 -0
  99. package/dist/cli/index.js +223 -58
  100. package/dist/cli/migrate.d.ts +59 -10
  101. package/dist/cli/migrate.js +128 -41
  102. package/dist/cli/observe-ui.d.ts +1 -1
  103. package/dist/cli/observe-ui.js +14 -5
  104. package/dist/cli/observe.d.ts +7 -1
  105. package/dist/cli/observe.js +48 -12
  106. package/dist/cli/prisma-report.d.ts +14 -0
  107. package/dist/cli/prisma-report.js +72 -0
  108. package/dist/cli/prisma-resolve.d.ts +6 -0
  109. package/dist/cli/prisma-resolve.js +1 -0
  110. package/dist/cli/prisma-schema.d.ts +62 -2
  111. package/dist/cli/prisma-schema.js +81 -4
  112. package/dist/cli/rate-limit.d.ts +32 -0
  113. package/dist/cli/rate-limit.js +40 -0
  114. package/dist/cli/studio.d.ts +5 -5
  115. package/dist/cli/studio.js +135 -70
  116. package/dist/cli/ui.d.ts +1 -1
  117. package/dist/cli/ui.js +51 -9
  118. package/dist/client.d.ts +40 -0
  119. package/dist/client.js +3 -0
  120. package/dist/dialect.d.ts +17 -1
  121. package/dist/dialect.js +37 -12
  122. package/dist/generate.js +40 -7
  123. package/dist/index.d.ts +1 -1
  124. package/dist/index.js +1 -1
  125. package/dist/mssql.js +6 -0
  126. package/dist/mysql.js +16 -0
  127. package/dist/nested-write.d.ts +2 -0
  128. package/dist/nested-write.js +415 -25
  129. package/dist/powdb.d.ts +4 -2
  130. package/dist/powdb.js +106 -21
  131. package/dist/powql.d.ts +5 -0
  132. package/dist/powql.js +42 -6
  133. package/dist/prisma-compat.d.ts +2 -0
  134. package/dist/prisma-compat.js +166 -8
  135. package/dist/query/aggregates.js +7 -3
  136. package/dist/query/builder.d.ts +292 -21
  137. package/dist/query/builder.js +570 -64
  138. package/dist/query/deferred.d.ts +39 -0
  139. package/dist/query/index.d.ts +1 -1
  140. package/dist/query/index.js +1 -1
  141. package/dist/query/relations.d.ts +173 -5
  142. package/dist/query/relations.js +688 -47
  143. package/dist/query/types.d.ts +123 -39
  144. package/dist/query/utils.d.ts +116 -0
  145. package/dist/query/utils.js +198 -0
  146. package/dist/query/warn-registry.d.ts +9 -0
  147. package/dist/query/warn-registry.js +9 -0
  148. package/dist/query/where.d.ts +38 -1
  149. package/dist/query/where.js +58 -23
  150. package/dist/query/writes.d.ts +42 -1
  151. package/dist/query/writes.js +104 -13
  152. package/dist/schema-sql.d.ts +14 -0
  153. package/dist/schema-sql.js +23 -5
  154. package/dist/schema.d.ts +38 -0
  155. package/dist/schema.js +123 -0
  156. package/dist/sqlite.js +4 -0
  157. package/package.json +77 -28
@@ -13,6 +13,71 @@
13
13
  import type pg from 'pg';
14
14
  import type { SchemaMetadata } from '../schema.js';
15
15
  import type { AggregateArgs, AggregateResult, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, GroupByResult, QueryResult, TypedWithClause, UpdateArgs, UpdateManyArgs, UpsertArgs, WithClause } from './types.js';
16
+ /**
17
+ * Marginal cost of keeping a to-one relation on the JOIN plan, per parent row.
18
+ *
19
+ * A to-one relation compiled into the join plan is a CORRELATED subquery: the
20
+ * engine re-evaluates it once per parent row, so the join plan costs roughly
21
+ * `AUTO_JOIN_PENALTY_MS_PER_ROW * parentRows` more CPU than one flat follow-up
22
+ * query, no matter how well indexed the correlation column is. The batched plan
23
+ * pays that back as a second statement, i.e. one extra round trip.
24
+ *
25
+ * Break-even is therefore, to a first approximation:
26
+ *
27
+ * parentRows = roundTripMs / AUTO_JOIN_PENALTY_MS_PER_ROW
28
+ *
29
+ * Measured (PostgreSQL 17, hasOne over a UNIQUE FK, 10K-row parent table,
30
+ * median of 15 reps per point) at two very different link speeds:
31
+ *
32
+ * link RTT penalty/row break-even observed crossover
33
+ * ─────────────────── ──────── ───────────── ──────────── ──────────────────
34
+ * loopback TCP 0.118ms 0.000711ms ~166-236 between 200 and 400
35
+ * +1ms/direction 2.683ms 0.000717ms ~3744-3993 between 3000 and 5000
36
+ *
37
+ * The two things that matters most in that table: the per-row penalty is
38
+ * essentially IDENTICAL across the two links (it is a property of the plan, not
39
+ * the wire), while the break-even moved by 17x. So the break-even is a function
40
+ * of the deployment's round-trip time and NOTHING ELSE that is knowable at plan
41
+ * time. That is why this is expressed as a per-row cost and a round-trip time
42
+ * rather than as a hard-coded row count: a row count tuned on a Unix socket is
43
+ * off by ~20x for a cross-region deployment, and vice versa. Concretely, the
44
+ * previously shipped flat `1000` was simultaneously too HIGH on loopback
45
+ * (up to 1.44x slower than the better plan just under the cliff) and too LOW
46
+ * over a 2.7ms link (1.26x slower just above it).
47
+ */
48
+ export declare const AUTO_JOIN_PENALTY_MS_PER_ROW = 0.0007;
49
+ /**
50
+ * Round-trip time assumed before this process has observed a real one, chosen
51
+ * as a typical same-region managed-Postgres latency (app and database in one
52
+ * region over TCP). It is stated as a LATENCY rather than a row count so the
53
+ * assumption is visible and re-derivable: at
54
+ * {@link AUTO_JOIN_PENALTY_MS_PER_ROW} it yields exactly the 1000-row default
55
+ * this heuristic has always shipped, so an unmeasured process behaves exactly
56
+ * as before.
57
+ */
58
+ export declare const AUTO_ASSUMED_ROUND_TRIP_MS = 0.7;
59
+ /**
60
+ * Default parent-row ceiling under which `'auto'` keeps a to-one relation on
61
+ * the single-statement join plan: {@link AUTO_ASSUMED_ROUND_TRIP_MS} divided by
62
+ * {@link AUTO_JOIN_PENALTY_MS_PER_ROW}. Used until the process has measured its
63
+ * own round-trip time, and whenever measurement is unavailable.
64
+ */
65
+ export declare const AUTO_TO_ONE_JOIN_MAX_ROWS: number;
66
+ /**
67
+ * Clamps on the MEASURED threshold (an explicit `autoToOneJoinMaxRows` is an
68
+ * instruction, not an estimate, and bypasses both).
69
+ *
70
+ * The lower clamp matters: on a very fast link the formula can drop the
71
+ * threshold to a few dozen rows, and the sweep shows the join plan winning by
72
+ * up to 1.83x on a handful of parent rows, where the second statement's fixed
73
+ * cost dwarfs everything. Holding the floor at 100 rows keeps those small
74
+ * queries on the join plan; the cost of doing so, in the band where batched has
75
+ * just started to win, is under 1.2x. The upper clamp is a sanity bound for a
76
+ * pathological latency reading (a 70ms measurement would otherwise ask for
77
+ * 100K rows).
78
+ */
79
+ export declare const AUTO_TO_ONE_JOIN_ROWS_MIN = 100;
80
+ export declare const AUTO_TO_ONE_JOIN_ROWS_MAX = 100000;
16
81
  export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions, ReselectExecutor, } from './deferred.js';
17
82
  import type { DeferredQuery, MiddlewareFn, QueryInterfaceOptions } from './deferred.js';
18
83
  export declare class QueryInterface<T extends object, R extends object = {}> {
@@ -57,6 +122,23 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
57
122
  private readonly relationLoadStrategy;
58
123
  /** Client-level default for {@link applyStableRelationOrder} (off unless configured). */
59
124
  private readonly stableRelationOrder;
125
+ /**
126
+ * Client-level opt-in: apply an implicit primary-key ascending `ORDER BY` to a
127
+ * `findMany` that paginates (`limit`/`take`/`offset`) but declares no
128
+ * `orderBy`. OFF by default in core, see {@link applyImplicitPkOrdering}.
129
+ */
130
+ private readonly implicitPkOrdering;
131
+ /**
132
+ * Explicitly configured parent-row ceiling for the `'auto'` to-one rule, or
133
+ * `undefined` to derive it from the observed round-trip time. See
134
+ * {@link autoToOneThreshold}.
135
+ */
136
+ private readonly autoToOneJoinMaxRowsOption;
137
+ /**
138
+ * Deployment round-trip time in milliseconds, from which the to-one threshold
139
+ * is derived. See {@link autoToOneThreshold}.
140
+ */
141
+ private readonly autoRoundTripMs;
60
142
  /** Nested-relation JSON encoding: 'object' (default) or 'positional'. */
61
143
  private readonly jsonEncoding;
62
144
  /**
@@ -169,11 +251,23 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
169
251
  * ROWS ONLY`. No params are pushed, so the collect path is unaffected.
170
252
  */
171
253
  private limitOneClause;
254
+ /**
255
+ * Coerce a LIMIT/OFFSET argument and validate it as a non-negative safe
256
+ * integer. Numeric strings (`'5'`) coerce; everything else (`NaN`, a
257
+ * non-numeric string, a negative, a fractional or out-of-safe-range number)
258
+ * throws {@link ValidationError} (E003) naming the argument and the table.
259
+ *
260
+ * This runs on EVERY pagination path, parameterized as well as inlined: a
261
+ * bound `NaN` serializes as SQL NULL, and Postgres reads `LIMIT NULL` as
262
+ * "no limit", so an unvalidated value silently turns a paginated query into
263
+ * a full-table read (and a bad OFFSET silently disappears).
264
+ */
265
+ private paginationValue;
172
266
  /**
173
267
  * Validate a LIMIT/OFFSET value as a non-negative integer and return it as an
174
268
  * inline SQL literal. Used only on `dialect.inlineLimitOffset` engines (MySQL).
175
269
  * The input is always a Turbine-controlled pagination value, never a raw user
176
- * string and this guard guarantees the output is `String` of a validated
270
+ * string, and this guard guarantees the output is `String` of a validated
177
271
  * integer, so inlining cannot inject SQL.
178
272
  */
179
273
  private limitOffsetLiteral;
@@ -216,6 +310,97 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
216
310
  * nothing stable to order by, so both are left untouched.
217
311
  */
218
312
  private applyStableRelationOrder;
313
+ /**
314
+ * The primary key of this table as an ascending `orderBy`, in DECLARATION
315
+ * order (a composite PK orders on every column), or `undefined` for a PK-less
316
+ * table. Field names are the camelCase accessor names, so the emitted SQL goes
317
+ * through the normal column mapping.
318
+ */
319
+ private pkOrderBy;
320
+ /**
321
+ * The field names a `cursor` actually seeks on (its own keys with a defined
322
+ * value), in the canonical sorted order the cursor conditions are built in.
323
+ * Empty for a missing cursor or one whose every value is `undefined` (which
324
+ * emits no seek condition at all, so it does not paginate).
325
+ */
326
+ private cursorFields;
327
+ /**
328
+ * The ascending ordering implied by a `cursor`, or `undefined` when the shape
329
+ * is too ambiguous to order safely.
330
+ *
331
+ * A cursor seek emits `col > $n` per field (`<` when the orderBy says desc),
332
+ * so the ONLY ordering coherent with it is on the cursor's own field: ordering
333
+ * a seek on column X by column Y walks the table in an order the seek does not
334
+ * follow, which skips and repeats rows just as badly as no order at all. That
335
+ * is why this orders on the cursor field rather than blindly on the primary
336
+ * key when the two differ.
337
+ *
338
+ * Returns `undefined` (warn, inject nothing) for two shapes:
339
+ * - a MULTI-field cursor. `a > $1 AND b > $2` is a conjunction, not a proper
340
+ * composite keyset seek (`(a, b) > ($1, $2)`), so no single ORDER BY makes
341
+ * it correct. Injecting `(a asc, b asc)` would dress a broken seek up as a
342
+ * sound one.
343
+ * - a field that does not resolve to a real column. Column validation belongs
344
+ * to the normal build path, which raises a precise error; synthesizing an
345
+ * ORDER BY on it here would only change which error the caller sees.
346
+ */
347
+ private cursorOrderBy;
348
+ /**
349
+ * Whether a findMany paginates (`limit` / `take` / `offset` / `cursor`) but
350
+ * declares no ordering, which makes the returned page NON-DETERMINISTIC:
351
+ * Postgres is free to return different rows for the same unordered `LIMIT`
352
+ * once the heap changes underneath it, so a row can appear on two pages or on
353
+ * none.
354
+ *
355
+ * `cursor` counts, and is the worst case rather than an exception: a keyset
356
+ * seek with no ORDER BY is exactly this bug (`WHERE id > $1 LIMIT $2` walks
357
+ * the heap in whatever order the plan happens to produce). An empty orderBy
358
+ * (`[]`, or an object whose every value is `undefined`) counts as absent,
359
+ * because it emits no ordering.
360
+ *
361
+ * `distinct` is still excluded: that path re-orders in an outer wrapper around
362
+ * a `DISTINCT ON` whose ordering picks the representative row, so an implicit
363
+ * key would change which rows come back, not just their order.
364
+ */
365
+ private isUnorderedPage;
366
+ /**
367
+ * Opt-in (`implicitPkOrdering`) primary-key ascending ordering for a paginating
368
+ * findMany that declares no `orderBy`, making its pages deterministic.
369
+ *
370
+ * OFF by default in CORE, deliberately: turning it on would add an `ORDER BY`
371
+ * to SQL that existing applications already emit, changing both the rows a
372
+ * given page returns and the plan the engine picks. That is a breaking change
373
+ * in everything but the type signature, so it waits for a major. The
374
+ * `turbine-orm/prisma-compat` layer defaults it ON instead, because reproducing
375
+ * Prisma's semantics is that layer's whole contract.
376
+ *
377
+ * An explicit `orderBy` always wins, a PK-less table is left alone (nothing
378
+ * stable to order by), and a composite PK orders on every column in
379
+ * declaration order. A `cursor` query orders on the CURSOR's field instead
380
+ * (see {@link cursorOrderBy}), and is left alone when that shape is ambiguous.
381
+ * `distinct` shapes are skipped (see {@link isUnorderedPage}). With the flag
382
+ * off this returns `undefined` before touching anything, so the emitted SQL is
383
+ * byte-identical to before.
384
+ */
385
+ private implicitPkOrderBy;
386
+ /**
387
+ * Dev-only, once per query shape: an unordered paginating findMany returns a
388
+ * non-deterministic page (see {@link isUnorderedPage}), and on real data it is
389
+ * also usually the slower plan (an unordered `LIMIT` can discard tens of
390
+ * thousands of heap rows that an index scan on the key would have skipped).
391
+ *
392
+ * Gated exactly like the other dev diagnostics (silent under
393
+ * `NODE_ENV=production`) and consistent with `warnOnUnlimited`: a per-call
394
+ * `warnOnUnlimited: false` silences it, `true` forces it past a config-level
395
+ * opt-out, and a config/per-table `warnOnUnlimited: false` silences it. Deduped
396
+ * process-wide through the shared warn registry, so it can never spam.
397
+ *
398
+ * Suppressed only when `implicitPkOrdering` will ACTUALLY order this query.
399
+ * The flag being on is not enough: a PK-less table and an ambiguous
400
+ * multi-field cursor both get no injected ordering, and those are precisely
401
+ * the shapes that still need saying out loud.
402
+ */
403
+ private maybeWarnUnorderedPage;
219
404
  /**
220
405
  * Whether a relation can be served by the batched loader, i.e. all its
221
406
  * correlation keys are single-column (the loader throws E017 on composite
@@ -234,18 +419,63 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
234
419
  /** The `_count` verdict under `'auto'`: any counted probe unindexed + all single-key. */
235
420
  private autoCountVerdict;
236
421
  /**
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.
422
+ * Whether a query's parent set is potentially large at plan time, which is the
423
+ * only cardinality signal available before the base query runs. A `findMany`
424
+ * with no `limit`/`take` (or one above {@link autoToOneJoinMaxRows}) can return
425
+ * an arbitrary number of parent rows; a small `limit` bounds it. `findUnique` /
426
+ * `findFirst` pass `false` explicitly (their parent set is one row).
427
+ */
428
+ private autoParentSetLarge;
429
+ /**
430
+ * The parent-row count at which `'auto'` stops preferring the single-statement
431
+ * join for a to-one relation.
432
+ *
433
+ * Resolution order:
434
+ * 1. an explicit `autoToOneJoinMaxRows` — an instruction, used verbatim
435
+ * (no clamping: the caller has measured their own workload);
436
+ * 2. the configured `autoRoundTripMs` divided by
437
+ * {@link AUTO_JOIN_PENALTY_MS_PER_ROW}, clamped to
438
+ * [{@link AUTO_TO_ONE_JOIN_ROWS_MIN}, {@link AUTO_TO_ONE_JOIN_ROWS_MAX}];
439
+ * 3. {@link AUTO_TO_ONE_JOIN_MAX_ROWS}, which is that same division applied
440
+ * to {@link AUTO_ASSUMED_ROUND_TRIP_MS}.
441
+ *
442
+ * Deriving it rather than hard-coding a row count is the whole point: the
443
+ * sweep in {@link AUTO_JOIN_PENALTY_MS_PER_ROW} shows the break-even moving
444
+ * 17x between a loopback link and a 2.7ms one while the per-row penalty stays
445
+ * put, so any single constant is wrong for someone by more than the margin it
446
+ * is trying to save. Placing the switch AT the break-even is also what removes
447
+ * the old cliff: two plans that cost the same at the boundary make the regret
448
+ * there ~1.0x, rising only as the true row count moves away from it — where
449
+ * the previous fixed 1000 put its WORST case (1.44x measured) immediately
450
+ * below its own switch point.
451
+ *
452
+ * Cheap enough to recompute per call (a division and two comparisons over
453
+ * readonly fields), so there is no cached copy to invalidate.
454
+ */
455
+ private autoToOneThreshold;
456
+ /**
457
+ * Partition a top-level `with` clause under `'auto'`. A relation routes to
458
+ * `batchedWith` when it is fully batched-eligible AND either
459
+ *
460
+ * 1. its subtree has a PROVEN unindexed probe (index metadata only), or
461
+ * 2. it is TO-ONE and the parent set is potentially large
462
+ * ({@link AUTO_TO_ONE_JOIN_MAX_ROWS}), since a correlated to-one subquery
463
+ * is re-evaluated per parent row no matter how well indexed it is.
464
+ *
465
+ * Everything else (indexed to-many, composite-key, unknown) stays in `joinWith`
466
+ * (byte-identical join). The reserved `_count` key falls back on rule 1 only,
467
+ * and only for a large parent set: an inline `_count` is one correlated
468
+ * `COUNT(*)` per parent row, so the grouped follow-up wins exactly when there
469
+ * are many parents, while for a handful of parents the extra round-trip costs
470
+ * more than the repeated (small) scans. Also returns the engaged relations for
471
+ * the dev note.
242
472
  */
243
473
  private partitionWithForAuto;
244
474
  /**
245
475
  * Plan the `'auto'` split for a query's `with` clause: normalize stable order,
246
476
  * partition, and return the split ONLY when at least one relation falls back
247
477
  * 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.
478
+ * cache keys) when nothing qualifies.
249
479
  */
250
480
  private planAuto;
251
481
  /** Dev-only once-per-relation note that `'auto'` engaged the batched fallback. */
@@ -459,14 +689,19 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
459
689
  * method fall back to the full cursor path.
460
690
  *
461
691
  * **Cursor path:** Uses DECLARE CURSOR within a dedicated transaction on a
462
- * single pooled connection. The cursor is automatically closed and the
463
- * connection released when iteration completes or is terminated early
464
- * (e.g. `break` from `for await`).
692
+ * single pooled connection. The cursor is CLOSEd (in the dialect's `finally`)
693
+ * and the connection released both when iteration completes normally and when
694
+ * it ends early (`break` from `for await`). An error mid-stream skips the
695
+ * CLOSE and rolls back instead, which drops the cursor with the transaction.
465
696
  *
466
- * **Snapshot semantics note:** The speculative fast-path runs outside a
467
- * transaction. If the result overflows and the cursor path is opened, the
468
- * cursor runs in its own transaction — spanning two separate snapshots.
469
- * For strict single-snapshot semantics, wrap the call in `$transaction`.
697
+ * **Snapshot semantics note:** Outside a transaction the speculative
698
+ * fast-path runs unwrapped, and an overflow opens the cursor in its own
699
+ * transaction, so the two fetches span two separate snapshots. Wrapping the
700
+ * call in `$transaction` gives strict single-snapshot semantics: both the
701
+ * speculative fetch and the cursor then run on the caller's connection
702
+ * inside the caller's transaction (the cursor path issues no BEGIN/COMMIT of
703
+ * its own and releases nothing, so the caller's transaction is intact when
704
+ * iteration finishes).
470
705
  *
471
706
  * @example
472
707
  * ```ts
@@ -490,15 +725,15 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
490
725
  private runInImplicitTx;
491
726
  private buildNestedCtx;
492
727
  private makeTxProxy;
493
- delete(args: DeleteArgs<T>): Promise<T>;
494
- upsert(args: UpsertArgs<T>): Promise<T>;
495
- updateMany(args: UpdateManyArgs<T>): Promise<{
728
+ delete(args: DeleteArgs<T, R>): Promise<T>;
729
+ upsert(args: UpsertArgs<T, R>): Promise<T>;
730
+ updateMany(args: UpdateManyArgs<T, R>): Promise<{
496
731
  count: number;
497
732
  }>;
498
- deleteMany(args: DeleteManyArgs<T>): Promise<{
733
+ deleteMany(args: DeleteManyArgs<T, R>): Promise<{
499
734
  count: number;
500
735
  }>;
501
- count(args?: CountArgs<T>): Promise<number>;
736
+ count(args?: CountArgs<T, R>): Promise<number>;
502
737
  buildCount(args?: CountArgs<T>): DeferredQuery<number>;
503
738
  /**
504
739
  * Group rows and compute per-group aggregates (Prisma-style). The result row
@@ -508,10 +743,10 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
508
743
  * {@link GroupByResult}). Grouping by a JSON-path key yields a runtime alias
509
744
  * that cannot be typed, so those columns are not projected onto the row type.
510
745
  */
511
- groupBy<A extends GroupByArgs<T>>(args: A): Promise<GroupByResult<T, A>[]>;
746
+ groupBy<A extends GroupByArgs<T, R>>(args: A): Promise<GroupByResult<T, A>[]>;
512
747
  buildGroupBy(args: GroupByArgs<T>): DeferredQuery<Record<string, unknown>[]>;
513
748
  buildAggregate(args: AggregateArgs<T>): DeferredQuery<AggregateResult<T>>;
514
- aggregate(args: AggregateArgs<T>): Promise<AggregateResult<T>>;
749
+ aggregate(args: AggregateArgs<T, R>): Promise<AggregateResult<T>>;
515
750
  private resolveColumns;
516
751
  withFingerprint(withClause: WithClause | undefined, table?: string, depth?: number): string;
517
752
  private collectWithParams;
@@ -527,6 +762,42 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
527
762
  private getCamelDateFields;
528
763
  private makeNestedParser;
529
764
  private buildSelectWithRelations;
765
+ /**
766
+ * Compile the `relationLoadStrategy: 'flatten'` plan for a findMany-shaped
767
+ * query, or `null` to emit exactly the SQL (and cache key) the default
768
+ * strategy emits.
769
+ *
770
+ * `'flatten'` compiles an eligible to-one relation to a `LEFT JOIN` with a
771
+ * prefixed scalar projection instead of a correlated `json_build_object`
772
+ * subquery. The correlated form is re-evaluated once per parent row, so its
773
+ * cost scales with the parent set no matter how well the FK is indexed; the
774
+ * join does not, and unlike `'batched'` it stays a single round trip.
775
+ *
776
+ * It is an EXPLICIT opt-in: `'auto'` is unchanged and never selects it.
777
+ *
778
+ * Query-shape gates (any of these routes the WHOLE query back to the default
779
+ * strategy, silently and byte-identically):
780
+ * - the resolved strategy is not `'flatten'`;
781
+ * - `jsonEncoding: 'positional'` (a flattened relation emits no JSON at all,
782
+ * so the two encodings are not composed in this version);
783
+ * - the dialect owns relation-subquery generation
784
+ * (`dialect.buildRelationSubquery`, i.e. SQL Server's `FOR JSON PATH`);
785
+ * - `distinct` (the `DISTINCT ON` rewrite re-orders in an outer wrapper, and
786
+ * the extra projected columns have not been proven safe there).
787
+ *
788
+ * `limit` / `offset` / `cursor` / `orderBy` need no gate: every flattened join
789
+ * is over a PROVABLY UNIQUE target key, so it matches at most one row per
790
+ * parent and cannot change the parent row count that pagination applies to.
791
+ *
792
+ * Per-relation eligibility lives in `planFlattenWith` / `planFlattenNode`.
793
+ * Only the findMany family is planned (`findMany`, `findFirst`,
794
+ * `findManyStream`, and pipelined `buildFindMany`); `findUnique` reads a single
795
+ * parent row, where the correlated subquery runs exactly once, so it stays on
796
+ * the default path.
797
+ */
798
+ private planFlatten;
799
+ /** Dev-only once-only note that `'flatten'` was refused for the whole query. */
800
+ private warnFlattenBlocked;
530
801
  /** Convert camelCase field name to snake_case column name (unquoted, for non-SQL uses) */
531
802
  private toColumn;
532
803
  /** Convert camelCase field name to a double-quoted SQL identifier */