turbine-orm 0.81.0 → 0.90.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 (295) hide show
  1. package/README.md +26 -25
  2. package/dist/checkout.d.ts +27 -2
  3. package/dist/checkout.js +75 -4
  4. package/dist/cjs/checkout.d.ts +27 -2
  5. package/dist/cjs/checkout.js +76 -3
  6. package/dist/cjs/cli/code-first-tags.d.ts +62 -0
  7. package/dist/cjs/cli/code-first-tags.js +124 -0
  8. package/dist/cjs/cli/compile-query.js +9 -0
  9. package/dist/cjs/cli/config.d.ts +35 -6
  10. package/dist/cjs/cli/config.js +17 -1
  11. package/dist/cjs/cli/destructive.d.ts +7 -4
  12. package/dist/cjs/cli/destructive.js +224 -20
  13. package/dist/cjs/cli/error-catalog.js +36 -0
  14. package/dist/cjs/cli/error-text.d.ts +15 -0
  15. package/dist/cjs/cli/error-text.js +40 -0
  16. package/dist/cjs/cli/index.d.ts +62 -4
  17. package/dist/cjs/cli/index.js +568 -108
  18. package/dist/cjs/cli/local-server-guard.d.ts +53 -0
  19. package/dist/cjs/cli/local-server-guard.js +119 -0
  20. package/dist/cjs/cli/local-server-output.d.ts +41 -0
  21. package/dist/cjs/cli/local-server-output.js +103 -0
  22. package/dist/cjs/cli/mcp.d.ts +8 -0
  23. package/dist/cjs/cli/mcp.js +121 -32
  24. package/dist/cjs/cli/migrate.d.ts +113 -5
  25. package/dist/cjs/cli/migrate.js +270 -21
  26. package/dist/cjs/cli/observe.d.ts +6 -1
  27. package/dist/cjs/cli/observe.js +85 -55
  28. package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
  29. package/dist/cjs/cli/pii-predicate-guard.js +98 -37
  30. package/dist/cjs/cli/prisma-resolve.js +17 -13
  31. package/dist/cjs/cli/prisma-schema.js +12 -4
  32. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  33. package/dist/cjs/cli/studio.d.ts +14 -2
  34. package/dist/cjs/cli/studio.js +244 -69
  35. package/dist/cjs/cli/ui.d.ts +1 -1
  36. package/dist/cjs/client.d.ts +558 -85
  37. package/dist/cjs/client.js +1270 -395
  38. package/dist/cjs/connection-guard.d.ts +13 -0
  39. package/dist/cjs/connection-guard.js +28 -0
  40. package/dist/cjs/connection-url.d.ts +51 -16
  41. package/dist/cjs/connection-url.js +67 -34
  42. package/dist/cjs/diagnostics.d.ts +81 -0
  43. package/dist/cjs/diagnostics.js +147 -0
  44. package/dist/cjs/dialect.d.ts +449 -20
  45. package/dist/cjs/dialect.js +117 -76
  46. package/dist/cjs/engine-config.d.ts +7 -6
  47. package/dist/cjs/errors.d.ts +126 -6
  48. package/dist/cjs/errors.js +256 -13
  49. package/dist/cjs/generate.d.ts +57 -7
  50. package/dist/cjs/generate.js +635 -279
  51. package/dist/cjs/global-filters-config.d.ts +59 -0
  52. package/dist/cjs/global-filters-config.js +367 -0
  53. package/dist/cjs/index.d.ts +9 -4
  54. package/dist/cjs/index.js +10 -2
  55. package/dist/cjs/introspect.d.ts +31 -6
  56. package/dist/cjs/introspect.js +108 -50
  57. package/dist/cjs/log-codes.d.ts +12 -0
  58. package/dist/cjs/log-codes.js +77 -0
  59. package/dist/cjs/logger.d.ts +87 -0
  60. package/dist/cjs/logger.js +98 -0
  61. package/dist/cjs/mssql.d.ts +8 -5
  62. package/dist/cjs/mssql.js +352 -65
  63. package/dist/cjs/mysql.d.ts +3 -5
  64. package/dist/cjs/mysql.js +420 -70
  65. package/dist/cjs/nested-write.d.ts +3 -0
  66. package/dist/cjs/nested-write.js +179 -114
  67. package/dist/cjs/pipeline-submittable.d.ts +12 -0
  68. package/dist/cjs/pipeline-submittable.js +118 -44
  69. package/dist/cjs/pipeline.js +21 -26
  70. package/dist/cjs/powdb-introspect.d.ts +5 -2
  71. package/dist/cjs/powdb-introspect.js +20 -15
  72. package/dist/cjs/powdb-shared.d.ts +36 -5
  73. package/dist/cjs/powdb-shared.js +67 -8
  74. package/dist/cjs/powdb.d.ts +4 -0
  75. package/dist/cjs/powdb.js +183 -51
  76. package/dist/cjs/powql.d.ts +394 -51
  77. package/dist/cjs/powql.js +1829 -284
  78. package/dist/cjs/prisma-compat.d.ts +23 -15
  79. package/dist/cjs/prisma-compat.js +507 -372
  80. package/dist/cjs/query/aggregates.d.ts +17 -1
  81. package/dist/cjs/query/aggregates.js +261 -29
  82. package/dist/cjs/query/batched-loader.d.ts +25 -6
  83. package/dist/cjs/query/batched-loader.js +103 -20
  84. package/dist/cjs/query/builder.d.ts +325 -184
  85. package/dist/cjs/query/builder.js +1452 -490
  86. package/dist/cjs/query/column-ref.d.ts +61 -0
  87. package/dist/cjs/query/column-ref.js +73 -0
  88. package/dist/cjs/query/compound-unique.d.ts +21 -0
  89. package/dist/cjs/query/compound-unique.js +98 -18
  90. package/dist/cjs/query/deferred.d.ts +53 -16
  91. package/dist/cjs/query/expr.d.ts +217 -0
  92. package/dist/cjs/query/expr.js +859 -0
  93. package/dist/cjs/query/filters.d.ts +155 -53
  94. package/dist/cjs/query/filters.js +613 -76
  95. package/dist/cjs/query/index.d.ts +2 -0
  96. package/dist/cjs/query/index.js +7 -1
  97. package/dist/cjs/query/json-value.d.ts +43 -0
  98. package/dist/cjs/query/json-value.js +59 -0
  99. package/dist/cjs/query/normalize-args.d.ts +87 -0
  100. package/dist/cjs/query/normalize-args.js +527 -0
  101. package/dist/cjs/query/option-surface.d.ts +16 -4
  102. package/dist/cjs/query/option-surface.js +57 -14
  103. package/dist/cjs/query/relation-names.d.ts +14 -0
  104. package/dist/cjs/query/relation-names.js +77 -18
  105. package/dist/cjs/query/relations.d.ts +30 -3
  106. package/dist/cjs/query/relations.js +219 -63
  107. package/dist/cjs/query/row-lock.d.ts +49 -0
  108. package/dist/cjs/query/row-lock.js +138 -0
  109. package/dist/cjs/query/sql-comment.d.ts +161 -0
  110. package/dist/cjs/query/sql-comment.js +381 -0
  111. package/dist/cjs/query/stream-idle.d.ts +132 -0
  112. package/dist/cjs/query/stream-idle.js +212 -0
  113. package/dist/cjs/query/table-client.d.ts +101 -0
  114. package/dist/cjs/query/table-client.js +74 -0
  115. package/dist/cjs/query/types.d.ts +677 -298
  116. package/dist/cjs/query/utils.d.ts +105 -6
  117. package/dist/cjs/query/utils.js +426 -19
  118. package/dist/cjs/query/warn-registry.d.ts +18 -0
  119. package/dist/cjs/query/warn-registry.js +18 -0
  120. package/dist/cjs/query/where-compile.d.ts +138 -14
  121. package/dist/cjs/query/where-compile.js +513 -29
  122. package/dist/cjs/query/where.d.ts +159 -59
  123. package/dist/cjs/query/where.js +716 -246
  124. package/dist/cjs/query/writes.d.ts +112 -50
  125. package/dist/cjs/query/writes.js +1498 -200
  126. package/dist/cjs/query-events.d.ts +4 -2
  127. package/dist/cjs/query-events.js +3 -1
  128. package/dist/cjs/realtime.d.ts +7 -3
  129. package/dist/cjs/realtime.js +13 -2
  130. package/dist/cjs/schema-builder.d.ts +55 -4
  131. package/dist/cjs/schema-builder.js +382 -18
  132. package/dist/cjs/schema-metadata.d.ts +21 -1
  133. package/dist/cjs/schema-metadata.js +29 -7
  134. package/dist/cjs/schema-sql.d.ts +75 -5
  135. package/dist/cjs/schema-sql.js +497 -45
  136. package/dist/cjs/schema.d.ts +26 -5
  137. package/dist/cjs/schema.js +110 -13
  138. package/dist/cjs/sql-placeholders.d.ts +56 -0
  139. package/dist/cjs/sql-placeholders.js +204 -0
  140. package/dist/cjs/sqlite.d.ts +75 -20
  141. package/dist/cjs/sqlite.js +593 -95
  142. package/dist/cjs/supplied-pool-types.d.ts +48 -0
  143. package/dist/cjs/supplied-pool-types.js +256 -0
  144. package/dist/cjs/transaction-control.d.ts +51 -0
  145. package/dist/cjs/transaction-control.js +316 -0
  146. package/dist/cjs/transaction-state.d.ts +342 -0
  147. package/dist/cjs/transaction-state.js +707 -0
  148. package/dist/cjs/typed-sql.d.ts +37 -3
  149. package/dist/cjs/typed-sql.js +102 -8
  150. package/dist/cli/code-first-tags.d.ts +62 -0
  151. package/dist/cli/code-first-tags.js +120 -0
  152. package/dist/cli/compile-query.js +9 -0
  153. package/dist/cli/config.d.ts +35 -6
  154. package/dist/cli/config.js +16 -1
  155. package/dist/cli/destructive.d.ts +7 -4
  156. package/dist/cli/destructive.js +224 -20
  157. package/dist/cli/error-catalog.js +36 -0
  158. package/dist/cli/error-text.d.ts +15 -0
  159. package/dist/cli/error-text.js +37 -0
  160. package/dist/cli/index.d.ts +62 -4
  161. package/dist/cli/index.js +569 -112
  162. package/dist/cli/local-server-guard.d.ts +53 -0
  163. package/dist/cli/local-server-guard.js +113 -0
  164. package/dist/cli/local-server-output.d.ts +41 -0
  165. package/dist/cli/local-server-output.js +98 -0
  166. package/dist/cli/mcp.d.ts +8 -0
  167. package/dist/cli/mcp.js +121 -34
  168. package/dist/cli/migrate.d.ts +113 -5
  169. package/dist/cli/migrate.js +261 -21
  170. package/dist/cli/observe.d.ts +6 -1
  171. package/dist/cli/observe.js +84 -54
  172. package/dist/cli/pii-predicate-guard.d.ts +43 -23
  173. package/dist/cli/pii-predicate-guard.js +100 -35
  174. package/dist/cli/prisma-resolve.js +15 -11
  175. package/dist/cli/prisma-schema.js +12 -4
  176. package/dist/cli/studio-ui.generated.js +1 -1
  177. package/dist/cli/studio.d.ts +14 -2
  178. package/dist/cli/studio.js +246 -71
  179. package/dist/cli/ui.d.ts +1 -1
  180. package/dist/client.d.ts +558 -85
  181. package/dist/client.js +1275 -400
  182. package/dist/connection-guard.d.ts +13 -0
  183. package/dist/connection-guard.js +27 -0
  184. package/dist/connection-url.d.ts +51 -16
  185. package/dist/connection-url.js +63 -34
  186. package/dist/diagnostics.d.ts +81 -0
  187. package/dist/diagnostics.js +144 -0
  188. package/dist/dialect.d.ts +449 -20
  189. package/dist/dialect.js +115 -43
  190. package/dist/engine-config.d.ts +7 -6
  191. package/dist/errors.d.ts +126 -6
  192. package/dist/errors.js +252 -12
  193. package/dist/generate.d.ts +57 -7
  194. package/dist/generate.js +635 -281
  195. package/dist/global-filters-config.d.ts +59 -0
  196. package/dist/global-filters-config.js +361 -0
  197. package/dist/index.d.ts +9 -4
  198. package/dist/index.js +4 -2
  199. package/dist/introspect.d.ts +31 -6
  200. package/dist/introspect.js +106 -50
  201. package/dist/log-codes.d.ts +12 -0
  202. package/dist/log-codes.js +74 -0
  203. package/dist/logger.d.ts +87 -0
  204. package/dist/logger.js +93 -0
  205. package/dist/mssql.d.ts +8 -5
  206. package/dist/mssql.js +354 -67
  207. package/dist/mysql.d.ts +3 -5
  208. package/dist/mysql.js +423 -73
  209. package/dist/nested-write.d.ts +3 -0
  210. package/dist/nested-write.js +177 -112
  211. package/dist/pipeline-submittable.d.ts +12 -0
  212. package/dist/pipeline-submittable.js +117 -44
  213. package/dist/pipeline.js +21 -26
  214. package/dist/powdb-introspect.d.ts +5 -2
  215. package/dist/powdb-introspect.js +21 -16
  216. package/dist/powdb-shared.d.ts +36 -5
  217. package/dist/powdb-shared.js +65 -8
  218. package/dist/powdb.d.ts +4 -0
  219. package/dist/powdb.js +183 -51
  220. package/dist/powql.d.ts +394 -51
  221. package/dist/powql.js +1836 -291
  222. package/dist/prisma-compat.d.ts +23 -15
  223. package/dist/prisma-compat.js +508 -373
  224. package/dist/query/aggregates.d.ts +17 -1
  225. package/dist/query/aggregates.js +263 -32
  226. package/dist/query/batched-loader.d.ts +25 -6
  227. package/dist/query/batched-loader.js +101 -21
  228. package/dist/query/builder.d.ts +325 -184
  229. package/dist/query/builder.js +1458 -495
  230. package/dist/query/column-ref.d.ts +61 -0
  231. package/dist/query/column-ref.js +66 -0
  232. package/dist/query/compound-unique.d.ts +21 -0
  233. package/dist/query/compound-unique.js +97 -19
  234. package/dist/query/deferred.d.ts +53 -16
  235. package/dist/query/expr.d.ts +217 -0
  236. package/dist/query/expr.js +848 -0
  237. package/dist/query/filters.d.ts +155 -53
  238. package/dist/query/filters.js +597 -72
  239. package/dist/query/index.d.ts +2 -0
  240. package/dist/query/index.js +4 -0
  241. package/dist/query/json-value.d.ts +43 -0
  242. package/dist/query/json-value.js +54 -0
  243. package/dist/query/normalize-args.d.ts +87 -0
  244. package/dist/query/normalize-args.js +517 -0
  245. package/dist/query/option-surface.d.ts +16 -4
  246. package/dist/query/option-surface.js +56 -13
  247. package/dist/query/relation-names.d.ts +14 -0
  248. package/dist/query/relation-names.js +77 -19
  249. package/dist/query/relations.d.ts +30 -3
  250. package/dist/query/relations.js +220 -67
  251. package/dist/query/row-lock.d.ts +49 -0
  252. package/dist/query/row-lock.js +131 -0
  253. package/dist/query/sql-comment.d.ts +161 -0
  254. package/dist/query/sql-comment.js +374 -0
  255. package/dist/query/stream-idle.d.ts +132 -0
  256. package/dist/query/stream-idle.js +203 -0
  257. package/dist/query/table-client.d.ts +101 -0
  258. package/dist/query/table-client.js +70 -0
  259. package/dist/query/types.d.ts +677 -298
  260. package/dist/query/utils.d.ts +105 -6
  261. package/dist/query/utils.js +414 -18
  262. package/dist/query/warn-registry.d.ts +18 -0
  263. package/dist/query/warn-registry.js +18 -0
  264. package/dist/query/where-compile.d.ts +138 -14
  265. package/dist/query/where-compile.js +508 -31
  266. package/dist/query/where.d.ts +159 -59
  267. package/dist/query/where.js +711 -248
  268. package/dist/query/writes.d.ts +112 -50
  269. package/dist/query/writes.js +1496 -202
  270. package/dist/query-events.d.ts +4 -2
  271. package/dist/query-events.js +3 -1
  272. package/dist/realtime.d.ts +7 -3
  273. package/dist/realtime.js +13 -2
  274. package/dist/schema-builder.d.ts +55 -4
  275. package/dist/schema-builder.js +381 -18
  276. package/dist/schema-metadata.d.ts +21 -1
  277. package/dist/schema-metadata.js +28 -7
  278. package/dist/schema-sql.d.ts +75 -5
  279. package/dist/schema-sql.js +494 -44
  280. package/dist/schema.d.ts +26 -5
  281. package/dist/schema.js +109 -13
  282. package/dist/sql-placeholders.d.ts +56 -0
  283. package/dist/sql-placeholders.js +200 -0
  284. package/dist/sqlite.d.ts +75 -20
  285. package/dist/sqlite.js +597 -99
  286. package/dist/supplied-pool-types.d.ts +48 -0
  287. package/dist/supplied-pool-types.js +247 -0
  288. package/dist/transaction-control.d.ts +51 -0
  289. package/dist/transaction-control.js +311 -0
  290. package/dist/transaction-state.d.ts +342 -0
  291. package/dist/transaction-state.js +696 -0
  292. package/dist/typed-sql.d.ts +37 -3
  293. package/dist/typed-sql.js +100 -8
  294. package/package.json +7 -4
  295. package/skills/turbine-orm/SKILL.md +56 -6
@@ -46,109 +46,78 @@ var __importStar = (this && this.__importStar) || (function () {
46
46
  })();
47
47
  Object.defineProperty(exports, "__esModule", { value: true });
48
48
  exports.QueryInterface = exports.AUTO_COUNT_BATCH_MIN_PARENT_ROWS = exports.AUTO_TO_ONE_JOIN_ROWS_MAX = exports.AUTO_TO_ONE_JOIN_ROWS_MIN = exports.AUTO_TO_ONE_JOIN_MAX_ROWS = exports.AUTO_ASSUMED_ROUND_TRIP_MS = exports.AUTO_JOIN_PENALTY_MS_PER_ROW = void 0;
49
- exports.resetCacheCrossCheckEnv = resetCacheCrossCheckEnv;
50
49
  exports.unlockNestedWriteTx = unlockNestedWriteTx;
51
50
  const checkout_js_1 = require("../checkout.js");
52
51
  const connection_guard_js_1 = require("../connection-guard.js");
52
+ const diagnostics_js_1 = require("../diagnostics.js");
53
53
  const dialect_js_1 = require("../dialect.js");
54
54
  const errors_js_1 = require("../errors.js");
55
55
  const index_advisor_js_1 = require("../index-advisor.js");
56
+ const logger_js_1 = require("../logger.js");
56
57
  const nested_write_js_1 = require("../nested-write.js");
57
58
  const schema_js_1 = require("../schema.js");
59
+ const transaction_state_js_1 = require("../transaction-state.js");
58
60
  const aggMod = __importStar(require("./aggregates.js"));
59
61
  const batched_loader_js_1 = require("./batched-loader.js");
60
62
  const compound_unique_js_1 = require("./compound-unique.js");
63
+ const expr_js_1 = require("./expr.js");
61
64
  const filters_js_1 = require("./filters.js");
65
+ const normalize_args_js_1 = require("./normalize-args.js");
62
66
  const option_surface_js_1 = require("./option-surface.js");
63
67
  const relation_names_js_1 = require("./relation-names.js");
64
68
  const relationsMod = __importStar(require("./relations.js"));
69
+ const row_lock_js_1 = require("./row-lock.js");
70
+ const sql_comment_js_1 = require("./sql-comment.js");
71
+ const stream_idle_js_1 = require("./stream-idle.js");
65
72
  const types_js_1 = require("./types.js");
66
73
  const utils_js_1 = require("./utils.js");
67
74
  const warn_registry_js_1 = require("./warn-registry.js");
68
75
  const whereMod = __importStar(require("./where.js"));
69
76
  const writesMod = __importStar(require("./writes.js"));
70
77
  /**
71
- * SQL-cache lockstep cross-check gate.
78
+ * SQL-cache lockstep cross-check.
72
79
  *
73
80
  * The SQL template cache requires three code paths to enumerate where-clause
74
81
  * keys identically: `fingerprintWhere` (builds the cache key),
75
82
  * `buildWhereClause` (builds SQL + `$N` params on a MISS), and
76
83
  * `collectWhereParams` (re-collects params on a HIT without rebuilding). Since
77
84
  * 0.36 all three consume ONE shared where-key walk (see `where-compile.ts`), so
78
- * enumeration drift is structurally eliminated; this check remains as a
85
+ * enumeration drift is structurally eliminated; these checks remain as a
79
86
  * tripwire in case a future leaf builder or its collect mirror falls out of
80
- * step. It rebuilds the SQL + params fresh on a cache HIT and compares them
81
- * against what the cache-hit path produced.
87
+ * step. Every failure is an {@link InvariantError} (E020), thrown before the
88
+ * statement is sent.
82
89
  *
83
- * Modes (decided per HIT; env read inline, not captured once, so tests and
84
- * perf-sensitive traffic can toggle per process):
85
- * - `'dev'`: `NODE_ENV !== 'production'` and `TURBINE_DISABLE_CACHE_CHECK !== '1'`
86
- * (always checks; the historical default, the whole unit suite runs under it).
87
- * - `'off'`: dev with `TURBINE_DISABLE_CACHE_CHECK=1`, OR production with no
88
- * sampling configured (the hot path is untouched).
89
- * - `'sampled'`: production with `TURBINE_CACHE_CHECK_SAMPLE` set to a float in
90
- * `(0,1]`, re-verify that fraction of cache hits (per-hit `Math.random()`;
91
- * `>= 1` checks every hit). A mismatch logs once per distinct fingerprint AND
92
- * throws, same as dev. `0`, unset, or an unparseable value means never check.
93
- */
94
- function cacheCrossCheckMode() {
95
- const env = crossCheckEnv ?? readCrossCheckEnv();
96
- if (env.mode !== 'sampled')
97
- return env.mode;
98
- // The SAMPLING decision, and only it, is genuinely per hit.
99
- return env.rate >= 1 || Math.random() < env.rate ? 'sampled' : 'off';
100
- }
101
- /**
102
- * The env-derived half of {@link cacheCrossCheckMode}, resolved ONCE per
103
- * process and memoized.
104
- *
105
- * `process.env` is not a plain object: every property read crosses a C++
106
- * interceptor. Measured at 112.7 ns per read against 3.6 ns for a module-level
107
- * constant, and this function ran TWO reads on every cache hit, which put it
108
- * at the top of the SQL-build CPU profile (12.53% self time, with a further
109
- * ~12% inside the interceptor) for a value that does not change.
110
- *
111
- * It is memoized rather than hoisted to a `const` because the toggle IS
112
- * observed at runtime, contrary to what a quick look suggests: the
113
- * cross-check and sampling suites flip `NODE_ENV` /
114
- * `TURBINE_DISABLE_CACHE_CHECK` / `TURBINE_CACHE_CHECK_SAMPLE` around a
115
- * synchronous block in-process, not at spawn. So the read is deferred to first
116
- * use and {@link resetCacheCrossCheckEnv} re-arms it; that is the entire
117
- * contract, and the only thing lost versus reading every time is that a
118
- * process which mutates these variables mid-run must say so.
119
- */
120
- let crossCheckEnv;
121
- function readCrossCheckEnv() {
122
- let resolved;
123
- if (process.env.NODE_ENV !== 'production') {
124
- resolved = { mode: process.env.TURBINE_DISABLE_CACHE_CHECK === '1' ? 'off' : 'dev' };
125
- }
126
- else {
127
- const raw = process.env.TURBINE_CACHE_CHECK_SAMPLE;
128
- const rate = raw === undefined ? Number.NaN : Number.parseFloat(raw);
129
- resolved = !Number.isFinite(rate) || rate <= 0 ? { mode: 'off' } : { mode: 'sampled', rate };
130
- }
131
- crossCheckEnv = resolved;
132
- return resolved;
133
- }
134
- /**
135
- * Discard the memoized cross-check environment so the next cache hit re-reads
136
- * `process.env`.
90
+ * Two checks always run, whatever the diagnostics mode, because each costs one
91
+ * array compare and the alternative is executing parameters known to be wrong:
92
+ * - On a MISS, the params the build closure pushed are compared with the params
93
+ * the collect path produced (`sql-cache-lockstep`). The miss already paid
94
+ * for both walks, and a shape run once per instance is otherwise never
95
+ * compared at all.
96
+ * - On a HIT, the collected param count must equal the count the entry was
97
+ * built with (`sql-cache-param-count`). Otherwise the server would reject
98
+ * the statement with a bind error that reads like a connection problem.
137
99
  *
138
- * @internal Exposed for the tests that toggle these variables in-process (see
139
- * {@link crossCheckEnv}). Production code sets them before the first query and
140
- * never again.
100
+ * The full rebuild-and-compare on a HIT follows the client's
101
+ * `diagnostics.cacheCheck`, resolved once at construction (see diagnostics.ts;
102
+ * the legacy `TURBINE_DISABLE_CACHE_CHECK` / `TURBINE_CACHE_CHECK_SAMPLE`
103
+ * variables are read there and nowhere else):
104
+ * - `'every-hit'` (the development default): every hit is compared, and a
105
+ * mismatch throws.
106
+ * - `{ sample }`: that fraction of hits is compared (per-hit `Math.random()`;
107
+ * a sample of 1 compares every hit). A mismatch is logged at `error` once
108
+ * per distinct fingerprint, then throws.
109
+ * - `'off'` (the production default): never rebuilt on a hit.
141
110
  */
142
- function resetCacheCrossCheckEnv() {
143
- crossCheckEnv = undefined;
144
- }
145
111
  /**
146
- * Distinct cache-mismatch fingerprints already logged by the sampled production
147
- * cross-check, so a hot mismatch logs `console.error` exactly once per shape
148
- * instead of flooding the log. Dev mode does not log (it throws immediately),
149
- * so this set is only ever touched on the `'sampled'` path.
112
+ * Warn-once namespace for the sampled cross-check's mismatch log, so a hot
113
+ * mismatch logs exactly once per table and shape instead of flooding the log.
114
+ * `'every-hit'` does not log (it throws immediately), so only the sampled path
115
+ * records here. Keyed on the TABLE as well as the fingerprint: the fingerprint
116
+ * alone carries no table, so a mismatch on one table used to silence the same
117
+ * shape's mismatch on every other table. The shared registry also bounds the
118
+ * set (`WARN_ONCE_CAP`) and is what `resetWarnOnce` clears.
150
119
  */
151
- const loggedCacheMismatchFingerprints = new Set();
120
+ const CACHE_MISMATCH_WARN_NS = 'cacheMismatch';
152
121
  /**
153
122
  * Marginal cost of keeping a to-one relation on the JOIN plan, per parent row.
154
123
  *
@@ -306,6 +275,29 @@ const ROW_PLAN_CACHE_SIZE = 128;
306
275
  * probe's own limit.
307
276
  */
308
277
  const STREAM_SPECULATION_MAX_BATCH = 1000;
278
+ /** Per-cell decode work in a {@link RowDecodePlan}. */
279
+ const CELL_PLAIN = 0;
280
+ const CELL_TEMPORAL = 1;
281
+ const CELL_FLOAT = 2;
282
+ const CELL_FLOAT_ARRAY = 3;
283
+ const CELL_TEXT_ARRAY = 4;
284
+ /** The dialect's own decode for a type whose storage differs (see `Dialect.decodeColumnValue`). */
285
+ const CELL_DIALECT = 5;
286
+ const FLOAT_TYPES = new Set(['float4', 'float8', 'real', 'double precision']);
287
+ const FLOAT_ARRAY_TYPES = new Set(['_float4', '_float8', 'float4[]', 'float8[]', 'real[]', 'double precision[]']);
288
+ /**
289
+ * The decode a column of `pgType` needs beyond the driver's, other than
290
+ * temporal: a float can arrive from JSON as "NaN" / "Infinity", and an enum
291
+ * array, which the driver has no parser for, as array text.
292
+ */
293
+ function cellKindOf(pgType, enums) {
294
+ if (FLOAT_TYPES.has(pgType))
295
+ return CELL_FLOAT;
296
+ if (FLOAT_ARRAY_TYPES.has(pgType))
297
+ return CELL_FLOAT_ARRAY;
298
+ const element = pgType.startsWith('_') ? pgType.slice(1) : pgType.endsWith('[]') ? pgType.slice(0, -2) : undefined;
299
+ return element !== undefined && enums !== undefined && Object.hasOwn(enums, element) ? CELL_TEXT_ARRAY : CELL_PLAIN;
300
+ }
309
301
  /**
310
302
  * Strict structural equality for a single SQL parameter value. Handles the
311
303
  * value shapes Turbine binds: primitives (incl. `NaN` and `bigint`), `null`/
@@ -448,35 +440,95 @@ function unlockNestedWriteTx(tx) {
448
440
  */
449
441
  // Moved to ./filters.ts in 0.74.0 so PowqlInterface consumes the SAME
450
442
  // predicate rather than a second copy of it. See applyStableRelationOrderTo.
443
+ /**
444
+ * Marks `findMany` args built for a `findFirst`: one row, not a page, so
445
+ * {@link QueryInterface.completePageOrder} leaves its order as written. A
446
+ * symbol, so it survives the spreads at the args seam and no JSON body can
447
+ * set it. Set through {@link firstRowOnly} only.
448
+ */
449
+ const FIRST_ROW_ONLY = Symbol('turbine.firstRowOnly');
450
+ /**
451
+ * The {@link FIRST_ROW_ONLY} mark for a read the caller bounded with neither
452
+ * `limit` nor `offset`. An `offset` makes the row depend on which tied rows it
453
+ * skipped, so `findFirst({ orderBy, offset })` is a page of one and gets the
454
+ * page tiebreaker like any other page.
455
+ */
456
+ function firstRowOnly(args) {
457
+ return args?.limit === undefined && args?.offset === undefined ? { [FIRST_ROW_ONLY]: true } : {};
458
+ }
459
+ /** The largest delay `setTimeout` honours; Node fires anything larger after 1 ms. */
460
+ const MAX_TIMEOUT_MS = 2_147_483_647;
461
+ /** The accepted `relationLoadStrategy` values. */
462
+ const RELATION_LOAD_STRATEGIES = Object.freeze([
463
+ 'join',
464
+ 'batched',
465
+ 'auto',
466
+ 'flatten',
467
+ ]);
468
+ /** A per-query `timeout`: milliseconds up to {@link MAX_TIMEOUT_MS}, or 0 for none (the same rule as `$transaction`). */
469
+ function isValidTimeout(value) {
470
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= MAX_TIMEOUT_MS;
471
+ }
472
+ /** A short, value-safe rendering of a rejected option value. */
473
+ function describeOptionValue(value) {
474
+ if (typeof value === 'number' || typeof value === 'boolean')
475
+ return String(value);
476
+ if (typeof value === 'string')
477
+ return JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}...` : value);
478
+ return value === null ? 'null' : `a value of type ${typeof value}`;
479
+ }
480
+ function relationLoadStrategyMessage(value) {
481
+ return (`\`relationLoadStrategy\` must be one of ${RELATION_LOAD_STRATEGIES.map((v) => `'${v}'`).join(', ')} ` +
482
+ `(received ${describeOptionValue(value)}).`);
483
+ }
484
+ /** A value the findUnique simple path may bind as it is: a primitive, Date or Buffer. */
485
+ function isFastPathScalar(value) {
486
+ switch (typeof value) {
487
+ case 'string':
488
+ case 'number':
489
+ case 'bigint':
490
+ case 'boolean':
491
+ return true;
492
+ case 'object':
493
+ return value instanceof Date || (value !== null && ArrayBuffer.isView(value));
494
+ default:
495
+ return false;
496
+ }
497
+ }
498
+ /** Resolves a race when a deadline passes first. */
499
+ const TIMED_OUT = Symbol('timedOut');
500
+ /**
501
+ * How long a statement that outlived its `timeout` gets, after the cancel
502
+ * request is sent, to answer before its connection is destroyed instead of
503
+ * reused. A cancel normally lands within a round trip or two.
504
+ */
505
+ const CANCEL_GRACE_MS = 2000;
506
+ /** Whether `err` is PostgreSQL's query_canceled (57014), as the driver raised it or as Turbine wrapped it. */
507
+ function isQueryCanceled(err) {
508
+ const e = err;
509
+ return e?.code === '57014' || e?.cause?.code === '57014';
510
+ }
511
+ /** The {@link DeferredQuery.rowLock} marker for a locking read, or nothing. */
512
+ function rowLockMarker(rowLock) {
513
+ return rowLock ? { rowLock: true } : {};
514
+ }
451
515
  /** The two accepted {@link JsonEncoding} values, frozen so the check is total. */
452
516
  const JSON_ENCODINGS = Object.freeze(['object', 'positional']);
453
517
  /**
454
518
  * The relation JSON encoding a client on `dialect` gets when it names none.
455
519
  *
456
- * `'positional'` (`json_build_array`) on PostgreSQL, `'object'`
457
- * (`json_build_object`) everywhere else. The positional form drops the repeated
458
- * key names from every relation row: measured on a 50-parent / ~10-child read
459
- * against local PostgreSQL 17, server time 0.685 ms → 0.350 ms and 152 KB →
460
- * 100 KB on the wire, returning byte-identical parsed rows.
461
- *
462
- * ## Why the test is `dialect.name`, not "does the dialect have buildJsonArray"
463
- *
464
- * Because a presence test does not distinguish engines HERE. `buildJsonArray`
465
- * is declared on `postgresDialect`, and sqlite.ts / mysql.ts / mssql.ts /
466
- * powdb.ts each build their dialect by SPREADING it, so every engine inherits
467
- * the hook and an "is it absent" fallback never fires. That is the documented
468
- * inheritance trap (see `buildPartitionLimit` in {@link
469
- * QueryInterface.batchedContext} and the `distinct` gate in
470
- * {@link QueryInterface.buildFindMany}), and it is exactly the shape that
471
- * silently turned the partition-limit window on for SQLite.
520
+ * `'positional'` (`json_build_array`) where the engine has it (PostgreSQL),
521
+ * `'object'` (`json_build_object`) everywhere else. The positional form drops
522
+ * the repeated key names from every relation row: measured on a 50-parent /
523
+ * ~10-child read against local PostgreSQL 17, server time 0.685 ms → 0.350 ms
524
+ * and 152 KB → 100 KB on the wire, returning byte-identical parsed rows.
472
525
  *
473
- * A new capability flag would work, but only if every engine set it
474
- * explicitly, and it would be a SECOND authority on the same question:
475
- * `buildSelectWithRelations` (relations.ts) already refuses `'positional'` with
476
- * E017 on `dialect.name !== 'postgresql'`. Deriving the default from the
477
- * identical predicate is what makes it impossible for the default to select an
478
- * encoding the builder then refuses. A flag could drift from that refusal; this
479
- * cannot.
526
+ * Read from `capabilities.positionalJson`, never from whether the dialect has
527
+ * `buildJsonArray`: that hook is on `postgresDialect` and every engine inherits
528
+ * it by spreading, so a presence test would hand every engine the positional
529
+ * default. `buildSelectWithRelations` (relations.ts) refuses `'positional'` on
530
+ * the SAME flag, so the default can never select an encoding the builder then
531
+ * refuses: one authority, not two.
480
532
  *
481
533
  * A wire-compatible engine that reaches this on `postgresDialect` itself
482
534
  * (CockroachDB, YugabyteDB, AlloyDB, Timescale, all of which are ADAPTERS over
@@ -484,9 +536,8 @@ const JSON_ENCODINGS = Object.freeze(['object', 'positional']);
484
536
  * which is correct: they speak `json_build_array`.
485
537
  */
486
538
  function defaultJsonEncoding(dialect) {
487
- return dialect.name === 'postgresql' ? 'positional' : 'object';
539
+ return dialect.capabilities.positionalJson ? 'positional' : 'object';
488
540
  }
489
- // biome-ignore lint/complexity/noBannedTypes: {} means "no relations known", intentional for untyped table access
490
541
  class QueryInterface {
491
542
  pool;
492
543
  table;
@@ -507,6 +558,13 @@ class QueryInterface {
507
558
  * inline, not through the top-level cache).
508
559
  */
509
560
  lastCacheHit = false;
561
+ /**
562
+ * The params the build closure pushed on the most recent MISS, held until
563
+ * {@link crossCheckCache} compares them with the collected params and clears
564
+ * them. `undefined` after a hit. Same single-flag reasoning as
565
+ * {@link lastCacheHit}.
566
+ */
567
+ lastMissParams;
510
568
  /**
511
569
  * Set while a build closure runs when the statement's WHERE (or HAVING)
512
570
  * carries a caller-written `AND`/`OR` combinator array, i.e. when its SQL text
@@ -518,6 +576,21 @@ class QueryInterface {
518
576
  */
519
577
  variableArityShape = false;
520
578
  middlewares;
579
+ /** The client's resolved `diagnostics` (see diagnostics.ts), fixed for this instance's life. */
580
+ diagnostics;
581
+ /** `streamIdleTimeoutMs`: how long a cursor stream may wait for its consumer. `0` disables. */
582
+ streamIdleTimeoutMs;
583
+ /** `sqlComment`, resolved; `undefined` when statements carry no comment (one falsy check per send). */
584
+ sqlCommenter;
585
+ /** Where this instance's log lines go: the client's `logger`, else the console logger. */
586
+ logger;
587
+ /**
588
+ * {@link logger} when `diagnostics.advice` is on, else `undefined`. Every
589
+ * per-query advice site starts with `const log = this.advice; if (!log)
590
+ * return;`, so with advice off a site costs one property read and builds no
591
+ * message.
592
+ */
593
+ advice;
521
594
  defaultLimit;
522
595
  warnOnUnlimited;
523
596
  scopedConnect;
@@ -619,7 +692,6 @@ class QueryInterface {
619
692
  * the audit's "Set<string>" guidance and leaves room for future
620
693
  * cross-table sharing.
621
694
  */
622
- warnedTables = new Set();
623
695
  /** Cache hit/miss counters for diagnostics */
624
696
  cacheHits = 0;
625
697
  cacheMisses = 0;
@@ -640,6 +712,10 @@ class QueryInterface {
640
712
  * to Date as well (otherwise nested dates leak through as strings).
641
713
  */
642
714
  camelDateFieldCache = new Map();
715
+ /** Per-table memo of {@link cellKindOf}, keyed by column name and field name. */
716
+ cellKindCache = new Map();
717
+ /** The dialect decode of each {@link CELL_DIALECT} column, by table, then by column and field name. */
718
+ dialectDecodeCache = new Map();
643
719
  /**
644
720
  * Per-table memo of `Object.entries(meta.relations)`, consumed by the nested
645
721
  * row parser (see `getRelationEntries` in relations.ts). Same rationale and
@@ -713,7 +789,14 @@ class QueryInterface {
713
789
  }
714
790
  this.tableMeta = meta;
715
791
  this.middlewares = middlewares ?? [];
792
+ // Resolved by the client once; a QueryInterface built directly (tests,
793
+ // tooling) resolves here, at construction, never per call.
794
+ this.diagnostics = (0, diagnostics_js_1.resolveDiagnostics)(options?.diagnostics);
795
+ this.logger = options?.logger ?? logger_js_1.consoleLogger;
796
+ this.advice = this.diagnostics.advice ? this.logger : undefined;
716
797
  this.defaultLimit = options?.defaultLimit;
798
+ this.streamIdleTimeoutMs =
799
+ (0, stream_idle_js_1.resolveStreamIdleTimeout)(options?.streamIdleTimeoutMs, 'streamIdleTimeoutMs') ?? stream_idle_js_1.DEFAULT_STREAM_IDLE_TIMEOUT_MS;
717
800
  // Default to ON: surfacing accidental full-table scans is more valuable
718
801
  // than the (small) risk of noisy logs. Callers opt out globally with
719
802
  // `warnOnUnlimited: false`, per table with `warnOnUnlimited: { users:
@@ -750,6 +833,17 @@ class QueryInterface {
750
833
  this.sqlCacheEnabled = options?.sqlCache !== false && sqlCacheSize !== 0;
751
834
  this.sqlTemplateCache = new utils_js_1.LRUCache(sqlCacheSize !== undefined && sqlCacheSize > 0 ? Math.floor(sqlCacheSize) : 1000);
752
835
  this.dialect = options?.dialect ?? dialect_js_1.postgresDialect;
836
+ this.sqlCommenter =
837
+ options?._sqlCommenter ??
838
+ (0, sql_comment_js_1.resolveSqlComment)(options?.sqlComment, this.logger, {
839
+ dialect: this.dialect.name,
840
+ preparesEveryStatement: this.dialect.capabilities.preparesEveryStatement,
841
+ buildsSql: true,
842
+ });
843
+ if (options?.relationLoadStrategy !== undefined &&
844
+ !RELATION_LOAD_STRATEGIES.includes(options.relationLoadStrategy)) {
845
+ throw new errors_js_1.ValidationError(relationLoadStrategyMessage(options.relationLoadStrategy));
846
+ }
753
847
  this.relationLoadStrategy = options?.relationLoadStrategy ?? 'auto';
754
848
  this.stableRelationOrder = options?.stableRelationOrder === true;
755
849
  this.implicitPkOrdering = options?.implicitPkOrdering === true;
@@ -816,6 +910,8 @@ class QueryInterface {
816
910
  // per `build*` call, so it is exposed as a LIVE getter (not a copied value).
817
911
  const self = this;
818
912
  this.ctx = {
913
+ diagnostics: this.diagnostics,
914
+ logger: this.logger,
819
915
  dialect: this.dialect,
820
916
  table: this.table,
821
917
  schema: this.schema,
@@ -882,7 +978,7 @@ class QueryInterface {
882
978
  };
883
979
  }
884
980
  /**
885
- * Dev-only, once per table: the columns whose database type is absent from
981
+ * Advice (`diagnostics.advice`), once per table: the columns whose database type is absent from
886
982
  * BOTH the column entry and the table-level type maps, the residual case
887
983
  * after the two-source resolution above.
888
984
  *
@@ -907,23 +1003,24 @@ class QueryInterface {
907
1003
  * Postgres rejects with `22007 invalid input syntax for type time`.
908
1004
  * - `timestamptz` and every non-temporal type: unaffected.
909
1005
  *
910
- * PostgreSQL only: the UTC bind rewrite is Postgres-gated (see
911
- * `utcDateTimeWrites` in writes.ts), so on the other engines a missing type
912
- * changes nothing about how a `Date` is bound. Suppressed under
913
- * `NODE_ENV=production` like the other dev diagnostics and deduped through
914
- * the shared registry, so a hot table logs one line for the process.
1006
+ * PostgreSQL only: the zone-less `timestamp` rewrite is Postgres-gated (see
1007
+ * `utcDateTimeRewrite` in where.ts), so on the other engines a missing type
1008
+ * costs only the `date` rewrite. Silent when `diagnostics.advice` is off
1009
+ * (the production default) like the other advice, and deduped through the
1010
+ * shared registry, so a hot table logs one line for the process.
915
1011
  *
916
1012
  * Cannot throw on odd metadata: it walks `tableMeta.columns`, the array the
917
1013
  * constructor loop above has already iterated (and that client.ts validates
918
1014
  * as an array), never `dateColumns`, which is a `Set` in every first-party
919
1015
  * metadata path but arrives as a plain object from JSON-round-tripped
920
- * metadata. A dev-only diagnostic that crashes a shape production would serve
1016
+ * metadata. An advisory diagnostic that crashes a shape production would serve
921
1017
  * is worse than the bug it reports.
922
1018
  */
923
1019
  warnUntypedColumns() {
924
- if (process.env.NODE_ENV === 'production')
1020
+ const log = this.advice;
1021
+ if (!log)
925
1022
  return;
926
- if (this.dialect.name !== 'postgresql')
1023
+ if (!this.dialect.capabilities.utcTimestampRewrite)
927
1024
  return;
928
1025
  const untyped = [];
929
1026
  for (const col of this.tableMeta.columns) {
@@ -939,14 +1036,14 @@ class QueryInterface {
939
1036
  const MAX_NAMED = 12;
940
1037
  const named = untyped.slice(0, MAX_NAMED).join(', ');
941
1038
  const rest = untyped.length > MAX_NAMED ? ` (+${untyped.length - MAX_NAMED} more)` : '';
942
- console.warn(`[turbine] table "${this.table}": no database type in metadata for column(s) ${named}${rest} (neither the ` +
1039
+ (0, logger_js_1.logTo)(log, 'warn', `table "${this.table}": no database type in metadata for column(s) ${named}${rest} (neither the ` +
943
1040
  "column entry's `dialectType`/`pgType` nor the table-level `dialectTypes`/`pgTypes` map). Turbine cannot " +
944
1041
  'tell which of them are zone-less, so it skips the UTC bind rewrite and a `Date` goes to the driver ' +
945
1042
  "as-is: right for `timestamptz`, but a zone-less `date`/`timestamp` then stores the PROCESS's local " +
946
1043
  'calendar fields rather than UTC (silently, since the read path shifts back by the same offset), and a ' +
947
1044
  '`time`/`timetz` column rejects the value outright (`22007 invalid input syntax for type time`). ' +
948
1045
  'Columns whose type IS resolved, every `timestamptz` among them, are unaffected. Fix: regenerate with ' +
949
- '`npx turbine generate`, or set the column types in `defineSchema`.');
1046
+ '`npx turbine generate`, or set the column types in `defineSchema`.', { code: 'untyped-columns', table: this.table, columns: untyped });
950
1047
  }
951
1048
  /** Quote an identifier through the active SQL dialect. */
952
1049
  q(name) {
@@ -1113,24 +1210,63 @@ class QueryInterface {
1113
1210
  * query/relation-names.ts.
1114
1211
  */
1115
1212
  withDeclaredRelationNames(args) {
1116
- if (!args?.with)
1213
+ const withClause = args?.with;
1214
+ if (!withClause)
1215
+ return args;
1216
+ const normalized = (0, relation_names_js_1.normalizeWithClause)(this.schema, this.table, withClause);
1217
+ return normalized === withClause ? args : { ...args, with: normalized };
1218
+ }
1219
+ /**
1220
+ * `args` with `take` / `skip` folded into `limit` / `offset` at every `with`
1221
+ * level (see `normalizeWithPagination` in query/relation-names.ts). Runs after
1222
+ * the relation-name pass, so it resolves the declared spelling.
1223
+ */
1224
+ withRelationPagination(args) {
1225
+ const withClause = args?.with;
1226
+ if (!withClause)
1117
1227
  return args;
1118
- const normalized = (0, relation_names_js_1.normalizeWithClause)(this.schema, this.table, args.with);
1119
- return normalized === args.with ? args : { ...args, with: normalized };
1228
+ const normalized = (0, relation_names_js_1.normalizeWithPagination)(this.schema, this.table, withClause);
1229
+ return normalized === withClause ? args : { ...args, with: normalized };
1120
1230
  }
1121
1231
  /**
1122
- * Caller args in canonical form: declared relation spellings, and the Prisma
1123
- * pagination aliases folded into `limit` / `offset`.
1232
+ * Caller args in canonical form: `undefined` pruned (query/normalize-args.ts),
1233
+ * declared relation spellings, and the Prisma pagination aliases folded into
1234
+ * `limit` / `offset`, with `take` / `skip` folded inside every `with` level too.
1124
1235
  *
1125
- * ONE method rather than two calls at each seam, because the two
1126
- * normalizations have the same requirement and the same failure mode: both
1127
- * must happen before `withFingerprint` / the cache key, and a seam that
1128
- * applies one but not the other is a seam where the alias survives into a
1129
- * fingerprint. Both return their input by reference when there was nothing to
1130
- * change, so the common path still allocates nothing.
1236
+ * Called at the seam of EVERY public operation and its `build*` twin, so the
1237
+ * guards, the fingerprint, the build and the collect all read one shape. Each
1238
+ * step returns its input by reference when there was nothing to change, so a
1239
+ * second call on already-normalized args is free and the common path
1240
+ * allocates nothing.
1241
+ *
1242
+ * `refuseEmptiedWhere` names the single-row read (`findFirst`,
1243
+ * `findFirstOrThrow`) that refuses a `where` whose every key was `undefined`.
1244
+ */
1245
+ normalizeArgs(args, refuseEmptiedWhere) {
1246
+ const pruned = (0, normalize_args_js_1.pruneUndefinedArgs)(this.schema, this.table, args);
1247
+ if (pruned !== args) {
1248
+ if (refuseEmptiedWhere)
1249
+ (0, normalize_args_js_1.assertWhereNotEmptiedByPruning)(this.table, refuseEmptiedWhere, args, pruned);
1250
+ (0, normalize_args_js_1.assertCursorNotEmptied)(this.table, args, pruned);
1251
+ }
1252
+ if (pruned)
1253
+ this.assertQueryOptions(pruned);
1254
+ return (0, utils_js_1.normalizePagination)(this.withRelationPagination(this.withDeclaredRelationNames(pruned)));
1255
+ }
1256
+ /**
1257
+ * Validate the per-query options whose bad values used to be ignored or
1258
+ * misread: `timeout` (Node clamps a non-positive, non-finite or oversized
1259
+ * delay to 1 ms) and `relationLoadStrategy`.
1131
1260
  */
1132
- normalizeArgs(args) {
1133
- return (0, utils_js_1.normalizePagination)(this.withDeclaredRelationNames(args));
1261
+ assertQueryOptions(args) {
1262
+ const { timeout, relationLoadStrategy } = args;
1263
+ if (timeout !== undefined && !isValidTimeout(timeout)) {
1264
+ throw new errors_js_1.ValidationError(`\`timeout\` on ${this.table} must be a positive number of milliseconds up to ${MAX_TIMEOUT_MS}, or 0 for ` +
1265
+ `no timeout (received ${describeOptionValue(timeout)}). Leave it out for no timeout.`);
1266
+ }
1267
+ if (relationLoadStrategy !== undefined && !RELATION_LOAD_STRATEGIES.includes(relationLoadStrategy)) {
1268
+ throw new errors_js_1.ValidationError(relationLoadStrategyMessage(relationLoadStrategy));
1269
+ }
1134
1270
  }
1135
1271
  /**
1136
1272
  * Refuse a `findUnique` whose `where` names no unique key. The rule and the
@@ -1140,6 +1276,30 @@ class QueryInterface {
1140
1276
  assertFindUniqueKey(where) {
1141
1277
  (0, compound_unique_js_1.assertWhereIdentifiesOneRow)(this.tableMeta, this.table, where);
1142
1278
  }
1279
+ /**
1280
+ * Refuse a locking read that is not inside a transaction, at the execute
1281
+ * entry of each find method and before any strategy runs. The `build*`
1282
+ * methods accept `lock` (the SQL is right wherever it runs); executing it
1283
+ * in autocommit releases the lock before the caller sees the rows.
1284
+ *
1285
+ * The value is validated first, so an engine with no row locks answers
1286
+ * E017 rather than asking for a transaction it would also refuse.
1287
+ * "Inside a transaction" is `_txScoped`: the accessors a TransactionClient
1288
+ * hands out, at any nesting depth.
1289
+ */
1290
+ assertLockInTransaction(args, operation) {
1291
+ if (args?.lock === undefined)
1292
+ return;
1293
+ (0, row_lock_js_1.resolveRowLock)(args.lock, this.dialect, this.table);
1294
+ if (!this.txScoped)
1295
+ throw new errors_js_1.ValidationError((0, row_lock_js_1.lockOutsideTransactionMessage)(this.table, operation));
1296
+ }
1297
+ /** Refuse `lock` on a streaming read (a cursor is not a locking read). */
1298
+ assertNoStreamLock(args, operation) {
1299
+ if (args?.lock !== undefined) {
1300
+ throw new errors_js_1.ValidationError((0, row_lock_js_1.lockOnStreamMessage)(this.table, operation));
1301
+ }
1302
+ }
1143
1303
  /**
1144
1304
  * Fill a PK-ascending `orderBy` into every to-many `with` relation that has no
1145
1305
  * explicit one, recursing into nested `with`. Returns a CLONED clause (user
@@ -1172,146 +1332,325 @@ class QueryInterface {
1172
1332
  const fields = pk.map((c) => this.tableMeta.reverseColumnMap[c] ?? c);
1173
1333
  return fields.length === 1 ? { [fields[0]]: 'asc' } : fields.map((f) => ({ [f]: 'asc' }));
1174
1334
  }
1335
+ /** Memo for {@link totalOrderKeys}; table metadata is immutable. */
1336
+ totalOrderKeysMemo;
1175
1337
  /**
1176
- * The field names a `cursor` actually seeks on (its own keys with a defined
1177
- * value), in canonical sorted order. This is the diagnostic / implicit-
1178
- * ordering view; the seek itself orders the fields by their `orderBy`
1179
- * precedence (see {@link cursorSeekEntries}). Empty for a missing cursor or
1180
- * one whose every value is `undefined` (which emits no seek condition at
1181
- * all, so it does not paginate).
1338
+ * The unique keys whose columns are all NOT NULL, primary key first: the
1339
+ * column sets that make an order total (no two rows can tie on them).
1182
1340
  */
1183
- cursorFields(cursor) {
1184
- if (!cursor || typeof cursor !== 'object')
1341
+ totalOrderKeys() {
1342
+ if (this.totalOrderKeysMemo)
1343
+ return this.totalOrderKeysMemo;
1344
+ const meta = this.tableMeta;
1345
+ const nullable = (column) => !meta.primaryKey.includes(column) && meta.columns.find((c) => c.name === column)?.nullable === true;
1346
+ this.totalOrderKeysMemo = (0, compound_unique_js_1.uniqueKeyNames)(meta).filter((cols) => cols.every((c) => !nullable(c)));
1347
+ return this.totalOrderKeysMemo;
1348
+ }
1349
+ /**
1350
+ * THE tiebreaker rule, shared by cursor pages ({@link planCursor}) and the
1351
+ * pages before them ({@link completePageOrder}), so every page of one walk
1352
+ * is cut from the same total order: the columns to append to an order over
1353
+ * `ordered` so that no two rows tie. `[]` when the order already contains a
1354
+ * unique NOT NULL key; the first such key's missing columns otherwise;
1355
+ * `undefined` when the table has none.
1356
+ */
1357
+ tiebreakColumns(ordered) {
1358
+ const keys = this.totalOrderKeys();
1359
+ if (keys.some((cols) => cols.every((c) => ordered.has(c))))
1185
1360
  return [];
1186
- return Object.entries(cursor)
1187
- .filter(([, v]) => v !== undefined)
1188
- .map(([k]) => k)
1189
- .sort();
1361
+ return keys[0]?.filter((c) => !ordered.has(c));
1190
1362
  }
1191
1363
  /**
1192
- * The ascending ordering implied by a `cursor`, or `undefined` when the shape
1193
- * is too ambiguous to order safely.
1194
- *
1195
- * A cursor seek compares against the cursor's own field(s) (`col > $n`, `<`
1196
- * when the orderBy says desc), so the ONLY ordering coherent with it is on
1197
- * those fields: ordering a seek on column X by column Y walks the table in an
1198
- * order the seek does not follow, which skips and repeats rows just as badly
1199
- * as no order at all. That is why this orders on the cursor field rather than
1200
- * blindly on the primary key when the two differ.
1364
+ * A paginated `findMany` (`limit` / `offset`) whose `orderBy` can tie gets
1365
+ * the tiebreaker appended, in the last term's direction, so a page boundary
1366
+ * cannot fall between tied rows in a different order on each page (skipping
1367
+ * or repeating one), and the first page of a cursor walk uses the order its
1368
+ * cursor pages use. Returns `args` itself when the order is already total,
1369
+ * the query is not paginated, or it pages by `cursor` or `distinct`, which
1370
+ * order themselves. A `findFirst` with no `offset` is one row, not a page,
1371
+ * and is left alone.
1201
1372
  *
1202
- * Returns `undefined` (warn, inject nothing) for two shapes:
1203
- * - a MULTI-field cursor. The seek is a proper keyset predicate, but its
1204
- * column PRECEDENCE comes from the `orderBy` ({@link cursorSeekEntries}),
1205
- * and with no orderBy there is nothing to derive it from: the cursor
1206
- * object's key order is canonicalized away, so `(a asc, b asc)` would be a
1207
- * guess at which key the caller meant to lead. The caller must state the
1208
- * order, and the unordered-page warning says so.
1209
- * - a field that does not resolve to a real column. Column validation belongs
1210
- * to the normal build path, which raises a precise error; synthesizing an
1211
- * ORDER BY on it here would only change which error the caller sees.
1373
+ * An order containing a vector distance gets no tiebreaker: a KNN index
1374
+ * serves only `ORDER BY <distance> LIMIT k`, a compound order turns that
1375
+ * into a scan and sort of the whole table, and two equal distances are
1376
+ * rare. A `cursor` still refuses such an order (see {@link planCursor}).
1212
1377
  */
1213
- cursorOrderBy(cursor) {
1214
- const fields = this.cursorFields(cursor);
1215
- if (fields.length !== 1)
1216
- return undefined;
1217
- const field = fields[0];
1218
- try {
1219
- this.toColumn(field);
1220
- }
1221
- catch {
1222
- return undefined;
1378
+ completePageOrder(args) {
1379
+ if (args.orderBy === undefined || (args.limit === undefined && args.offset === undefined))
1380
+ return args;
1381
+ if ((0, normalize_args_js_1.hasCursor)(args.cursor) || (args.distinct && args.distinct.length > 0))
1382
+ return args;
1383
+ if (args[FIRST_ROW_ONLY])
1384
+ return args;
1385
+ const entries = (0, filters_js_1.orderByEntries)(args.orderBy).filter(([, d]) => d !== undefined);
1386
+ if (entries.some(([, d]) => (0, filters_js_1.isVectorOrderBy)(d)))
1387
+ return args;
1388
+ const ordered = new Set();
1389
+ let lastDesc = false;
1390
+ for (const [key, dir] of entries) {
1391
+ lastDesc = false;
1392
+ if ((0, filters_js_1.isVectorOrderBy)(dir) || (0, filters_js_1.isJsonPathOrderBy)(dir) || this.isRelationOrderByValue(dir))
1393
+ continue;
1394
+ const column = (0, utils_js_1.resolveColumnName)(this.tableMeta, key);
1395
+ if (column === undefined)
1396
+ continue;
1397
+ ordered.add(column);
1398
+ lastDesc = (0, filters_js_1.normalizeOrderBy)(dir).dir === 'DESC';
1223
1399
  }
1224
- return { [field]: 'asc' };
1400
+ const missing = this.tiebreakColumns(ordered);
1401
+ if (!missing || missing.length === 0)
1402
+ return args;
1403
+ const dir = lastDesc ? 'desc' : 'asc';
1404
+ const field = (column) => this.tableMeta.reverseColumnMap[column] ?? column;
1405
+ return {
1406
+ ...args,
1407
+ orderBy: [...entries.map(([k, v]) => ({ [k]: v })), ...missing.map((c) => ({ [field(c)]: dir }))],
1408
+ };
1225
1409
  }
1226
1410
  /**
1227
- * The fields a `cursor` seeks on, in KEYSET order with their seek direction.
1228
- * THE single authority the cache fingerprint (`cur=`), the SQL build and the
1229
- * cache-hit param collect all consume, so the three cannot disagree about
1230
- * which value binds to which `$n`.
1411
+ * Plan a `cursor` page: THE single authority the cache key, the SQL build and
1412
+ * the cache-hit param collect all read, so the three cannot disagree.
1231
1413
  *
1232
- * Keyset order is the `orderBy` precedence: the predicate for
1233
- * `orderBy: [{ viewCount }, { id }]` must test `view_count` first and `id`
1234
- * only within equal `view_count`, whatever order the caller wrote the cursor
1235
- * object in (its keys are canonicalized for the cache anyway). A cursor field
1236
- * the orderBy does not name trails the named ones in sorted key order and
1237
- * seeks ascending, the same default a single-field cursor with no orderBy has
1238
- * always had.
1414
+ * The page is ordered by the EFFECTIVE order: the caller's `orderBy` plus a
1415
+ * tiebreaker (the primary key, else a unique key on NOT NULL columns) when
1416
+ * the orderBy does not already end in one, in the last term's direction. A
1417
+ * cursor query is always ordered, because a seek over rows the engine may
1418
+ * return in any order skips and repeats rows. `args.orderBy` is rewritten to
1419
+ * that order (by reference when nothing changes).
1239
1420
  *
1240
- * Directions are indexed by the RESOLVED COLUMN, never the caller's key: both
1241
- * `cursor` and `orderBy` take either spelling, so a cursor written
1242
- * `{ created_at }` against `orderBy: { createdAt: 'desc' }` used to miss the
1243
- * lookup, default to ascending, and seek the wrong side of the page. The
1244
- * `{ sort, nulls }` spec form is normalized for the same reason. A relation /
1245
- * JSON-path / vector orderBy key resolves to no column and is skipped.
1421
+ * Two ways to place the page, picked from what the cursor carries:
1422
+ * - VALUES: the cursor has a value for every column of the effective order,
1423
+ * so the seek compares against them directly (`col > $1`, expanded to a
1424
+ * keyset across several columns). Extra cursor fields are ignored.
1425
+ * - ROW: otherwise the cursor must identify one row by a unique key (the
1426
+ * findUnique rule), and the seek reads that row's ordered values in
1427
+ * scalar subqueries, guarded by `EXISTS` so a deleted cursor row gives an
1428
+ * empty page rather than a wrong one.
1429
+ * Anything else is refused (E003) naming the missing fields.
1246
1430
  *
1247
- * An unknown cursor field throws the same E003 here that the build path
1248
- * threw before this helper existed; the fingerprint simply meets it first.
1431
+ * NULL sort keys are placed where the engine sorts them (PostgreSQL: last
1432
+ * ascending, first descending; SQLite / MySQL / SQL Server: NULL is smallest)
1433
+ * or where an explicit `nulls` puts them, and each row is visited exactly
1434
+ * once. On PostgreSQL a nullable ordered column has its placement written
1435
+ * out, so a Postgres-compatible engine with a different default reads the
1436
+ * same order the seek assumes.
1249
1437
  */
1250
- cursorSeekEntries(cursor, orderBy) {
1251
- const orderPos = new Map();
1252
- const orderDesc = new Map();
1253
- for (const [ok, od] of (0, filters_js_1.orderByEntries)(orderBy)) {
1254
- const ocol = (0, utils_js_1.resolveColumnName)(this.tableMeta, ok);
1255
- if (ocol === undefined)
1438
+ planCursor(args) {
1439
+ const raw = args.cursor;
1440
+ if (!(0, normalize_args_js_1.hasCursor)(raw))
1441
+ return { args, plan: undefined };
1442
+ const meta = this.tableMeta;
1443
+ const field = (column) => meta.reverseColumnMap[column] ?? column;
1444
+ const nullableColumn = (column) => !meta.primaryKey.includes(column) && meta.columns.find((c) => c.name === column)?.nullable === true;
1445
+ // The cursor's own values, by resolved column (either spelling, or a
1446
+ // compound-unique selector, is accepted). Each is a row's value, checked
1447
+ // by the rule every engine shares (cursorFieldValue).
1448
+ const flat = (0, compound_unique_js_1.expandCompoundUniqueWhere)(meta, raw);
1449
+ const cursorValues = new Map();
1450
+ for (const [key, written] of (0, filters_js_1.sortedEntries)(flat)) {
1451
+ if (written === undefined)
1256
1452
  continue;
1257
- if (!orderPos.has(ocol))
1258
- orderPos.set(ocol, orderPos.size);
1259
- // Last wins, matching object-key semantics (duplicates are refused
1260
- // upstream anyway, so in practice there is exactly one).
1261
- orderDesc.set(ocol, (0, filters_js_1.isOrderBySpec)(od) ? od.sort === 'desc' : od === 'desc');
1262
- }
1263
- const entries = (0, filters_js_1.sortedEntries)(cursor)
1264
- .filter(([, v]) => v !== undefined)
1265
- .map(([k, value]) => {
1266
- const column = this.toColumn(k);
1267
- return { column, value, desc: orderDesc.get(column) === true };
1268
- });
1269
- // Stable sort over the canonical (sorted-key) list: orderBy precedence
1270
- // first, unnamed fields after it in their sorted order.
1271
- return entries.sort((a, b) => (orderPos.get(a.column) ?? Number.MAX_SAFE_INTEGER) - (orderPos.get(b.column) ?? Number.MAX_SAFE_INTEGER));
1453
+ const column = this.toColumn(key);
1454
+ const value = (0, filters_js_1.cursorFieldValue)(key, written, {
1455
+ column,
1456
+ table: this.table,
1457
+ kind: (0, filters_js_1.operandColumnKind)(this.getColumnPgType(column)),
1458
+ nullable: nullableColumn(column),
1459
+ });
1460
+ cursorValues.set(column, value);
1461
+ }
1462
+ if (cursorValues.size === 0)
1463
+ return { args, plan: undefined };
1464
+ // The caller's order, plain columns only: a relation, JSON-path or vector
1465
+ // ordering has no value a seek can compare against.
1466
+ const terms = [];
1467
+ const orderEntries = (0, filters_js_1.orderByEntries)(args.orderBy).filter(([, d]) => d !== undefined);
1468
+ for (const [key, dir] of orderEntries) {
1469
+ if ((0, filters_js_1.isVectorOrderBy)(dir) || (0, filters_js_1.isJsonPathOrderBy)(dir) || this.isRelationOrderByValue(dir)) {
1470
+ throw new errors_js_1.ValidationError(`findMany on "${this.table}" refused: \`cursor\` cannot page past the orderBy entry "${key}", which ` +
1471
+ 'orders by a relation, a JSON path or a vector distance rather than a column. Order the cursor ' +
1472
+ 'query by columns only, or page it with `offset`.');
1473
+ }
1474
+ const column = this.toColumn(key);
1475
+ if (terms.some((t) => t.column === column))
1476
+ continue;
1477
+ const { dir: d, nulls } = (0, filters_js_1.normalizeOrderBy)(dir);
1478
+ terms.push({ column, key, desc: d === 'DESC', nulls, nullable: nullableColumn(column) });
1479
+ }
1480
+ // Tiebreaker, the same one a first page gets (see tiebreakColumns).
1481
+ const tiebreak = this.tiebreakColumns(new Set(terms.map((t) => t.column)));
1482
+ if (!tiebreak) {
1483
+ throw new errors_js_1.ValidationError(`findMany on "${this.table}" refused: a \`cursor\` page needs an order with no ties, and "${this.table}" ` +
1484
+ 'has no primary key or unique key on NOT NULL columns to break them. Add a primary key, or page ' +
1485
+ 'this table with `offset`.');
1486
+ }
1487
+ const desc = terms.length > 0 ? terms[terms.length - 1].desc : false;
1488
+ for (const column of tiebreak) {
1489
+ terms.push({ column, key: field(column), desc, nulls: undefined, nullable: false });
1490
+ }
1491
+ const appended = tiebreak.length > 0;
1492
+ // Where NULLs sort, per term. Where the engine sorts them high (PostgreSQL)
1493
+ // a nullable column's placement is written out, so a Postgres-compatible
1494
+ // engine with a different default reads the order the seek assumes.
1495
+ const { nullsSortHigh, nullsOrderingClause } = this.dialect.capabilities;
1496
+ const pinNulls = nullsSortHigh && nullsOrderingClause;
1497
+ let pinned = false;
1498
+ for (const t of terms) {
1499
+ if (t.nulls === undefined && t.nullable && pinNulls) {
1500
+ t.nulls = t.desc ? 'first' : 'last';
1501
+ t.pin = true;
1502
+ pinned = true;
1503
+ }
1504
+ }
1505
+ const nullsFirst = (t) => t.nulls !== undefined ? t.nulls === 'first' : nullsSortHigh ? t.desc : !t.desc;
1506
+ let nextArgs = args;
1507
+ if (appended || pinned) {
1508
+ const orderBy = terms.map((t) => {
1509
+ const dir = t.desc ? 'desc' : 'asc';
1510
+ const original = orderEntries.find(([k]) => k === t.key)?.[1];
1511
+ return { [t.key]: t.pin ? { sort: dir, nulls: t.nulls } : (original ?? dir) };
1512
+ });
1513
+ nextArgs = { ...args, orderBy };
1514
+ }
1515
+ const inclusive = (0, normalize_args_js_1.isCursorInclusive)(args);
1516
+ const coerce = (column, value) => whereMod.coerceWhereOperand(this.ctx, meta, column, value);
1517
+ const keysetTerms = terms.map((t) => ({
1518
+ column: t.column,
1519
+ desc: t.desc,
1520
+ nullsFirst: nullsFirst(t),
1521
+ nullable: t.nullable,
1522
+ }));
1523
+ // An inclusive cursor (prisma-compat) names a ROW, as Prisma's does, so it
1524
+ // always reads that row: a deleted cursor row gives an empty page, never a
1525
+ // page placed where the row used to be.
1526
+ const namesRow = inclusive && (0, compound_unique_js_1.whereIdentifiesOneRow)(meta, flat);
1527
+ if (!namesRow && terms.every((t) => cursorValues.has(t.column))) {
1528
+ const values = terms.map((t) => {
1529
+ const v = cursorValues.get(t.column);
1530
+ return v === null ? null : coerce(t.column, v);
1531
+ });
1532
+ const fp = `v:${keysetTerms.map((t, i) => `${t.column}:${t.desc ? 'd' : 'a'}${t.nullable ? (t.nullsFirst ? 'f' : 'l') : ''}${values[i] === null ? '0' : ''}`).join(',')}` +
1533
+ (inclusive ? ':i' : '');
1534
+ return {
1535
+ args: nextArgs,
1536
+ plan: { mode: 'values', terms: keysetTerms, values, lookup: [], inclusive, fingerprint: fp },
1537
+ };
1538
+ }
1539
+ if (!(0, compound_unique_js_1.whereIdentifiesOneRow)(meta, flat)) {
1540
+ const missing = terms.filter((t) => !cursorValues.has(t.column)).map((t) => `"${field(t.column)}"`);
1541
+ const order = terms.map((t) => `${field(t.column)} ${t.desc ? 'desc' : 'asc'}`).join(', ');
1542
+ const keys = (0, compound_unique_js_1.uniqueKeyNames)(meta).map((cols) => cols.length === 1 ? `{ ${field(cols[0])} }` : `{ ${cols.map(field).join(', ')} }`);
1543
+ throw new errors_js_1.ValidationError(`findMany on "${this.table}" refused: the \`cursor\` has no value for ${missing.join(', ')}, which the ` +
1544
+ `page order (${order}) needs, and it does not name one row by a unique key. Give the cursor a value ` +
1545
+ `for every ordered field, or name the cursor row by ${keys.length > 0 ? keys.join(' or ') : 'a unique key'}.`);
1546
+ }
1547
+ const lookup = [...cursorValues].map(([column, v]) => ({ column, value: v === null ? null : coerce(column, v) }));
1548
+ const fp = `r:${keysetTerms.map((t) => `${t.column}:${t.desc ? 'd' : 'a'}${t.nullable ? (t.nullsFirst ? 'f' : 'l') : ''}`).join(',')}` +
1549
+ `|k=${lookup.map((l) => `${l.column}${l.value === null ? '0' : ''}`).join(',')}` +
1550
+ (inclusive ? ':i' : '');
1551
+ return {
1552
+ args: nextArgs,
1553
+ plan: { mode: 'row', terms: keysetTerms, values: [], lookup, inclusive, fingerprint: fp },
1554
+ };
1272
1555
  }
1273
1556
  /**
1274
- * The keyset predicate for cursor terms in keyset order.
1275
- *
1276
- * One field is the plain `col > $1` (byte-identical to every single-field
1277
- * cursor ever emitted). Two or more expand to
1278
- * `(c1 > $1 OR (c1 = $1 AND c2 > $2) OR (c1 = $1 AND c2 = $2 AND c3 > $3))`,
1279
- * with `<` for a `desc` field: the row-value form `(c1, c2) > ($1, $2)` is
1280
- * shorter, but it cannot express a MIXED direction at all and SQL Server has
1281
- * no row-value comparison, so the expanded form is the one path every engine
1282
- * and every direction set share. Each value is bound ONCE and its `$n` is
1283
- * referenced by every branch that needs it (legal on every supported
1284
- * placeholder syntax), which is what keeps the param count equal to the field
1285
- * count on both the build and the collect side.
1557
+ * Render a {@link CursorPlan}'s seek predicate, pushing its params. Run by
1558
+ * the build AND by the cache-hit collect (which discards the SQL), so the two
1559
+ * cannot bind different params.
1286
1560
  *
1287
- * The former `c1 > $1 AND c2 > $2` was a conjunction, not a seek: it skipped
1288
- * every row whose leading key EQUALED the cursor's, so a table with few
1289
- * distinct leading values lost most of its rows across a page walk, silently.
1561
+ * Each value is bound once and referenced by every branch that needs it. One
1562
+ * non-null column is the plain `col > $1`; several expand to
1563
+ * `(c1 > $1 OR (c1 = $1 AND c2 > $2) OR ...)`, with `<` for a descending
1564
+ * column (a row-value comparison cannot mix directions, and SQL Server has
1565
+ * none). A nullable column gets NULL-aware equality and "after" terms.
1290
1566
  */
1291
- keysetPredicate(terms) {
1292
- const compare = (t) => `${t.column} ${t.desc ? '<' : '>'} ${t.placeholder}`;
1293
- if (terms.length === 1)
1294
- return compare(terms[0]);
1295
- const branches = terms.map((term, i) => {
1567
+ renderCursorPredicate(plan, params) {
1568
+ const qt = this.q(this.table);
1569
+ const col = (t) => `${qt}.${this.q(t.column)}`;
1570
+ let ref;
1571
+ let guard = '';
1572
+ if (plan.mode === 'values') {
1573
+ const refs = plan.values.map((v) => {
1574
+ if (v === null)
1575
+ return null;
1576
+ params.push(v);
1577
+ return this.p(params.length);
1578
+ });
1579
+ ref = (i) => refs[i] ?? null;
1580
+ }
1581
+ else {
1582
+ const alias = 'cursor_row';
1583
+ const conds = plan.lookup.map((l) => {
1584
+ if (l.value === null)
1585
+ return `${alias}.${this.q(l.column)} IS NULL`;
1586
+ params.push(l.value);
1587
+ return `${alias}.${this.q(l.column)} = ${this.p(params.length)}`;
1588
+ });
1589
+ const gf = whereMod.targetGlobalFilterAlias(this.ctx, this.table, alias, params);
1590
+ if (gf)
1591
+ conds.push(gf);
1592
+ const from = `FROM ${qt} ${alias} WHERE ${conds.join(' AND ')}`;
1593
+ ref = (i) => `(SELECT ${alias}.${this.q(plan.terms[i].column)} ${from})`;
1594
+ guard = `EXISTS (SELECT 1 ${from})`;
1595
+ }
1596
+ // Value mode knows each value; row mode knows only whether the column can
1597
+ // hold NULL, so its comparisons stay NULL-aware for nullable columns.
1598
+ const rowMode = plan.mode === 'row';
1599
+ const eq = (i) => {
1600
+ const t = plan.terms[i];
1601
+ const r = ref(i);
1602
+ if (r === null)
1603
+ return `${col(t)} IS NULL`;
1604
+ if (rowMode && t.nullable)
1605
+ return `(${col(t)} = ${r} OR (${col(t)} IS NULL AND ${r} IS NULL))`;
1606
+ return `${col(t)} = ${r}`;
1607
+ };
1608
+ const after = (i, orEqual) => {
1609
+ const t = plan.terms[i];
1610
+ const r = ref(i);
1611
+ const op = `${t.desc ? '<' : '>'}${orEqual ? '=' : ''}`;
1612
+ if (r === null)
1613
+ return t.nullsFirst ? `${col(t)} IS NOT NULL` : null;
1614
+ if (!t.nullable)
1615
+ return `${col(t)} ${op} ${r}`;
1616
+ if (rowMode) {
1617
+ return t.nullsFirst
1618
+ ? `((${r} IS NULL AND ${col(t)} IS NOT NULL) OR ${col(t)} ${op} ${r})`
1619
+ : `(${r} IS NOT NULL AND (${col(t)} ${op} ${r} OR ${col(t)} IS NULL))`;
1620
+ }
1621
+ return t.nullsFirst ? `${col(t)} ${op} ${r}` : `(${col(t)} ${op} ${r} OR ${col(t)} IS NULL)`;
1622
+ };
1623
+ const n = plan.terms.length;
1624
+ const last = n - 1;
1625
+ // An inclusive page folds "equal on every column" into the last branch when
1626
+ // that column is NOT NULL and its value known non-null (`>=`); otherwise it
1627
+ // is its own branch.
1628
+ const foldEqual = plan.inclusive && !plan.terms[last].nullable && ref(last) !== null;
1629
+ const branches = [];
1630
+ for (let i = 0; i < n; i++) {
1631
+ const a = after(i, foldEqual && i === last);
1632
+ if (a === null)
1633
+ continue;
1296
1634
  if (i === 0)
1297
- return compare(term);
1298
- const prefix = terms.slice(0, i).map((t) => `${t.column} = ${t.placeholder}`);
1299
- return `(${[...prefix, compare(term)].join(' AND ')})`;
1300
- });
1301
- return `(${branches.join(' OR ')})`;
1635
+ branches.push(a);
1636
+ else
1637
+ branches.push(`(${[...plan.terms.slice(0, i).map((_, j) => eq(j)), a].join(' AND ')})`);
1638
+ }
1639
+ if (plan.inclusive && !foldEqual) {
1640
+ branches.push(n === 1 ? eq(0) : `(${plan.terms.map((_, j) => eq(j)).join(' AND ')})`);
1641
+ }
1642
+ const keyset = branches.length === 0 ? '1 = 0' : branches.length === 1 ? branches[0] : `(${branches.join(' OR ')})`;
1643
+ return guard ? `(${guard} AND ${keyset})` : keyset;
1302
1644
  }
1303
1645
  /**
1304
- * Whether a findMany paginates (`limit` / `take` / `offset` / `cursor`) but
1305
- * declares no ordering, which makes the returned page NON-DETERMINISTIC:
1306
- * Postgres is free to return different rows for the same unordered `LIMIT`
1307
- * once the heap changes underneath it, so a row can appear on two pages or on
1308
- * none.
1646
+ * Whether a findMany paginates (`limit` / `take` / `offset`) but declares no
1647
+ * ordering, which makes the returned page NON-DETERMINISTIC: Postgres is
1648
+ * free to return different rows for the same unordered `LIMIT` once the heap
1649
+ * changes underneath it, so a row can appear on two pages or on none.
1309
1650
  *
1310
- * `cursor` counts, and is the worst case rather than an exception: a keyset
1311
- * seek with no ORDER BY is exactly this bug (`WHERE id > $1 LIMIT $2` walks
1312
- * the heap in whatever order the plan happens to produce). An empty orderBy
1313
- * (`[]`, or an object whose every value is `undefined`) counts as absent,
1314
- * because it emits no ordering.
1651
+ * A `cursor` page never counts: it is always ordered by its effective order
1652
+ * (see {@link planCursor}). An empty orderBy (`[]`, or an object whose every
1653
+ * value is `undefined`) counts as absent, because it emits no ordering.
1315
1654
  *
1316
1655
  * `distinct` is still excluded: that path re-orders in an outer wrapper around
1317
1656
  * a `DISTINCT ON` whose ordering picks the representative row, so an implicit
@@ -1322,11 +1661,11 @@ class QueryInterface {
1322
1661
  return false;
1323
1662
  if (args.distinct !== undefined)
1324
1663
  return false;
1664
+ if ((0, normalize_args_js_1.hasCursor)(args.cursor))
1665
+ return false;
1325
1666
  if (!(0, filters_js_1.isEmptyOrderBy)(args.orderBy))
1326
1667
  return false;
1327
- if (args.limit !== undefined || args.offset !== undefined)
1328
- return true;
1329
- return this.cursorFields(args.cursor).length > 0;
1668
+ return args.limit !== undefined || args.offset !== undefined;
1330
1669
  }
1331
1670
  /**
1332
1671
  * Opt-in (`implicitPkOrdering`) primary-key ascending ordering for a paginating
@@ -1334,47 +1673,42 @@ class QueryInterface {
1334
1673
  *
1335
1674
  * OFF by default in CORE, deliberately: turning it on would add an `ORDER BY`
1336
1675
  * to SQL that existing applications already emit, changing both the rows a
1337
- * given page returns and the plan the engine picks. That is a breaking change
1338
- * in everything but the type signature, so it waits for a major. The
1676
+ * given page returns and the plan the engine picks. The
1339
1677
  * `turbine-orm/prisma-compat` layer defaults it ON instead, because reproducing
1340
1678
  * Prisma's semantics is that layer's whole contract.
1341
1679
  *
1342
1680
  * An explicit `orderBy` always wins, a PK-less table is left alone (nothing
1343
1681
  * stable to order by), and a composite PK orders on every column in
1344
- * declaration order. A `cursor` query orders on the CURSOR's field instead
1345
- * (see {@link cursorOrderBy}), and is left alone when that shape is ambiguous.
1682
+ * declaration order. A `cursor` page is ordered by its own plan, not here, and
1346
1683
  * `distinct` shapes are skipped (see {@link isUnorderedPage}). With the flag
1347
- * off this returns `undefined` before touching anything, so the emitted SQL is
1348
- * byte-identical to before.
1684
+ * off this returns `undefined` before touching anything.
1349
1685
  */
1350
1686
  implicitPkOrderBy(args) {
1351
1687
  if (!this.implicitPkOrdering)
1352
1688
  return undefined;
1353
1689
  if (!this.isUnorderedPage(args))
1354
1690
  return undefined;
1355
- if (this.cursorFields(args?.cursor).length > 0)
1356
- return this.cursorOrderBy(args?.cursor);
1357
1691
  return this.pkOrderBy();
1358
1692
  }
1359
1693
  /**
1360
- * Dev-only, once per query shape: an unordered paginating findMany returns a
1694
+ * Advice (`diagnostics.advice`), once per query shape: an unordered paginating findMany returns a
1361
1695
  * non-deterministic page (see {@link isUnorderedPage}), and on real data it is
1362
1696
  * also usually the slower plan (an unordered `LIMIT` can discard tens of
1363
1697
  * thousands of heap rows that an index scan on the key would have skipped).
1364
1698
  *
1365
- * Gated exactly like the other dev diagnostics (silent under
1366
- * `NODE_ENV=production`) and consistent with `warnOnUnlimited`: a per-call
1699
+ * Gated exactly like the other advice (silent when `diagnostics.advice` is
1700
+ * off) and consistent with `warnOnUnlimited`: a per-call
1367
1701
  * `warnOnUnlimited: false` silences it, `true` forces it past a config-level
1368
1702
  * opt-out, and a config/per-table `warnOnUnlimited: false` silences it. Deduped
1369
1703
  * process-wide through the shared warn registry, so it can never spam.
1370
1704
  *
1371
1705
  * Suppressed only when `implicitPkOrdering` will ACTUALLY order this query.
1372
- * The flag being on is not enough: a PK-less table and an ambiguous
1373
- * multi-field cursor both get no injected ordering, and those are precisely
1374
- * the shapes that still need saying out loud.
1706
+ * The flag being on is not enough: a PK-less table gets no injected
1707
+ * ordering, and that is precisely the shape that still needs saying out loud.
1375
1708
  */
1376
1709
  maybeWarnUnorderedPage(args) {
1377
- if (process.env.NODE_ENV === 'production')
1710
+ const log = this.advice;
1711
+ if (!log)
1378
1712
  return;
1379
1713
  if (this.implicitPkOrdering && this.implicitPkOrderBy(args) !== undefined)
1380
1714
  return;
@@ -1385,30 +1719,18 @@ class QueryInterface {
1385
1719
  return;
1386
1720
  if (!this.isUnorderedPage(args))
1387
1721
  return;
1388
- const cursorFields = this.cursorFields(args?.cursor);
1389
- const shape = [
1390
- cursorFields.length > 0 ? 'cursor' : '',
1391
- args?.limit !== undefined ? 'limit' : '',
1392
- args?.offset !== undefined ? 'offset' : '',
1393
- ]
1722
+ const shape = [args?.limit !== undefined ? 'limit' : '', args?.offset !== undefined ? 'offset' : '']
1394
1723
  .filter(Boolean)
1395
1724
  .join('+');
1396
1725
  if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.unorderedPage, `${this.table}|${shape}`))
1397
1726
  return;
1398
- const asOrderBy = (fields) => fields.length === 1 ? `{ ${fields[0]}: 'asc' }` : `[${fields.map((f) => `{ ${f}: 'asc' }`).join(', ')}]`;
1399
1727
  const pk = (this.tableMeta.primaryKey ?? []).map((c) => this.tableMeta.reverseColumnMap[c] ?? c);
1400
- // A cursor seeks on its own field, so that is the ordering to recommend:
1401
- // the primary key would be the wrong advice whenever the two differ.
1402
- const orderFields = cursorFields.length > 0 ? cursorFields : pk;
1403
- const suggestion = orderFields.length > 0 ? `Add \`orderBy: ${asOrderBy(orderFields)}\`` : 'Add an `orderBy` on a unique column';
1404
- const seek = cursorFields.length > 0
1405
- ? 'a `cursor` seek with no orderBy compares against rows the engine is free to return in any order, so ' +
1406
- 'paging with it skips and repeats rows: '
1407
- : '';
1408
- console.warn(`[turbine] findMany on "${this.table}" paginates (${shape}) with no orderBy: ${seek}the page is NOT ` +
1728
+ const asOrderBy = pk.length === 1 ? `{ ${pk[0]}: 'asc' }` : `[${pk.map((f) => `{ ${f}: 'asc' }`).join(', ')}]`;
1729
+ const suggestion = pk.length > 0 ? `Add \`orderBy: ${asOrderBy}\`` : 'Add an `orderBy` on a unique column';
1730
+ (0, logger_js_1.logTo)(log, 'warn', `findMany on "${this.table}" paginates (${shape}) with no orderBy: the page is NOT ` +
1409
1731
  'deterministic (the same query can return different rows as the table changes, so a row may appear ' +
1410
1732
  `on two pages or on none). ${suggestion}, or set \`implicitPkOrdering: true\` in the client config ` +
1411
- 'to order paginated queries automatically (by the cursor field, or by the primary key).');
1733
+ 'to order paginated queries by the primary key automatically.', { code: 'unordered-page', table: this.table, operation: 'findMany', paging: shape });
1412
1734
  }
1413
1735
  // -------------------------------------------------------------------------
1414
1736
  // relationLoadStrategy: 'auto', per-relation batched fallback when the
@@ -1635,7 +1957,7 @@ class QueryInterface {
1635
1957
  return split;
1636
1958
  }
1637
1959
  /**
1638
- * Dev-only once-per-relation note that `'auto'` engaged the batched fallback.
1960
+ * Advice, once per relation: `'auto'` engaged the batched fallback.
1639
1961
  *
1640
1962
  * Both lines follow the same four parts: the CONDITION that tripped the rule,
1641
1963
  * the MECHANISM (which plan shape was replaced by which, and what that costs),
@@ -1644,20 +1966,21 @@ class QueryInterface {
1644
1966
  * as a bug, so the mechanism sentence is not optional.
1645
1967
  */
1646
1968
  emitAutoNotes(engaged) {
1647
- if (process.env.NODE_ENV === 'production')
1969
+ const log = this.advice;
1970
+ if (!log)
1648
1971
  return;
1649
1972
  for (const e of engaged) {
1650
1973
  if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.autoStrategy, `${this.table}.${e.relation}`))
1651
1974
  continue;
1652
1975
  if (e.reason === 'to-one-cardinality') {
1653
- console.warn(`[turbine] auto strategy: to-one relation "${e.relation}" on "${this.table}" loads batched ` +
1976
+ (0, logger_js_1.logTo)(log, 'warn', `auto strategy: to-one relation "${e.relation}" on "${this.table}" loads batched ` +
1654
1977
  `(the query is unbounded or its limit exceeds ${this.autoToOneThreshold()} rows). On the join plan ` +
1655
1978
  'a to-one relation is a correlated subquery the engine re-evaluates once per parent row, a per-row ' +
1656
1979
  'cost that stands even on a unique index; batched replaces it with ONE follow-up statement ' +
1657
1980
  '(`key = ANY(...)` for the whole page), so it trades that per-row cost for a single extra round ' +
1658
1981
  'trip. Bound the query with a smaller `limit`, tune `autoToOneJoinMaxRows` (the break-even is ' +
1659
1982
  "round-trip time / per-row cost), or set `relationLoadStrategy: 'join'` to force the " +
1660
- 'single-statement plan.');
1983
+ 'single-statement plan.', { code: 'auto-strategy', table: this.table, relation: e.relation, reason: e.reason });
1661
1984
  continue;
1662
1985
  }
1663
1986
  const probe = e.miss
@@ -1675,9 +1998,9 @@ class QueryInterface {
1675
1998
  : ' On the join plan that relation is a correlated subquery re-evaluated once per parent row, so an ' +
1676
1999
  `unindexed probe is one full scan of "${child}" per parent row; the batched follow-up scans it ` +
1677
2000
  'once for the whole page.';
1678
- console.warn(`[turbine] auto strategy: relation "${e.relation}" on "${this.table}" loads batched (${probe}).${why} ` +
2001
+ (0, logger_js_1.logTo)(log, 'warn', `auto strategy: relation "${e.relation}" on "${this.table}" loads batched (${probe}).${why} ` +
1679
2002
  "Create the covering index (or set `relationLoadStrategy: 'join'` to force the single-statement " +
1680
- 'plan); run `npx turbine doctor` for the exact CREATE INDEX SQL.');
2003
+ 'plan); run `npx turbine doctor` for the exact CREATE INDEX SQL.', { code: 'auto-strategy', table: this.table, relation: e.relation, reason: e.reason });
1681
2004
  }
1682
2005
  }
1683
2006
  /**
@@ -1768,16 +2091,15 @@ class QueryInterface {
1768
2091
  // a closure rather than passing `this.dialect` so the loader keeps
1769
2092
  // needing nothing else from the dialect.
1770
2093
  //
1771
- // The `dialect.name` test is NOT redundant with the hook's presence, and
1772
- // this is the same trap the `distinct` gate documents: every engine
1773
- // dialect is built by SPREADING `postgresDialect`, so an optional hook
1774
- // added there is INHERITED by sqlite / mysql / mssql, whose "absent"
1775
- // fallback would then never be taken. The wrapper is not portable as
1776
- // written either (T-SQL rejects an ORDER BY inside a derived table
1777
- // without TOP/OFFSET, and the compiled child query carries one), so the
1778
- // pushdown stays with the dialect that owns the implementation until
1779
- // another engine ships its own and asserts it.
1780
- buildPartitionLimit: this.dialect.name === 'postgresql' && this.dialect.buildPartitionLimit
2094
+ // The capability is NOT redundant with the hook's presence: every engine
2095
+ // dialect is built by SPREADING `postgresDialect`, so the hook is
2096
+ // INHERITED by sqlite / mysql / mssql, whose "absent" fallback would then
2097
+ // never be taken. The wrapper is not portable as written either (T-SQL
2098
+ // rejects an ORDER BY inside a derived table without TOP/OFFSET, and the
2099
+ // compiled child query carries one), so the pushdown stays with the
2100
+ // dialect that owns the implementation until another engine ships its own
2101
+ // and asserts it.
2102
+ buildPartitionLimit: this.dialect.capabilities.partitionLimitPushdown && this.dialect.buildPartitionLimit
1781
2103
  ? (input) => this.dialect.buildPartitionLimit(input)
1782
2104
  : undefined,
1783
2105
  skipGlobalFilters: skip,
@@ -1916,15 +2238,14 @@ class QueryInterface {
1916
2238
  *
1917
2239
  * When `sqlCache` is disabled, always calls `build()` without caching.
1918
2240
  *
1919
- * `build` receives a fresh `$N` param scratch array. On a miss those params
1920
- * are discarded (the returned params come from each call site's dedicated
1921
- * collect path); the array exists so the build path can number placeholders
1922
- * via `params.length` exactly as it does today. On a HIT, `build` is skipped
1923
- * here but re-run by {@link crossCheckCache} (dev only) with a fresh array to
1924
- * verify the collect path stayed in lockstep with the build path.
2241
+ * `build` receives a fresh `$N` param scratch array. The executed params
2242
+ * always come from each call site's dedicated collect path; on a miss the
2243
+ * scratch params are kept in {@link lastMissParams} so `crossCheckCache` can
2244
+ * compare the two. On a HIT, `build` is skipped here and re-run by
2245
+ * {@link crossCheckCache} only when `diagnostics.cacheCheck` asks for it.
1925
2246
  *
1926
- * Sets {@link lastCacheHit} so the caller's `crossCheckCache` knows whether a
1927
- * cross-check is warranted.
2247
+ * Sets {@link lastCacheHit} so the caller's `crossCheckCache` knows which
2248
+ * comparison applies.
1928
2249
  */
1929
2250
  acquireSql(cacheKey, build) {
1930
2251
  if (!this.sqlCacheEnabled) {
@@ -1935,6 +2256,7 @@ class QueryInterface {
1935
2256
  if (cached) {
1936
2257
  this.cacheHits++;
1937
2258
  this.lastCacheHit = true;
2259
+ this.lastMissParams = undefined;
1938
2260
  return cached;
1939
2261
  }
1940
2262
  this.lastCacheHit = false;
@@ -1964,40 +2286,68 @@ class QueryInterface {
1964
2286
  */
1965
2287
  buildCacheEntry(build) {
1966
2288
  this.variableArityShape = false;
1967
- const sql = build([]);
2289
+ this.lastMissParams = undefined;
2290
+ const built = [];
2291
+ const sql = build(built);
1968
2292
  const variableArity = this.variableArityShape;
1969
2293
  this.variableArityShape = false;
1970
2294
  this.cacheMisses++;
1971
- return { sql, name: variableArity ? '' : (0, utils_js_1.sqlToPreparedName)(sql) };
2295
+ this.lastMissParams = built;
2296
+ // With `sqlComment` putting a comment on every statement, the template's own
2297
+ // name is never sent (the execute seam sends a `c_` name derived from it),
2298
+ // so it is not claimed from the budget: only names that reach the wire do.
2299
+ const name = variableArity
2300
+ ? ''
2301
+ : this.sqlCommenter?.alwaysComments
2302
+ ? (0, utils_js_1.sqlToPreparedName)(sql)
2303
+ : (0, utils_js_1.boundedPreparedName)(sql);
2304
+ return { sql, name, paramCount: built.length };
1972
2305
  }
1973
2306
  /**
1974
- * Dev-mode SQL-cache lockstep cross-check (see {@link cacheCrossCheckEnabled}).
2307
+ * The SQL-cache lockstep checks (see the comment above `CACHE_MISMATCH_WARN_NS`).
1975
2308
  *
1976
- * Runs only when the most recent {@link acquireSql} was a cache HIT and the
1977
- * check is enabled. Rebuilds the SQL + `$N` params fresh via the same `build`
1978
- * closure the caller passed to `acquireSql`, then compares:
2309
+ * On a MISS: the collected params must equal the params the build pushed.
2310
+ * On a HIT: the collected param count must equal the entry's, always; then,
2311
+ * when `diagnostics.cacheCheck` selects this hit, the SQL and params are
2312
+ * rebuilt fresh through the same closure and compared:
1979
2313
  * (a) the cached SQL string byte-for-byte against the fresh SQL, and
1980
- * (b) the params the cache-hit collect path produced against the fresh
1981
- * build-path params (length and element-wise strict deep-equal).
2314
+ * (b) the collected params against the fresh build-path params (length
2315
+ * and element-wise strict deep-equal).
1982
2316
  *
1983
2317
  * A mismatch means the fingerprint / build / collect paths have drifted out
1984
2318
  * of lockstep (the exact class of bug that has silently corrupted results
1985
- * before), so it throws a {@link ValidationError} (E003) naming the
1986
- * fingerprint, the operation, and both SQL strings (truncated). Failing loud
1987
- * in dev/test is the point. Production never reaches the comparison.
2319
+ * before), so it throws an {@link InvariantError} (E020) naming the
2320
+ * fingerprint, the operation, and both SQL strings (truncated), never a
2321
+ * parameter value.
1988
2322
  *
1989
2323
  * @param op human label of the calling build method (for the error message).
1990
- * @param cacheKey the cache fingerprint that HIT.
1991
- * @param entry the cached SQL entry that will be executed.
2324
+ * @param cacheKey the cache fingerprint.
2325
+ * @param entry the SQL entry that will be executed.
1992
2326
  * @param build the same closure passed to `acquireSql`; re-run here to
1993
2327
  * capture the fresh build-path SQL + params.
1994
2328
  * @param collectedParams the params the caller's collect path produced.
1995
2329
  */
1996
2330
  crossCheckCache(op, cacheKey, entry, build, collectedParams) {
1997
- if (!this.lastCacheHit)
2331
+ if (!this.lastCacheHit) {
2332
+ const built = this.lastMissParams;
2333
+ this.lastMissParams = undefined;
2334
+ if (built === undefined || cacheParamsEqual(collectedParams, built))
2335
+ return;
2336
+ throw this.lockstepError(op, cacheKey, [
2337
+ `built params and collected params diverge on a cache miss (collected ${collectedParams.length}, built ${built.length})`,
2338
+ ], false);
2339
+ }
2340
+ if (entry.paramCount !== undefined && collectedParams.length !== entry.paramCount) {
2341
+ throw new errors_js_1.InvariantError('sql-cache-param-count', `SQL cache param count mismatch on ${op} (fingerprint "${cacheKey}"): the cached statement has ` +
2342
+ `${entry.paramCount} placeholders but the cache hit collected ${collectedParams.length} params, so ` +
2343
+ 'the statement was not sent.');
2344
+ }
2345
+ const check = this.diagnostics.cacheCheck;
2346
+ if (check === 'off')
1998
2347
  return;
1999
- const mode = cacheCrossCheckMode();
2000
- if (mode === 'off')
2348
+ const sampled = typeof check === 'object';
2349
+ // The sampling decision, and only it, is per hit.
2350
+ if (sampled && check.sample < 1 && Math.random() >= check.sample)
2001
2351
  return;
2002
2352
  const freshParams = [];
2003
2353
  const freshSql = build(freshParams);
@@ -2013,34 +2363,39 @@ class QueryInterface {
2013
2363
  if (!paramsOk) {
2014
2364
  details.push(`cache-hit params and freshly-built params diverge (collected ${collectedParams.length}, built ${freshParams.length})`);
2015
2365
  }
2016
- const message = `SQL cache lockstep violation on ${op} (fingerprint "${cacheKey}"). ` +
2017
- `This is a Turbine internal invariant violation, please report it at ` +
2018
- `https://github.com/zvndev/turbine-orm/issues. The fingerprint, SQL-build, and ` +
2019
- `param-collect paths must enumerate where-clause keys identically.\n${details.join('\n')}`;
2020
- // Sampled production path: log once per distinct fingerprint so the failure
2021
- // is durably recorded in the server log before the throw unwinds the query.
2022
- // (Dev mode throws straight away; its behavior is unchanged.)
2023
- if (mode === 'sampled' && !loggedCacheMismatchFingerprints.has(cacheKey)) {
2024
- loggedCacheMismatchFingerprints.add(cacheKey);
2025
- console.error(message);
2366
+ throw this.lockstepError(op, cacheKey, details, sampled);
2367
+ }
2368
+ /**
2369
+ * The `sql-cache-lockstep` {@link InvariantError}. When `log` is set (the
2370
+ * sampled hit path) the failure is also logged once per table and shape, so
2371
+ * it is durably recorded in the server log before the throw unwinds the
2372
+ * query; the other paths throw straight away.
2373
+ */
2374
+ lockstepError(op, cacheKey, details, log) {
2375
+ const err = new errors_js_1.InvariantError('sql-cache-lockstep', `SQL cache lockstep violation on ${op} (fingerprint "${cacheKey}"). The fingerprint, SQL-build, and ` +
2376
+ `param-collect paths must enumerate where-clause keys identically. ${errors_js_1.INVARIANT_REPORT_HINT}\n` +
2377
+ details.join('\n'));
2378
+ if (log && (0, warn_registry_js_1.shouldWarnOnce)(CACHE_MISMATCH_WARN_NS, `${this.table}|${cacheKey}`)) {
2379
+ (0, logger_js_1.logTo)(this.logger, 'error', err.message, { code: 'cache-mismatch', table: this.table, operation: op });
2026
2380
  }
2027
- throw new errors_js_1.ValidationError(message);
2381
+ return err;
2028
2382
  }
2029
2383
  /**
2030
- * Reset the per-instance unlimited-query warning dedupe set.
2031
- * Exposed for tests so a single test process can verify the warning fires
2032
- * exactly once per table without bleeding state between assertions.
2384
+ * Reset the unlimited-query warning dedupe, which is process-wide (see
2385
+ * {@link maybeWarnUnlimited}). Exposed for tests so a single test process can
2386
+ * verify the warning fires once per table without bleeding state between
2387
+ * assertions.
2033
2388
  */
2034
2389
  resetUnlimitedWarnings() {
2035
- this.warnedTables.clear();
2390
+ (0, warn_registry_js_1.resetWarnOnce)(warn_registry_js_1.WARN_NS.unlimitedFindMany);
2036
2391
  }
2037
- emitQueryEvent(sql, params, duration, action, rows, error, retried) {
2392
+ emitQueryEvent(out, params, duration, action, rows, error, retried) {
2038
2393
  const onQuery = this.options?._onQuery;
2039
2394
  if (!onQuery)
2040
2395
  return;
2041
2396
  try {
2042
2397
  onQuery({
2043
- sql,
2398
+ sql: out.sql,
2044
2399
  params,
2045
2400
  duration,
2046
2401
  model: this.table,
@@ -2050,12 +2405,28 @@ class QueryInterface {
2050
2405
  error,
2051
2406
  strategy: this.currentStrategyTag,
2052
2407
  ...(retried ? { retried: true } : {}),
2408
+ ...(out.comment === undefined ? {} : { comment: out.comment }),
2053
2409
  });
2054
2410
  }
2055
2411
  catch {
2056
2412
  // Listener errors must never crash a query
2057
2413
  }
2058
2414
  }
2415
+ /**
2416
+ * The statement `sql` as it goes out under `preparedName`: with the
2417
+ * `sqlComment` appended, and under the name that comment calls for (see
2418
+ * query/sql-comment.ts). This is the one place a comment is added, at the
2419
+ * execute seam and after the SQL-template cache, so the cache, its keys and
2420
+ * its lockstep checks only ever see commentless text. Without `sqlComment`
2421
+ * it is the statement unchanged.
2422
+ */
2423
+ outgoing(sql, preparedName, action) {
2424
+ const commenter = this.sqlCommenter;
2425
+ const c = commenter?.commentFor(this.table, action);
2426
+ if (commenter === undefined || c === undefined)
2427
+ return { sql, text: sql, name: preparedName, comment: undefined };
2428
+ return { sql, text: `${sql} ${c.comment}`, name: commenter.preparedName(preparedName, c), comment: c.comment };
2429
+ }
2059
2430
  /**
2060
2431
  * Resolve the prepared-statement name a read should execute under, honouring
2061
2432
  * the per-query {@link FindManyArgs.forceCustomPlan} opt-in.
@@ -2146,13 +2517,31 @@ class QueryInterface {
2146
2517
  }
2147
2518
  /**
2148
2519
  * Execute a pool.query with an optional timeout.
2149
- * If timeout is set, races the query against a timer and rejects on expiry.
2520
+ *
2521
+ * On a node-postgres pool outside a transaction the timeout is enforced on
2522
+ * the SERVER ({@link queryCancellable}). Anywhere else (a transaction's
2523
+ * connection, an engine shim, an HTTP pool) it races the query against a
2524
+ * timer: the call rejects on time but the statement keeps running, so the
2525
+ * error's `outcome` is `'unknown'`.
2526
+ *
2150
2527
  * pg driver errors are translated to typed Turbine errors via wrapPgError.
2151
2528
  * `retryIfStale` is {@link readWithTimeout}'s; nothing else sets it.
2152
2529
  */
2153
- async queryWithTimeout(sql, params, timeout, preparedName, retryIfStale = false) {
2154
- let start = performance.now();
2530
+ async queryWithTimeout(sql, params, requestedTimeout, preparedName, retryIfStale = false, pool = this.pool) {
2155
2531
  const action = this.currentAction;
2532
+ const out = this.outgoing(sql, preparedName, action);
2533
+ // 0 is no timeout, as it is for `$transaction`'s option and the server's statement_timeout.
2534
+ const timeout = requestedTimeout === 0 ? undefined : requestedTimeout;
2535
+ if (timeout !== undefined && pool === this.pool && this.cancelsOnTimeout()) {
2536
+ return this.queryCancellable(out, params, timeout, retryIfStale, action);
2537
+ }
2538
+ if (timeout !== undefined && pool === this.pool && this.txScoped && this.dialect.capabilities.backendCancel) {
2539
+ const state = (0, transaction_state_js_1.transactionStateOf)(this.pool);
2540
+ if (state !== undefined && (0, utils_js_1.canCancelBackend)(state.client)) {
2541
+ return this.queryCancellableInTransaction(out, params, timeout, state, action);
2542
+ }
2543
+ }
2544
+ let start = performance.now();
2156
2545
  // Build the query argument, use object form with `name` for prepared
2157
2546
  // statements, or the plain (text, values) form otherwise.
2158
2547
  //
@@ -2160,19 +2549,19 @@ class QueryInterface {
2160
2549
  // pool shims only speak `(text, values)`), so it is reached through a
2161
2550
  // cast. Guarded at runtime by `preparedStatementsEnabled`, which only the
2162
2551
  // drivers that accept it turn on.
2163
- const usePrepared = preparedName && this.preparedStatementsEnabled;
2552
+ const usePrepared = out.name && this.preparedStatementsEnabled;
2164
2553
  const send = () => usePrepared
2165
- ? this.pool.query({
2166
- name: preparedName,
2167
- text: sql,
2554
+ ? pool.query({
2555
+ name: out.name,
2556
+ text: out.text,
2168
2557
  values: params,
2169
2558
  })
2170
- : this.pool.query(sql, params);
2559
+ : pool.query(out.text, params);
2171
2560
  let exec = send();
2172
2561
  // Set when the caller's timeout has already answered, so a first attempt
2173
2562
  // that fails AFTER that does not send a retry nobody is waiting for.
2174
2563
  let abandoned = false;
2175
- if (retryIfStale && !this.txScoped) {
2564
+ if (retryIfStale && !this.txScoped && pool === this.pool) {
2176
2565
  exec = exec.catch(async (err) => {
2177
2566
  if (abandoned || !(0, errors_js_1.isStaleConnectionError)(err))
2178
2567
  throw err;
@@ -2180,7 +2569,7 @@ class QueryInterface {
2180
2569
  // so a listener counting errors sees it and one alerting on failed
2181
2570
  // CALLS can skip it. The final event below times the retry alone.
2182
2571
  const wrapped = (0, errors_js_1.wrapPgError)(err);
2183
- this.emitQueryEvent(sql, params, performance.now() - start, action, 0, wrapped instanceof Error ? wrapped : undefined, true);
2572
+ this.emitQueryEvent(out, params, performance.now() - start, action, 0, wrapped instanceof Error ? wrapped : undefined, true);
2184
2573
  await (0, connection_guard_js_1.settleEventLoop)();
2185
2574
  if (abandoned)
2186
2575
  throw err;
@@ -2191,12 +2580,12 @@ class QueryInterface {
2191
2580
  if (!timeout) {
2192
2581
  try {
2193
2582
  const result = await exec;
2194
- this.emitQueryEvent(sql, params, performance.now() - start, action, result.rowCount ?? 0);
2583
+ this.emitQueryEvent(out, params, performance.now() - start, action, result.rowCount ?? 0);
2195
2584
  return result;
2196
2585
  }
2197
2586
  catch (err) {
2198
2587
  const wrapped = (0, errors_js_1.wrapPgError)(err);
2199
- this.emitQueryEvent(sql, params, performance.now() - start, action, 0, wrapped instanceof Error ? wrapped : undefined);
2588
+ this.emitQueryEvent(out, params, performance.now() - start, action, 0, wrapped instanceof Error ? wrapped : undefined);
2200
2589
  throw wrapped;
2201
2590
  }
2202
2591
  }
@@ -2204,23 +2593,240 @@ class QueryInterface {
2204
2593
  const timeoutPromise = new Promise((_, reject) => {
2205
2594
  timer = setTimeout(() => {
2206
2595
  abandoned = true;
2207
- reject(new errors_js_1.TimeoutError(timeout));
2596
+ reject(new errors_js_1.TimeoutError(timeout, 'Query', {
2597
+ outcome: 'unknown',
2598
+ message: `Query timed out after ${timeout}ms. It could not be cancelled on the server and may still ` +
2599
+ 'complete, so a write may have been applied: check before retrying it.',
2600
+ }));
2208
2601
  }, timeout);
2209
2602
  });
2210
2603
  try {
2211
2604
  const result = await Promise.race([exec, timeoutPromise]);
2212
- this.emitQueryEvent(sql, params, performance.now() - start, action, result.rowCount ?? 0);
2605
+ this.emitQueryEvent(out, params, performance.now() - start, action, result.rowCount ?? 0);
2213
2606
  return result;
2214
2607
  }
2215
2608
  catch (err) {
2216
2609
  const wrapped = (0, errors_js_1.wrapPgError)(err);
2217
- this.emitQueryEvent(sql, params, performance.now() - start, action, 0, wrapped instanceof Error ? wrapped : undefined);
2610
+ this.emitQueryEvent(out, params, performance.now() - start, action, 0, wrapped instanceof Error ? wrapped : undefined);
2218
2611
  throw wrapped;
2219
2612
  }
2220
2613
  finally {
2221
2614
  clearTimeout(timer);
2222
2615
  }
2223
2616
  }
2617
+ /**
2618
+ * Whether a timed statement can be stopped on the server: an engine with a
2619
+ * protocol cancel (PostgreSQL) on a node-postgres pool, outside a transaction
2620
+ * (inside one, the connection belongs to the transaction and is not this
2621
+ * method's to cancel or destroy).
2622
+ */
2623
+ cancelsOnTimeout() {
2624
+ return !this.txScoped && this.dialect.capabilities.backendCancel && (0, utils_js_1.isNodePostgresPool)(this.pool);
2625
+ }
2626
+ /**
2627
+ * {@link queryWithTimeout} with a timeout the server enforces. The statement
2628
+ * runs on a connection checked out for it, so when the deadline passes the
2629
+ * backend running it is known and gets a protocol cancel request.
2630
+ *
2631
+ * The call then waits, at most {@link CANCEL_GRACE_MS}, for the statement to
2632
+ * answer, and the answer decides the result:
2633
+ * - cancelled (57014): TimeoutError, `outcome: 'not-executed'`;
2634
+ * - finished first: the result, returned late, because reporting a write
2635
+ * that committed as a timeout invites a retry that applies it twice;
2636
+ * - failed: that error;
2637
+ * - no answer: TimeoutError, `outcome: 'unknown'`, and the connection is
2638
+ * destroyed rather than handed to the next caller still busy.
2639
+ * A deadline that passes while still waiting for a connection sends nothing
2640
+ * (`'not-executed'`). Either way the connection goes back to the pool only
2641
+ * once its backend is idle, so timed-out statements cannot hold the pool.
2642
+ */
2643
+ async queryCancellable(out, params, timeout, retryIfStale, action) {
2644
+ let start = performance.now();
2645
+ const config = out.name && this.preparedStatementsEnabled ? { name: out.name, text: out.text, values: params } : null;
2646
+ const report = (err, retried) => {
2647
+ const wrapped = err instanceof errors_js_1.TimeoutError ? err : (0, errors_js_1.wrapPgError)(err);
2648
+ const error = wrapped instanceof Error ? wrapped : undefined;
2649
+ this.emitQueryEvent(out, params, performance.now() - start, action, 0, error, retried);
2650
+ return wrapped;
2651
+ };
2652
+ let expired = false;
2653
+ let expire = () => { };
2654
+ const deadline = new Promise((resolve) => {
2655
+ expire = resolve;
2656
+ });
2657
+ const timer = setTimeout(() => {
2658
+ expired = true;
2659
+ expire(TIMED_OUT);
2660
+ }, timeout);
2661
+ const neverSent = () => new errors_js_1.TimeoutError(timeout, 'Query', {
2662
+ outcome: 'not-executed',
2663
+ message: `Query timed out after ${timeout}ms before it reached the server. It made no change.`,
2664
+ });
2665
+ try {
2666
+ for (let attempt = 0;; attempt++) {
2667
+ const checkingOut = (0, checkout_js_1.acquireConnection)(this.pool);
2668
+ let held;
2669
+ try {
2670
+ held = await Promise.race([checkingOut, deadline]);
2671
+ }
2672
+ catch (err) {
2673
+ throw report(err);
2674
+ }
2675
+ if (held === TIMED_OUT) {
2676
+ checkingOut.then((late) => late.checkout.release(), () => { });
2677
+ throw report(neverSent());
2678
+ }
2679
+ const { client, checkout } = held;
2680
+ const running = (config
2681
+ ? client.query(config)
2682
+ : client.query(out.text, params)).then((result) => ({ result }), (error) => ({ error }));
2683
+ const first = await Promise.race([running, deadline]);
2684
+ const settled = first === TIMED_OUT ? await this.cancelTimedOut(held, running, timeout) : first;
2685
+ if ('result' in settled) {
2686
+ if (first !== TIMED_OUT)
2687
+ checkout.release();
2688
+ this.emitQueryEvent(out, params, performance.now() - start, action, settled.result.rowCount ?? 0);
2689
+ return settled.result;
2690
+ }
2691
+ if ('timeout' in settled)
2692
+ throw report(settled.timeout);
2693
+ const err = settled.error;
2694
+ if (first === TIMED_OUT)
2695
+ throw report(err);
2696
+ checkout.release(err instanceof Error ? err : true);
2697
+ if (!(retryIfStale && attempt === 0 && (0, errors_js_1.isStaleConnectionError)(err)))
2698
+ throw report(err);
2699
+ report(err, true);
2700
+ await (0, connection_guard_js_1.settleEventLoop)();
2701
+ if (expired)
2702
+ throw report(neverSent());
2703
+ start = performance.now();
2704
+ }
2705
+ }
2706
+ finally {
2707
+ clearTimeout(timer);
2708
+ }
2709
+ }
2710
+ /**
2711
+ * A timed statement inside a transaction, cancelled on the server when the
2712
+ * deadline passes, on the transaction's own connection.
2713
+ *
2714
+ * A cancelled statement made no change, but PostgreSQL aborts the
2715
+ * transaction it ran in, so the timeout ends the transaction too: it is
2716
+ * marked to roll back ({@link TransactionState.abort}), which also covers a
2717
+ * cancel that arrived after the statement finished. The error is
2718
+ * `outcome: 'not-executed'` either way, because nothing it did can commit.
2719
+ * A statement that finishes before the deadline is returned as usual.
2720
+ */
2721
+ async queryCancellableInTransaction(out, params, timeout, state, action) {
2722
+ const start = performance.now();
2723
+ const running = (out.name && this.preparedStatementsEnabled
2724
+ ? this.pool.query({
2725
+ name: out.name,
2726
+ text: out.text,
2727
+ values: params,
2728
+ })
2729
+ : this.pool.query(out.text, params)).then((result) => ({ result }), (error) => ({ error }));
2730
+ let timer;
2731
+ const deadline = new Promise((resolve) => {
2732
+ timer = setTimeout(() => resolve(TIMED_OUT), timeout);
2733
+ });
2734
+ let settled;
2735
+ try {
2736
+ const first = await Promise.race([running, deadline]);
2737
+ settled = first === TIMED_OUT ? await this.cancelInTransaction(state, running, timeout) : first;
2738
+ }
2739
+ finally {
2740
+ clearTimeout(timer);
2741
+ }
2742
+ if ('result' in settled) {
2743
+ this.emitQueryEvent(out, params, performance.now() - start, action, settled.result.rowCount ?? 0);
2744
+ return settled.result;
2745
+ }
2746
+ const err = 'timeout' in settled ? settled.timeout : (0, errors_js_1.wrapPgError)(settled.error);
2747
+ this.emitQueryEvent(out, params, performance.now() - start, action, 0, err instanceof Error ? err : undefined);
2748
+ throw err;
2749
+ }
2750
+ /** Cancel a timed-out statement in a transaction and end the transaction. See {@link queryCancellableInTransaction}. */
2751
+ async cancelInTransaction(state, running, timeout) {
2752
+ let graceTimer;
2753
+ const graceOver = new Promise((resolve) => {
2754
+ graceTimer = setTimeout(() => resolve(TIMED_OUT), CANCEL_GRACE_MS);
2755
+ });
2756
+ try {
2757
+ await Promise.race([(0, utils_js_1.sendCancelRequest)(state.client, CANCEL_GRACE_MS), graceOver]);
2758
+ const answer = await Promise.race([running, graceOver]);
2759
+ if (answer !== TIMED_OUT && 'result' in answer)
2760
+ return answer;
2761
+ const cancelled = answer !== TIMED_OUT && isQueryCanceled(answer.error);
2762
+ if (answer !== TIMED_OUT && !cancelled)
2763
+ return answer;
2764
+ const error = new errors_js_1.TimeoutError(timeout, 'Query', {
2765
+ outcome: 'not-executed',
2766
+ ...(cancelled ? { cause: answer.error } : {}),
2767
+ message: (cancelled
2768
+ ? `Query timed out after ${timeout}ms and was cancelled on the server, so it made no change. `
2769
+ : `Query timed out after ${timeout}ms, and the server did not confirm its cancellation within ` +
2770
+ `${CANCEL_GRACE_MS}ms. `) +
2771
+ 'The transaction it ran in is over: it rolls back instead of committing, and no statement in it is ' +
2772
+ 'kept. Let this error leave the transaction callback and retry the whole transaction, or run a ' +
2773
+ 'statement that may time out in a nested `tx.$transaction(...)`, which rolls back only that block.',
2774
+ });
2775
+ state.abort(error);
2776
+ return { timeout: error };
2777
+ }
2778
+ finally {
2779
+ clearTimeout(graceTimer);
2780
+ }
2781
+ }
2782
+ /**
2783
+ * Cancel a statement that outlived its timeout, wait for it to answer, and
2784
+ * release its connection accordingly. See {@link queryCancellable}.
2785
+ */
2786
+ async cancelTimedOut(held, running, timeout) {
2787
+ let graceTimer;
2788
+ const graceOver = new Promise((resolve) => {
2789
+ graceTimer = setTimeout(() => resolve(TIMED_OUT), CANCEL_GRACE_MS);
2790
+ });
2791
+ try {
2792
+ const delivered = await Promise.race([(0, utils_js_1.sendCancelRequest)(held.client, CANCEL_GRACE_MS), graceOver]);
2793
+ const answer = await Promise.race([running, graceOver]);
2794
+ if (answer === TIMED_OUT) {
2795
+ held.checkout.release(new Error('the statement did not stop after a cancel request'));
2796
+ return {
2797
+ timeout: new errors_js_1.TimeoutError(timeout, 'Query', {
2798
+ outcome: 'unknown',
2799
+ message: `Query timed out after ${timeout}ms and the server did not confirm its cancellation within ` +
2800
+ `${CANCEL_GRACE_MS}ms, so it may still complete and a write may have been applied: check before ` +
2801
+ 'retrying it. The connection was closed.',
2802
+ }),
2803
+ };
2804
+ }
2805
+ // A cancel that was sent but not confirmed could still land on the NEXT
2806
+ // statement this connection runs, so such a connection is not reused.
2807
+ const reusable = delivered === true;
2808
+ if ('result' in answer) {
2809
+ held.checkout.release(reusable ? undefined : true);
2810
+ return answer;
2811
+ }
2812
+ const err = answer.error;
2813
+ if (reusable && err?.code === '57014') {
2814
+ held.checkout.release();
2815
+ return {
2816
+ timeout: new errors_js_1.TimeoutError(timeout, 'Query', {
2817
+ outcome: 'not-executed',
2818
+ cause: err,
2819
+ message: `Query timed out after ${timeout}ms and was cancelled on the server. It made no change.`,
2820
+ }),
2821
+ };
2822
+ }
2823
+ held.checkout.release(err instanceof Error ? err : true);
2824
+ return answer;
2825
+ }
2826
+ finally {
2827
+ clearTimeout(graceTimer);
2828
+ }
2829
+ }
2224
2830
  /**
2225
2831
  * Execute a write `DeferredQuery` (create/update/delete/upsert) according to
2226
2832
  * the active dialect's {@link Dialect.resultStrategy}, then apply its
@@ -2235,18 +2841,104 @@ class QueryInterface {
2235
2841
  * write and a follow-up SELECT; the SELECT's rows feed the transform.
2236
2842
  */
2237
2843
  async executeMutation(deferred, timeout) {
2238
- if (this.dialect.resultStrategy === 'reselect' && deferred.reselect) {
2239
- const exec = (sql, params, preparedName) => this.queryWithTimeout(sql, params, timeout, preparedName);
2240
- const result = await deferred.reselect(exec);
2241
- return deferred.transform(result);
2844
+ const reselect = deferred.reselect;
2845
+ if (this.dialect.resultStrategy === 'reselect' && reselect) {
2846
+ // One transaction, so the row returned is the row this call wrote, and
2847
+ // the transform runs before COMMIT so a refusal it raises writes nothing.
2848
+ return this.inOneTransaction(async (exec) => deferred.transform(await reselect(exec)), timeout, `A ${this.dialect.name} write`, 'Pass a pool that can check out a connection.');
2242
2849
  }
2243
2850
  const result = await this.queryWithTimeout(deferred.sql, deferred.params, timeout, deferred.preparedName);
2244
2851
  return deferred.transform(result);
2245
2852
  }
2853
+ /**
2854
+ * The executor for a write's statements, sharing one deadline: each one gets
2855
+ * what is left of `timeout`, so a chunked `createMany` or a lock, write and
2856
+ * re-read cannot run for N times the timeout. A statement due past the
2857
+ * deadline is not sent. `onClient` routes them to a held connection.
2858
+ */
2859
+ deadlineExec(timeout, onClient) {
2860
+ const deadline = timeout ? performance.now() + timeout : undefined;
2861
+ return async (sql, params, preparedName) => {
2862
+ let left = timeout;
2863
+ if (deadline !== undefined) {
2864
+ left = Math.ceil(deadline - performance.now());
2865
+ if (left <= 0) {
2866
+ throw new errors_js_1.TimeoutError(timeout, 'Query', {
2867
+ outcome: 'not-executed',
2868
+ message: `Query timed out after ${timeout}ms: the write's statements share one deadline, and it passed ` +
2869
+ 'before the next one was sent. Its earlier statements are rolled back, so it makes no change.',
2870
+ });
2871
+ }
2872
+ }
2873
+ return onClient
2874
+ ? this.queryWithTimeout(sql, params, left, preparedName, false, onClient)
2875
+ : this.queryWithTimeout(sql, params, left, preparedName);
2876
+ };
2877
+ }
2878
+ /**
2879
+ * Run a write that takes several statements as one transaction on one
2880
+ * connection: the caller's transaction when there is one, else one opened
2881
+ * here. A statement that times out is cancelled where the engine allows it,
2882
+ * and its connection is closed rather than rolled back (a ROLLBACK would wait
2883
+ * behind it); the transaction never commits, so the write made no change.
2884
+ * `timeout` is one deadline for every statement (see {@link deadlineExec}).
2885
+ */
2886
+ async inOneTransaction(work, timeout, operation, alternative) {
2887
+ if (this.txScoped) {
2888
+ // In the caller's transaction, a savepoint keeps the write whole: a later
2889
+ // statement that fails and is caught cannot leave the earlier ones behind.
2890
+ return (0, transaction_state_js_1.withSavepoint)(this.pool, () => work(this.deadlineExec(timeout)));
2891
+ }
2892
+ if (typeof this.pool.connect !== 'function') {
2893
+ throw new errors_js_1.UnsupportedFeatureError(`${operation} on a pool without connect()`, this.dialect.name, `${operation} runs several statements in one transaction, which needs a connection of its own, and ` +
2894
+ `this pool cannot check one out (a stateless HTTP driver). ${alternative}`);
2895
+ }
2896
+ const { client, checkout } = await (0, checkout_js_1.openCheckout)(this.pool, (c) => c.query(this.dialect.beginStatement()));
2897
+ const onClient = {
2898
+ query: (text, values) => typeof text === 'string'
2899
+ ? client.query(text, values)
2900
+ : client.query(text),
2901
+ };
2902
+ try {
2903
+ const value = await work(this.deadlineExec(timeout, onClient));
2904
+ await client.query(this.dialect.commitStatement());
2905
+ checkout.release();
2906
+ return value;
2907
+ }
2908
+ catch (err) {
2909
+ if (err instanceof errors_js_1.TimeoutError) {
2910
+ if (await (0, utils_js_1.sendCancelRequest)(client, CANCEL_GRACE_MS)) {
2911
+ checkout.release(err);
2912
+ }
2913
+ else {
2914
+ // No cancel reached the server (or the engine has none), so the
2915
+ // statement may still be running. The ROLLBACK queues behind it on
2916
+ // this connection, and the connection goes back to the pool only
2917
+ // after it: never with the transaction still open.
2918
+ client.query(this.dialect.rollbackStatement()).then(() => checkout.release(), (e) => checkout.release(e instanceof Error ? e : true));
2919
+ }
2920
+ throw new errors_js_1.TimeoutError(err.timeoutMs, 'Query', {
2921
+ outcome: 'not-executed',
2922
+ cause: err,
2923
+ message: `Query timed out after ${err.timeoutMs}ms. Its transaction is rolled back rather than committed, ` +
2924
+ 'so it makes no change.',
2925
+ });
2926
+ }
2927
+ try {
2928
+ await client.query(this.dialect.rollbackStatement());
2929
+ }
2930
+ catch {
2931
+ // Best effort: the connection may be gone.
2932
+ }
2933
+ checkout.release();
2934
+ throw (0, errors_js_1.explainConnectionLoss)((0, errors_js_1.wrapPgError)(err), checkout.lostWith);
2935
+ }
2936
+ }
2246
2937
  // ---------------------------------------------------------------------------
2247
2938
  // Write compilation (extracted to writes.ts).
2248
2939
  // ---------------------------------------------------------------------------
2249
2940
  buildCreate(args) {
2941
+ args = this.normalizeArgs(args);
2250
2942
  return writesMod.buildCreate(this.ctx, args, this.resolveWriteProjection(args));
2251
2943
  }
2252
2944
  /**
@@ -2268,22 +2960,26 @@ class QueryInterface {
2268
2960
  return { columns };
2269
2961
  }
2270
2962
  buildCreateMany(args) {
2271
- return writesMod.buildCreateMany(this.ctx, args);
2963
+ args = this.normalizeArgs(args);
2964
+ return writesMod.buildCreateMany(this.ctx, args, 'refuse', this.resolveWriteProjection(args));
2272
2965
  }
2273
2966
  buildUpdate(args) {
2967
+ args = this.normalizeArgs(args);
2274
2968
  return writesMod.buildUpdate(this.ctx, args, this.resolveWriteProjection(args));
2275
2969
  }
2276
2970
  buildDelete(args) {
2971
+ args = this.normalizeArgs(args);
2277
2972
  return writesMod.buildDelete(this.ctx, args, this.resolveWriteProjection(args));
2278
2973
  }
2279
2974
  buildUpsert(args) {
2975
+ args = this.normalizeArgs(args);
2280
2976
  return writesMod.buildUpsert(this.ctx, args, this.resolveWriteProjection(args));
2281
2977
  }
2282
2978
  buildUpdateMany(args) {
2283
- return writesMod.buildUpdateMany(this.ctx, args);
2979
+ return writesMod.buildUpdateMany(this.ctx, this.normalizeArgs(args));
2284
2980
  }
2285
2981
  buildDeleteMany(args) {
2286
- return writesMod.buildDeleteMany(this.ctx, args);
2982
+ return writesMod.buildDeleteMany(this.ctx, this.normalizeArgs(args));
2287
2983
  }
2288
2984
  /**
2289
2985
  * Best-effort extraction of an auto-generated primary key from a write
@@ -2299,11 +2995,10 @@ class QueryInterface {
2299
2995
  * If no middlewares are registered, executes directly.
2300
2996
  *
2301
2997
  * Middleware can inspect and log query parameters, measure timing, and
2302
- * transform the result returned by `next()`. Note: query SQL is generated
2303
- * BEFORE middleware runs, `params.args` is a read-only snapshot, and
2304
- * mutating it does NOT change the executed SQL. Cross-cutting filters
2305
- * (e.g. soft deletes) belong in the query itself: pass an explicit
2306
- * `where: { deletedAt: null }` or wrap the table accessor in a small helper.
2998
+ * transform the result returned by `next()`. `params.args` is a plain copy
2999
+ * (see `readOnlyMiddlewareParams`): the query is compiled from the caller's
3000
+ * args, so `next()` throws a TypeError when the copy was changed. Cross-cutting
3001
+ * filters (e.g. soft deletes) belong in the query itself, or in `globalFilters`.
2307
3002
  */
2308
3003
  async executeWithMiddleware(action, args, executor) {
2309
3004
  this.currentAction = action;
@@ -2312,14 +3007,17 @@ class QueryInterface {
2312
3007
  // can be complete. Putting it in each method instead would mean fifteen
2313
3008
  // sites and a new one every time an operation is added, which is precisely
2314
3009
  // how the surface it checks came to need checking.
2315
- (0, option_surface_js_1.warnUnknownQueryOptions)(this.table, action, args);
3010
+ (0, option_surface_js_1.warnUnknownQueryOptions)(this.table, action, args, this.advice ?? null);
2316
3011
  if (this.middlewares.length === 0) {
2317
3012
  return executor();
2318
3013
  }
2319
- const params = { model: this.table, action, args: { ...args } };
3014
+ // A copy: the query is compiled from `args`, not from what middleware
3015
+ // sees, so `next()` throws on a changed copy rather than doing nothing.
3016
+ const params = (0, utils_js_1.readOnlyMiddlewareParams)(this.table, action, args);
2320
3017
  // Build middleware chain
2321
3018
  let index = 0;
2322
3019
  const next = async (p) => {
3020
+ (0, utils_js_1.assertMiddlewareArgsKept)(params, p);
2323
3021
  if (index < this.middlewares.length) {
2324
3022
  const mw = this.middlewares[index++];
2325
3023
  return mw(p, next);
@@ -2342,6 +3040,7 @@ class QueryInterface {
2342
3040
  // nothing else). It is here so the invariant holds at the seam rather than
2343
3041
  // depending on every consumer to re-establish it.
2344
3042
  args = this.normalizeArgs(args);
3043
+ this.assertLockInTransaction(args, 'findUnique');
2345
3044
  return this.executeWithMiddleware('findUnique', args, async () => {
2346
3045
  if (args.with) {
2347
3046
  const strategy = this.resolveLoadStrategy(args.relationLoadStrategy);
@@ -2394,6 +3093,9 @@ class QueryInterface {
2394
3093
  return entity;
2395
3094
  }
2396
3095
  buildFindUnique(args) {
3096
+ // Canonical args FIRST: the empty-where guard and the unique-key rule below
3097
+ // must read the pruned `where`, never one still carrying `undefined` keys.
3098
+ args = this.normalizeArgs(args);
2397
3099
  this.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
2398
3100
  // Pinned before the cache key, which carries it (see buildFindMany).
2399
3101
  const jsonEncoding = this.resolveJsonEncoding(args.jsonEncoding);
@@ -2415,9 +3117,10 @@ class QueryInterface {
2415
3117
  // deliberately so: a tenant filter is not a unique selector, and letting it
2416
3118
  // satisfy this would still hand back an arbitrary row from inside the tenant.
2417
3119
  //
2418
- // `findFirst` is intentionally NOT guarded. "The first row matching an
2419
- // optional filter" is its whole contract and Prisma's `findFirst` behaves
2420
- // the same way; ask for one row by identity with `findUnique`.
3120
+ // `findFirst` takes no such guard: "the first row matching an optional
3121
+ // filter" is its contract, and `where: {}` or no `where` asks for any row.
3122
+ // It refuses only a `where` that was emptied by `undefined` values (see
3123
+ // normalizeArgs), which never meant "any row".
2421
3124
  if (whereMod.userPredicateIsEmpty(this.ctx, (args.where ?? {}))) {
2422
3125
  throw new errors_js_1.ValidationError(`findUnique on "${this.table}" refused: the \`where\` clause has no predicate, ` +
2423
3126
  'so this would return an arbitrary row rather than a specific one. ' +
@@ -2437,9 +3140,9 @@ class QueryInterface {
2437
3140
  // a global filter is not an identity, and letting one satisfy this would
2438
3141
  // hand back an arbitrary row from inside the tenant.
2439
3142
  this.assertFindUniqueKey(args.where);
2440
- // Declared relation spellings first, before stable-order and the
2441
- // fingerprint (see buildFindMany).
2442
- args = this.normalizeArgs(args);
3143
+ // The locking clause, read by the cache key and all three SQL paths below.
3144
+ const rowLock = (0, row_lock_js_1.resolveRowLock)(args.lock, this.dialect, this.table);
3145
+ const lockSql = rowLock?.sql ?? '';
2443
3146
  // Stable relation order (opt-in): fill PK-asc orderBy into unordered to-many
2444
3147
  // relations before fingerprinting (see buildFindMany).
2445
3148
  if (args.with && this.resolveStableOrder(args.stableRelationOrder)) {
@@ -2469,7 +3172,9 @@ class QueryInterface {
2469
3172
  // `|je=`: same rule and same hazard as in buildFindMany. findUnique is never
2470
3173
  // flatten-planned, so this is the only plan-shape segment it needs.
2471
3174
  const encodingFp = `|je=${jsonEncoding === 'positional' ? 'p' : 'o'}`;
2472
- const ck = `fu:${whereFingerprint}|c=${colKey}|w=${withFp}|pii=${includePii ? 1 : 0}${encodingFp}${this.globalFilterCacheSegment()}`;
3175
+ // Empty without a lock, so every unlocked key is unchanged.
3176
+ const lockFp = rowLock ? `|lk=${rowLock.fp}` : '';
3177
+ const ck = `fu:${whereFingerprint}|c=${colKey}|w=${withFp}|pii=${includePii ? 1 : 0}${encodingFp}${lockFp}${this.globalFilterCacheSegment()}`;
2473
3178
  const params = [];
2474
3179
  // Check if all where values are simple (plain equality, no operators/null/OR).
2475
3180
  // Keys are sorted to match fingerprintWhere, insertion order here would let
@@ -2477,14 +3182,19 @@ class QueryInterface {
2477
3182
  const whereKeys = Object.keys(whereObj)
2478
3183
  .filter((k) => whereObj[k] !== undefined)
2479
3184
  .sort();
3185
+ // Only a scalar the general path would accept as it is (a primitive, Date
3186
+ // or Buffer) on a non-JSON column takes the simple path, so no operand
3187
+ // reaches SQL without the general path's validation. Resolved, not looked
3188
+ // up: a relation filter spelled the snake_case way must not read as a
3189
+ // plain equality.
2480
3190
  const isSimpleWhere = !whereObj.OR &&
2481
3191
  !whereObj.AND &&
2482
3192
  !whereObj.NOT &&
2483
3193
  whereKeys.every((k) => {
2484
- const v = whereObj[k];
2485
- // Resolved, not looked up: a relation filter spelled the snake_case
2486
- // way must not read as a plain equality and take the simple path.
2487
- return v !== null && !(0, filters_js_1.isWhereOperator)(v) && !(0, utils_js_1.resolveRelationDef)(this.tableMeta.relations, k);
3194
+ if (!isFastPathScalar(whereObj[k]) || (0, utils_js_1.resolveRelationDef)(this.tableMeta.relations, k))
3195
+ return false;
3196
+ const column = (0, utils_js_1.resolveColumnName)(this.tableMeta, k);
3197
+ return column !== undefined && !this.isJsonColumnType(this.getColumnPgType(column));
2488
3198
  });
2489
3199
  // Simple path: plain equality, no operators/null/OR.
2490
3200
  //
@@ -2505,7 +3215,7 @@ class QueryInterface {
2505
3215
  });
2506
3216
  const whereSql = whereClauses.length > 0 ? ` WHERE ${whereClauses.join(' AND ')}` : '';
2507
3217
  const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ') : `${qt}.*`;
2508
- return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}`;
3218
+ return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}${lockSql}`;
2509
3219
  };
2510
3220
  const entry = this.acquireSql(ck, buildSql);
2511
3221
  // Collect params (same order and same coercion as build)
@@ -2522,6 +3232,7 @@ class QueryInterface {
2522
3232
  },
2523
3233
  tag: `${this.table}.findUnique`,
2524
3234
  preparedName: entry.name,
3235
+ ...rowLockMarker(rowLock),
2525
3236
  };
2526
3237
  }
2527
3238
  // General path (with operators, null, OR, with clause)
@@ -2531,7 +3242,7 @@ class QueryInterface {
2531
3242
  const whereSql = clause ? ` WHERE ${clause}` : '';
2532
3243
  const qt = this.q(this.table);
2533
3244
  const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ') : `${qt}.*`;
2534
- return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}`;
3245
+ return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}${lockSql}`;
2535
3246
  };
2536
3247
  const entry = this.acquireSql(ck, buildSql);
2537
3248
  // Collect params
@@ -2546,6 +3257,7 @@ class QueryInterface {
2546
3257
  },
2547
3258
  tag: `${this.table}.findUnique`,
2548
3259
  preparedName: entry.name,
3260
+ ...rowLockMarker(rowLock),
2549
3261
  };
2550
3262
  }
2551
3263
  // Nested queries with `with` clause.
@@ -2557,7 +3269,7 @@ class QueryInterface {
2557
3269
  const clause = this.buildWhereClause(whereObj, freshParams);
2558
3270
  const whereSql = clause ? ` WHERE ${clause}` : '';
2559
3271
  const selectClause = this.buildSelectWithRelations(this.table, args.with, freshParams, columnsList, undefined, undefined, includePii);
2560
- return `SELECT ${selectClause} FROM ${this.q(this.table)}${whereSql}${this.limitOneClause()}`;
3272
+ return `SELECT ${selectClause} FROM ${this.q(this.table)}${whereSql}${this.limitOneClause()}${lockSql}`;
2561
3273
  };
2562
3274
  const entry = this.acquireSql(ck, buildSql);
2563
3275
  // Collect params in exact build order: where first, then with-clause relations
@@ -2574,6 +3286,7 @@ class QueryInterface {
2574
3286
  },
2575
3287
  tag: `${this.table}.findUnique`,
2576
3288
  preparedName: entry.name,
3289
+ ...rowLockMarker(rowLock),
2577
3290
  };
2578
3291
  }
2579
3292
  // -------------------------------------------------------------------------
@@ -2589,19 +3302,20 @@ class QueryInterface {
2589
3302
  // nothing else). It is here so the invariant holds at the seam rather than
2590
3303
  // depending on every consumer to re-establish it.
2591
3304
  args = this.normalizeArgs(args);
3305
+ this.assertLockInTransaction(args, 'findMany');
2592
3306
  this.maybeWarnUnlimited(args);
2593
3307
  this.maybeWarnUnorderedPage(args);
2594
- // Dev-only: warn on deeply nested with clauses
2595
- if (process.env.NODE_ENV !== 'production') {
2596
- if (args?.with) {
2597
- const depth = this.measureWithDepth(args.with);
2598
- if (depth > 5 && (0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.deepWith, this.table)) {
2599
- console.warn(`[turbine] Deep with clause (depth ${depth}) on "${this.tableMeta.name}": every level is a ` +
2600
- 'correlated subquery the engine re-evaluates once per row of the level above, so the work ' +
2601
- 'multiplies down the tree and the whole subtree is built as JSON inside each parent row. Split ' +
2602
- "into separate queries, or use `relationLoadStrategy: 'batched'` (one flat statement per " +
2603
- 'relation, no per-parent re-evaluation). Dev-only: silent under `NODE_ENV=production`.');
2604
- }
3308
+ // Advice: warn on deeply nested with clauses.
3309
+ const advice = this.advice;
3310
+ if (advice && args?.with) {
3311
+ const depth = this.measureWithDepth(args.with);
3312
+ if (depth > 5 && (0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.deepWith, this.table)) {
3313
+ (0, logger_js_1.logTo)(advice, 'warn', `Deep with clause (depth ${depth}) on "${this.tableMeta.name}": every level is a ` +
3314
+ 'correlated subquery the engine re-evaluates once per row of the level above, so the work ' +
3315
+ 'multiplies down the tree and the whole subtree is built as JSON inside each parent row. Split ' +
3316
+ "into separate queries, or use `relationLoadStrategy: 'batched'` (one flat statement per " +
3317
+ 'relation, no per-parent re-evaluation). Advice: silent when `diagnostics.advice` is off, as it ' +
3318
+ 'is by default in production.', { code: 'deep-with', table: this.table, operation: 'findMany', depth });
2605
3319
  }
2606
3320
  }
2607
3321
  return this.executeWithMiddleware('findMany', (args ?? {}), async () => {
@@ -2676,11 +3390,13 @@ class QueryInterface {
2676
3390
  }
2677
3391
  }
2678
3392
  /**
2679
- * Emit a one-time `console.warn` when {@link findMany} is called without an
3393
+ * Emit a one-time warning when {@link findMany} is called without an
2680
3394
  * explicit `limit`/`take` and `warnOnUnlimited` has not been disabled.
2681
3395
  *
2682
- * Deduped per QueryInterface instance via {@link warnedTables} so a busy
2683
- * loop calling `db.users.findMany()` thousands of times only logs once.
3396
+ * Said once per table per process (`WARN_NS.unlimitedFindMany`), so a busy
3397
+ * loop calling `db.users.findMany()` thousands of times logs once, and so
3398
+ * does the same call inside every `$transaction`, which builds its own
3399
+ * query interfaces.
2684
3400
  * Suppressed when `defaultLimit` is configured (the caller has already
2685
3401
  * opted in to a bounded query) and when the user passed an explicit
2686
3402
  * `limit`, `take`, or `cursor`. A per-call `warnOnUnlimited` overrides the
@@ -2696,19 +3412,18 @@ class QueryInterface {
2696
3412
  return;
2697
3413
  if (this.defaultLimit !== undefined)
2698
3414
  return;
2699
- const hasExplicitLimit = args?.limit !== undefined || args?.cursor !== undefined;
3415
+ const hasExplicitLimit = args?.limit !== undefined || (0, normalize_args_js_1.hasCursor)(args?.cursor);
2700
3416
  if (hasExplicitLimit)
2701
3417
  return;
2702
3418
  if (this.whereMatchesAtMostOneRow(args?.where))
2703
3419
  return;
2704
- if (this.warnedTables.has(this.table))
3420
+ if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.unlimitedFindMany, this.table))
2705
3421
  return;
2706
- this.warnedTables.add(this.table);
2707
- console.warn(`[turbine] warning: findMany on "${this.table}" has no limit: the statement is emitted with no row limit, ` +
3422
+ (0, logger_js_1.logTo)(this.logger, 'warn', `warning: findMany on "${this.table}" has no limit: the statement is emitted with no row limit, ` +
2708
3423
  'so the engine returns every matching row and the driver materializes all of them as objects before ' +
2709
3424
  'this call resolves (the cost grows with the table, not with the rows you use). Pass `limit`/`take`, ' +
2710
3425
  'or set `defaultLimit` in the client config; silence with `warnOnUnlimited: false` (per call, per ' +
2711
- 'table, or in config).');
3426
+ 'table, or in config).', { code: 'unlimited-find-many', table: this.table, operation: 'findMany' });
2712
3427
  }
2713
3428
  /**
2714
3429
  * Whether `where` can match at most one row, because it pins every column of
@@ -2755,7 +3470,7 @@ class QueryInterface {
2755
3470
  }
2756
3471
  /**
2757
3472
  * Recursively measure the maximum depth of a `with` clause tree.
2758
- * Used by the dev-only deep-with warning guard.
3473
+ * Used by the deep-with advice.
2759
3474
  */
2760
3475
  measureWithDepth(withClause) {
2761
3476
  let maxDepth = 1;
@@ -2806,7 +3521,7 @@ class QueryInterface {
2806
3521
  if (args?.orderBy !== undefined) {
2807
3522
  const deduped = (0, filters_js_1.dedupeOrderEntries)(this.tableMeta, (0, filters_js_1.orderByEntries)(args.orderBy), this.table);
2808
3523
  if (deduped) {
2809
- (0, utils_js_1.warnRedundantSortTerm)(this.table, 'orderBy', deduped.dropped);
3524
+ (0, utils_js_1.warnRedundantSortTerm)(this.advice, this.table, 'orderBy', deduped.dropped);
2810
3525
  args = {
2811
3526
  ...args,
2812
3527
  orderBy: deduped.entries.map(([k, v]) => ({ [k]: v })),
@@ -2816,7 +3531,7 @@ class QueryInterface {
2816
3531
  if (args?.distinct && args.distinct.length > 0) {
2817
3532
  const deduped = (0, filters_js_1.dedupeColumnList)(this.tableMeta, args.distinct);
2818
3533
  if (deduped) {
2819
- (0, utils_js_1.warnRedundantSortTerm)(this.table, 'distinct', deduped.dropped);
3534
+ (0, utils_js_1.warnRedundantSortTerm)(this.advice, this.table, 'distinct', deduped.dropped);
2820
3535
  args = { ...args, distinct: deduped.columns };
2821
3536
  }
2822
3537
  }
@@ -2827,29 +3542,40 @@ class QueryInterface {
2827
3542
  const implicitOrder = this.implicitPkOrderBy(args);
2828
3543
  if (implicitOrder)
2829
3544
  args = { ...args, orderBy: implicitOrder };
3545
+ // A `cursor` that names no row (`null`, `{}`) is refused before either
3546
+ // reader below asks whether there is one.
3547
+ (0, normalize_args_js_1.assertCursorWritten)(this.table, 'findMany', args?.cursor);
3548
+ // A page whose order can tie gets the cursor tiebreaker (completePageOrder),
3549
+ // before fingerprinting, so the cache key carries it.
3550
+ if (args)
3551
+ args = this.completePageOrder(args);
3552
+ // A cursor page is planned (and its orderBy completed to the effective
3553
+ // order) BEFORE fingerprinting, so the key, the SQL and the param collect
3554
+ // all read one plan. See planCursor.
3555
+ let cursorPlan;
3556
+ if ((0, normalize_args_js_1.hasCursor)(args?.cursor))
3557
+ ({ args, plan: cursorPlan } = this.planCursor(args));
2830
3558
  // `distinct` compiles to `SELECT DISTINCT ON (...)`, which is PostgreSQL
2831
3559
  // syntax and nothing else's. Every other engine parsed it as far as the
2832
3560
  // word `ON` and answered with a RAW driver error carrying no TURBINE_ code
2833
3561
  // (SQLite: `near "ON": syntax error`; MySQL: ER_PARSE_ERROR), i.e. the one
2834
3562
  // shape the typed-error surface exists to prevent. Refused with the same
2835
- // E017 the OTHER spelling of this feature already used, `groupBy({
2836
- // distinctOn })` in aggregates.ts, which was gated correctly all along.
2837
- //
2838
- // `dialect.name` rather than a new capability flag, deliberately, and this
2839
- // is the exception to the "capability flags, not engine names" rule the
2840
- // rest of the dialect follows: every engine dialect is built by SPREADING
2841
- // `postgresDialect`, so a flag defaulting to `true` would be inherited by
2842
- // all of them (which is the bug) and one defaulting to `false` would have
2843
- // to be re-declared on Postgres-compatible dialects that already work.
2844
- // The established precedent for exactly this feature is the `dialect.name`
2845
- // test in `buildDistinctOnSource`.
3563
+ // E017, on the same `distinctOn` capability, as the OTHER spelling of this
3564
+ // feature, `groupBy({ distinctOn })` in aggregates.ts.
2846
3565
  //
2847
3566
  // Before the cache, like the two guards below, so a warmed template cannot
2848
3567
  // serve the query the cold path refuses.
2849
- if (args?.distinct && args.distinct.length > 0 && this.dialect.name !== 'postgresql') {
3568
+ if (args?.distinct && args.distinct.length > 0 && !this.dialect.capabilities.distinctOn) {
2850
3569
  throw new errors_js_1.UnsupportedFeatureError('DISTINCT ON (findMany distinct)', this.dialect.name, 'findMany({ distinct }) requires PostgreSQL: SELECT DISTINCT ON is not portable. Group in ' +
2851
3570
  'application code, or use groupBy({ by }) for one row per combination.');
2852
3571
  }
3572
+ // The locking clause, validated and rendered once: the cache key and the
3573
+ // SQL both read this value. PostgreSQL refuses a lock on a DISTINCT query
3574
+ // (0A000), so that pair is refused here, before the cache.
3575
+ const rowLock = (0, row_lock_js_1.resolveRowLock)(args?.lock, this.dialect, this.table);
3576
+ if (rowLock && args?.distinct && args.distinct.length > 0) {
3577
+ throw new errors_js_1.ValidationError((0, row_lock_js_1.lockWithDistinctMessage)(this.table));
3578
+ }
2853
3579
  // `distinct` + relation orderBy is refused up front (E003): the distinct
2854
3580
  // path re-orders in an outer wrapper (`... AS "<table>_distinct" ORDER BY
2855
3581
  // <userOrder>`) where a correlated relation subquery (pick-row, `_count`,
@@ -2896,14 +3622,9 @@ class QueryInterface {
2896
3622
  })
2897
3623
  .join(',')
2898
3624
  : '';
2899
- // Keyset order AND per-field direction: a multi-field cursor's predicate
2900
- // is one OR-branch per field in orderBy precedence, and `<` / `>` are SQL
2901
- // text, so `asc` and `desc` (or two precedences) can never share a template.
2902
- const cursorFp = args?.cursor
2903
- ? this.cursorSeekEntries(args.cursor, args.orderBy)
2904
- .map((e) => `${e.column}:${e.desc ? 'd' : 'a'}`)
2905
- .join(',')
2906
- : '';
3625
+ // Mode, keyset order, directions, NULL placement and which values are
3626
+ // NULL all change the predicate's SQL text (see planCursor).
3627
+ const cursorFp = cursorPlan?.fingerprint ?? '';
2907
3628
  // distinct must fingerprint in USER order: the SQL emits `DISTINCT ON` in
2908
3629
  // the caller's column order, so a permuted array rebuilds different SQL and
2909
3630
  // must not collapse onto the same cache entry (would trip the cross-check).
@@ -2944,7 +3665,9 @@ class QueryInterface {
2944
3665
  // non-default one, so the key never depends on what the client default
2945
3666
  // happens to be.
2946
3667
  const encodingFp = `|je=${jsonEncoding === 'positional' ? 'p' : 'o'}`;
2947
- const ck = `fm:${whereFp}|c=${colKey}|o=${orderFp}|l=${limitFp}|off=${offsetFp}|cur=${cursorFp}|d=${distinctFp}|w=${withFp}|pii=${includePii ? 1 : 0}${flattenFp}${encodingFp}${this.globalFilterCacheSegment()}`;
3668
+ // Empty without a lock, so every unlocked key is unchanged.
3669
+ const lockFp = rowLock ? `|lk=${rowLock.fp}` : '';
3670
+ const ck = `fm:${whereFp}|c=${colKey}|o=${orderFp}|l=${limitFp}|off=${offsetFp}|cur=${cursorFp}|d=${distinctFp}|w=${withFp}|pii=${includePii ? 1 : 0}${flattenFp}${encodingFp}${lockFp}${this.globalFilterCacheSegment()}`;
2948
3671
  const params = [];
2949
3672
  const buildSql = (freshParams) => {
2950
3673
  // Fresh build: generates SQL and populates freshParams
@@ -3010,18 +3733,9 @@ class QueryInterface {
3010
3733
  // WHERE + cursor conditions accumulate into `tail`, pushing params in
3011
3734
  // where → cursor order (the collect path mirrors this exactly).
3012
3735
  let tail = freshWhereSql;
3013
- if (args?.cursor) {
3014
- // Keyset order (orderBy precedence), MUST match cursorFp and the
3015
- // cache-hit collect below: both go through cursorSeekEntries.
3016
- const seek = this.cursorSeekEntries(args.cursor, args.orderBy);
3017
- if (seek.length > 0) {
3018
- const terms = seek.map((e) => {
3019
- freshParams.push(e.value);
3020
- return { column: `${qt}.${this.q(e.column)}`, placeholder: this.p(freshParams.length), desc: e.desc };
3021
- });
3022
- const predicate = this.keysetPredicate(terms);
3023
- tail += freshWhereSql ? ` AND ${predicate}` : ` WHERE ${predicate}`;
3024
- }
3736
+ if (cursorPlan) {
3737
+ const predicate = this.renderCursorPredicate(cursorPlan, freshParams);
3738
+ tail += freshWhereSql ? ` AND ${predicate}` : ` WHERE ${predicate}`;
3025
3739
  }
3026
3740
  // ORDER BY is built AFTER the cursor pushes (param order
3027
3741
  // where → with → cursor → orderBy → limit → offset) and BEFORE final
@@ -3095,6 +3809,9 @@ class QueryInterface {
3095
3809
  offsetPh = this.paginationRef(args.offset, freshParams, 'skip/offset');
3096
3810
  }
3097
3811
  sql += this.buildPagination(limitPh, offsetPh, !!args?.orderBy);
3812
+ // The locking clause follows pagination and binds nothing.
3813
+ if (rowLock)
3814
+ sql += rowLock.sql;
3098
3815
  return sql;
3099
3816
  };
3100
3817
  const entry = this.acquireSql(ck, buildSql);
@@ -3107,13 +3824,9 @@ class QueryInterface {
3107
3824
  if (args?.with) {
3108
3825
  this.collectWithParams(args.with, params, undefined, flattenPlan);
3109
3826
  }
3110
- // 3. Cursor params, keyset order, matching cursorFp and the build path
3111
- // (each value binds ONCE; the expanded predicate re-references its `$n`).
3112
- if (args?.cursor) {
3113
- for (const e of this.cursorSeekEntries(args.cursor, args.orderBy)) {
3114
- params.push(e.value);
3115
- }
3116
- }
3827
+ // 3. Cursor params: the build's own renderer, SQL discarded.
3828
+ if (cursorPlan)
3829
+ this.renderCursorPredicate(cursorPlan, params);
3117
3830
  // 4. ORDER BY params (vector KNN ordering binds a `$n::vector` query vector).
3118
3831
  // Mirrors buildOrderBy's push order, between cursor and LIMIT.
3119
3832
  if (args?.orderBy) {
@@ -3140,6 +3853,7 @@ class QueryInterface {
3140
3853
  transform: (result) => result.rows.map((row) => (parseWith ? parseWith(row) : this.parseRow(row, this.table))),
3141
3854
  tag: `${this.table}.findMany`,
3142
3855
  preparedName: entry.name,
3856
+ ...rowLockMarker(rowLock),
3143
3857
  };
3144
3858
  }
3145
3859
  // -------------------------------------------------------------------------
@@ -3200,20 +3914,21 @@ class QueryInterface {
3200
3914
  * the result set is empty. Callers may therefore treat a yielded batch as
3201
3915
  * non-empty.
3202
3916
  */
3203
- async *streamRaw(args, action) {
3204
- // Pagination aliases folded BEFORE the speculative build below, which
3205
- // spreads `args` and overrides `limit`. A surviving `take` would reach that
3206
- // spread alongside the override and be read as two different values for one
3207
- // bound. Relation names are left to `buildFindMany`, which both branches go
3208
- // through.
3209
- args = (0, utils_js_1.normalizePagination)(args);
3917
+ async *streamRaw(args, action, idle, unit) {
3918
+ // Canonical args BEFORE the speculative build below, which spreads `args`
3919
+ // and sets its own `limit`: a surviving `take` would reach that spread
3920
+ // beside it and be read as a second value for one bound.
3921
+ args = this.normalizeArgs(args);
3210
3922
  const batchSize = Math.max(1, Math.floor(Number(args?.batchSize ?? 1000)));
3923
+ // Validated on every path, so a bad value is refused whether or not this
3924
+ // stream turns out to need a cursor.
3925
+ const idleTimeoutMs = (0, stream_idle_js_1.resolveStreamIdleTimeout)(args?.idleTimeoutMs, 'idleTimeoutMs') ?? this.streamIdleTimeoutMs;
3211
3926
  this.currentAction = action;
3212
3927
  // The two streaming methods do NOT go through `executeWithMiddleware`, so
3213
3928
  // the unknown-key diagnostic has to be hung here as well or a stream would
3214
3929
  // be the one read that silently drops an `include`. Both public methods
3215
3930
  // reach this, and each passes its own name.
3216
- (0, option_surface_js_1.warnUnknownQueryOptions)(this.table, action, args);
3931
+ (0, option_surface_js_1.warnUnknownQueryOptions)(this.table, action, args, this.advice ?? null);
3217
3932
  // Streaming is ALREADY immune to the generic-plan cliff: the speculative
3218
3933
  // fetch has never passed a prepared name, and the cursor path runs through
3219
3934
  // DECLARE, so neither statement enters the plan cache. `preparedNameFor` is
@@ -3250,9 +3965,18 @@ class QueryInterface {
3250
3965
  // four round trips on a drain that would have fit, never any transferred
3251
3966
  // rows.
3252
3967
  if (batchSize <= STREAM_SPECULATION_MAX_BATCH) {
3968
+ // The caller's bound (or the client's `defaultLimit`, exactly as findMany
3969
+ // applies it) caps the speculative fetch. When the bound fits in a batch
3970
+ // the speculative fetch IS the whole answer; otherwise `rows <= batchSize`
3971
+ // still means fewer rows exist than either bound, and an overflow goes to
3972
+ // the cursor, whose statement carries the caller's own LIMIT.
3973
+ const callerLimit = args?.limit ?? this.defaultLimit;
3974
+ const bound = callerLimit === undefined ? undefined : this.paginationValue(callerLimit, 'limit');
3253
3975
  const speculativeDeferred = this.buildFindMany({
3254
3976
  ...args,
3255
- limit: batchSize + 1,
3977
+ limit: bound !== undefined && bound <= batchSize ? bound : batchSize + 1,
3978
+ // Only the caller's own bound makes this a page (completePageOrder).
3979
+ ...firstRowOnly(args),
3256
3980
  });
3257
3981
  const speculativeResult = await this.readWithTimeout(speculativeDeferred.sql, speculativeDeferred.params, args?.timeout, streamPreparedName);
3258
3982
  if (speculativeResult.rows.length <= batchSize) {
@@ -3276,22 +4000,100 @@ class QueryInterface {
3276
4000
  // transaction control of its own (`ambientTransaction`).
3277
4001
  const held = this.txScoped ? null : await this.acquireConnection();
3278
4002
  const client = held?.client ?? null;
3279
- const conn = client ?? {
4003
+ const base = client ?? {
3280
4004
  query: async (text, values) => (await this.pool.query(text, values)),
3281
4005
  };
4006
+ // The idle guard watches only a stream that holds a connection of its own
4007
+ // between pulls. Inside a caller's transaction the connection is theirs and
4008
+ // their transaction's timeout bounds it. On PostgreSQL the stream's BEGIN
4009
+ // also carries the server-side limit (see streamBackstopStatement), and the
4010
+ // guard keeps a live consumer's transaction from looking idle to it.
4011
+ const watched = held !== null && idleTimeoutMs > 0;
4012
+ const backstop = watched && this.dialect.capabilities.idleTransactionTimeout;
4013
+ let guard;
4014
+ const conn = watched
4015
+ ? (0, stream_idle_js_1.watchStreamConnection)(base, () => guard, backstop
4016
+ ? { begin: this.dialect.beginStatement(), statement: (0, stream_idle_js_1.streamBackstopStatement)(idleTimeoutMs) }
4017
+ : undefined)
4018
+ : base;
4019
+ // The cursor's statement carries the `sqlComment` like any other (the
4020
+ // speculative fetch above got it at the execute seam). Never named: a
4021
+ // cursor is declared by text.
4022
+ const streamSql = this.outgoing(deferred.sql, undefined, action).text;
4023
+ const stream = this.dialect.openStream(conn, streamSql, deferred.params, batchSize, {
4024
+ ambientTransaction: this.txScoped,
4025
+ });
4026
+ if (watched) {
4027
+ guard = new stream_idle_js_1.StreamIdleGuard(idle, idleTimeoutMs, () => (0, stream_idle_js_1.streamIdleExpiredError)(action, this.table, idleTimeoutMs, unit), () => this.expireIdleStream(stream, held, action, idleTimeoutMs, idle, unit), backstop ? () => base.query('SELECT 1') : undefined);
4028
+ }
3282
4029
  try {
3283
- yield* this.dialect.openStream(conn, deferred.sql, deferred.params, batchSize, {
3284
- ambientTransaction: this.txScoped,
3285
- });
4030
+ // Driven by hand rather than `yield*`, so an expired stream's cleanup can
4031
+ // run from the timer while this generator is suspended.
4032
+ while (true) {
4033
+ const step = await stream.next();
4034
+ if (step.done)
4035
+ break;
4036
+ yield step.value;
4037
+ if (idle.expired)
4038
+ throw idle.expired;
4039
+ }
3286
4040
  }
3287
4041
  catch (err) {
4042
+ if (err === idle.expired)
4043
+ throw err;
3288
4044
  // Wrap pg constraint errors so streaming surfaces typed errors like the rest of the API
3289
4045
  throw (0, errors_js_1.explainConnectionLoss)((0, errors_js_1.wrapPgError)(err), held?.checkout.lostWith);
3290
4046
  }
3291
4047
  finally {
3292
- held?.checkout.release();
4048
+ guard?.stop();
4049
+ if (guard?.expiry) {
4050
+ // The timer already closed the stream and released the connection;
4051
+ // wait for that so the connection is never handed back mid-cleanup.
4052
+ await guard.expiry;
4053
+ }
4054
+ else {
4055
+ try {
4056
+ // An early exit (`break`) closes the cursor here; after a full drain
4057
+ // or an error the dialect's generator has finished and this is a no-op.
4058
+ await stream.return(undefined);
4059
+ }
4060
+ catch (err) {
4061
+ // Only an early exit can reach this throw: after an error or a full
4062
+ // drain the dialect's generator has finished and return() resolves.
4063
+ // A failed CLOSE / COMMIT on an early exit must reach the caller, as
4064
+ // it did when this loop was a `yield*`.
4065
+ // biome-ignore lint/correctness/noUnsafeFinally: it replaces only an early-exit return, see above
4066
+ throw (0, errors_js_1.explainConnectionLoss)((0, errors_js_1.wrapPgError)(err), held?.checkout.lostWith);
4067
+ }
4068
+ finally {
4069
+ held?.checkout.release();
4070
+ }
4071
+ }
3293
4072
  }
3294
4073
  }
4074
+ /**
4075
+ * Close a stream whose consumer asked for nothing within its idle timeout:
4076
+ * run the dialect's own cleanup (PostgreSQL: CLOSE, COMMIT), release the
4077
+ * connection (with the cleanup's error, if any, so the pool discards it), and
4078
+ * log `stream-idle-timeout`. The log line matters: an abandoned consumer
4079
+ * never pulls again, so it never sees the E002 waiting for it.
4080
+ */
4081
+ async expireIdleStream(stream, held, action, timeoutMs, idle, unit) {
4082
+ let failure;
4083
+ try {
4084
+ await stream.return(undefined);
4085
+ }
4086
+ catch (err) {
4087
+ failure = err;
4088
+ }
4089
+ finally {
4090
+ held.checkout.release(failure === undefined ? undefined : failure instanceof Error ? failure : true);
4091
+ }
4092
+ (0, logger_js_1.logTo)(this.logger, 'warn', `${action} on "${this.table}" had no ${unit} requested for ${timeoutMs} ms, so Turbine closed its cursor ` +
4093
+ `and released its connection (${idle.rows} ${idle.rows === 1 ? 'row' : 'rows'} handed out before that). ` +
4094
+ 'An iterator that is kept and never finished, or a response stream torn down without calling return(), ' +
4095
+ 'leaves a stream like this. The next request on it throws TimeoutError (E002).', { code: 'stream-idle-timeout', table: this.table, operation: action, timeoutMs, rows: idle.rows });
4096
+ }
3295
4097
  /**
3296
4098
  * Stream rows from a findMany query using PostgreSQL cursors, one BATCH of
3297
4099
  * rows at a time.
@@ -3327,13 +4129,24 @@ class QueryInterface {
3327
4129
  * ```
3328
4130
  */
3329
4131
  async *findManyStreamBatches(args) {
4132
+ args = this.normalizeArgs(args);
4133
+ this.assertNoStreamLock(args, 'findManyStreamBatches');
3330
4134
  const parse = this.makeStreamRowParser(args);
3331
- for await (const batch of this.streamRaw(args, 'findManyStreamBatches')) {
4135
+ const idle = (0, stream_idle_js_1.newStreamIdleState)();
4136
+ for await (const batch of this.streamRaw(args, 'findManyStreamBatches', idle, 'batch')) {
3332
4137
  const parsed = new Array(batch.length);
3333
4138
  for (let i = 0; i < batch.length; i++) {
3334
4139
  parsed[i] = parse(batch[i]);
3335
4140
  }
4141
+ idle.rows += parsed.length;
4142
+ // `suspended` and `pulls` are the idle guard's whole view of the
4143
+ // consumer: two writes per yield, no timer work.
4144
+ idle.suspended = true;
3336
4145
  yield parsed;
4146
+ idle.suspended = false;
4147
+ idle.pulls++;
4148
+ if (idle.expired)
4149
+ throw idle.expired;
3337
4150
  }
3338
4151
  }
3339
4152
  /**
@@ -3372,14 +4185,25 @@ class QueryInterface {
3372
4185
  * ```
3373
4186
  */
3374
4187
  async *findManyStream(args) {
4188
+ args = this.normalizeArgs(args);
4189
+ this.assertNoStreamLock(args, 'findManyStream');
3375
4190
  const parse = this.makeStreamRowParser(args);
3376
4191
  // Parsed one row at a time, INSIDE the yield loop rather than a batch ahead
3377
4192
  // of it, so a consumer that breaks early still pays only for the rows it
3378
4193
  // took. That laziness is the reason this loop is not written over
3379
4194
  // `findManyStreamBatches`.
3380
- for await (const batch of this.streamRaw(args, 'findManyStream')) {
4195
+ const idle = (0, stream_idle_js_1.newStreamIdleState)();
4196
+ for await (const batch of this.streamRaw(args, 'findManyStream', idle, 'row')) {
3381
4197
  for (let i = 0; i < batch.length; i++) {
3382
- yield parse(batch[i]);
4198
+ const row = parse(batch[i]);
4199
+ idle.rows++;
4200
+ // See findManyStreamBatches: the idle guard's view of the consumer.
4201
+ idle.suspended = true;
4202
+ yield row;
4203
+ idle.suspended = false;
4204
+ idle.pulls++;
4205
+ if (idle.expired)
4206
+ throw idle.expired;
3383
4207
  }
3384
4208
  }
3385
4209
  }
@@ -3387,28 +4211,28 @@ class QueryInterface {
3387
4211
  // findFirst, like findMany but returns a single row or null
3388
4212
  // -------------------------------------------------------------------------
3389
4213
  async findFirst(args) {
3390
- // BEFORE the strategy branch, not only inside the builders, so the
3391
- // join-vs-batched decision and its dev warning see the same declared name
3392
- // every other stage does. `planAuto` splits `with` on the caller's keys and
3393
- // `runAutoSplit` carries them onward; each downstream consumer normalizes
3394
- // too, so this is not load-bearing for correctness (measured: removing it
3395
- // changes the warning text from the declared name back to the caller's and
3396
- // nothing else). It is here so the invariant holds at the seam rather than
3397
- // depending on every consumer to re-establish it.
3398
- args = this.normalizeArgs(args);
4214
+ // BEFORE the strategy branch, so the batched and auto paths below read the
4215
+ // same canonical args the builders do, and so an all-undefined `where` is
4216
+ // refused before anything runs (see normalizeArgs).
4217
+ args = this.normalizeArgs(args, 'findFirst');
4218
+ this.assertLockInTransaction(args, 'findFirst');
3399
4219
  return this.executeWithMiddleware('findFirst', (args ?? {}), async () => {
3400
4220
  if (args?.with) {
3401
4221
  const strategy = this.resolveLoadStrategy(args.relationLoadStrategy);
3402
4222
  // findFirst is findMany + LIMIT 1: batch the single base row, then load.
3403
4223
  if (strategy === 'batched') {
3404
- const rows = await this.runFindManyBatched({ ...args, limit: 1 });
4224
+ const rows = await this.runFindManyBatched({
4225
+ ...args,
4226
+ limit: 1,
4227
+ ...firstRowOnly(args),
4228
+ });
3405
4229
  return (rows[0] ?? null);
3406
4230
  }
3407
4231
  if (strategy === 'auto') {
3408
4232
  // findFirst is findMany + LIMIT 1: a one-row parent set.
3409
4233
  const split = this.planAuto(args.with, args.stableRelationOrder, false, 1);
3410
4234
  if (split) {
3411
- const rows = (await this.runAutoSplit({ ...args, limit: 1 }, split, false));
4235
+ const rows = (await this.runAutoSplit({ ...args, limit: 1, ...firstRowOnly(args) }, split, false));
3412
4236
  return (rows[0] ?? null);
3413
4237
  }
3414
4238
  }
@@ -3419,8 +4243,16 @@ class QueryInterface {
3419
4243
  });
3420
4244
  }
3421
4245
  buildFindFirst(args) {
4246
+ return this.buildFindFirstAs(args, 'findFirst');
4247
+ }
4248
+ /**
4249
+ * findFirst / findFirstOrThrow over findMany's builder with `LIMIT 1`.
4250
+ * `operation` names the method in the refusal of an all-undefined `where`.
4251
+ */
4252
+ buildFindFirstAs(args, operation) {
4253
+ args = this.normalizeArgs(args, operation);
3422
4254
  // Reuse findMany's SQL builder but force LIMIT 1
3423
- const findManyArgs = { ...args, limit: 1 };
4255
+ const findManyArgs = { ...args, limit: 1, ...firstRowOnly(args) };
3424
4256
  const deferred = this.buildFindMany(findManyArgs);
3425
4257
  return {
3426
4258
  sql: deferred.sql,
@@ -3430,12 +4262,15 @@ class QueryInterface {
3430
4262
  return rows.length > 0 ? rows[0] : null;
3431
4263
  },
3432
4264
  tag: `${this.table}.findFirst`,
4265
+ ...(deferred.rowLock ? { rowLock: true } : {}),
3433
4266
  };
3434
4267
  }
3435
4268
  // -------------------------------------------------------------------------
3436
4269
  // findFirstOrThrow, like findFirst but throws if no record found
3437
4270
  // -------------------------------------------------------------------------
3438
4271
  async findFirstOrThrow(args) {
4272
+ args = this.normalizeArgs(args, 'findFirstOrThrow');
4273
+ this.assertLockInTransaction(args, 'findFirstOrThrow');
3439
4274
  return this.executeWithMiddleware('findFirstOrThrow', (args ?? {}), async () => {
3440
4275
  const deferred = this.buildFindFirstOrThrow(args);
3441
4276
  const result = await this.readWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
@@ -3443,7 +4278,8 @@ class QueryInterface {
3443
4278
  });
3444
4279
  }
3445
4280
  buildFindFirstOrThrow(args) {
3446
- const inner = this.buildFindFirst(args);
4281
+ args = this.normalizeArgs(args, 'findFirstOrThrow');
4282
+ const inner = this.buildFindFirstAs(args, 'findFirstOrThrow');
3447
4283
  return {
3448
4284
  sql: inner.sql,
3449
4285
  params: inner.params,
@@ -3459,12 +4295,15 @@ class QueryInterface {
3459
4295
  return row;
3460
4296
  },
3461
4297
  tag: `${this.table}.findFirstOrThrow`,
4298
+ ...(inner.rowLock ? { rowLock: true } : {}),
3462
4299
  };
3463
4300
  }
3464
4301
  // -------------------------------------------------------------------------
3465
4302
  // findUniqueOrThrow, like findUnique but throws if no record found
3466
4303
  // -------------------------------------------------------------------------
3467
4304
  async findUniqueOrThrow(args) {
4305
+ args = this.normalizeArgs(args);
4306
+ this.assertLockInTransaction(args, 'findUniqueOrThrow');
3468
4307
  return this.executeWithMiddleware('findUniqueOrThrow', args, async () => {
3469
4308
  const deferred = this.buildFindUniqueOrThrow(args);
3470
4309
  const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
@@ -3472,6 +4311,7 @@ class QueryInterface {
3472
4311
  });
3473
4312
  }
3474
4313
  buildFindUniqueOrThrow(args) {
4314
+ args = this.normalizeArgs(args);
3475
4315
  const inner = this.buildFindUnique(args);
3476
4316
  return {
3477
4317
  sql: inner.sql,
@@ -3488,12 +4328,14 @@ class QueryInterface {
3488
4328
  return row;
3489
4329
  },
3490
4330
  tag: `${this.table}.findUniqueOrThrow`,
4331
+ ...(inner.rowLock ? { rowLock: true } : {}),
3491
4332
  };
3492
4333
  }
3493
4334
  // -------------------------------------------------------------------------
3494
4335
  // create
3495
4336
  // -------------------------------------------------------------------------
3496
4337
  async create(args) {
4338
+ args = this.normalizeArgs(args);
3497
4339
  return this.executeWithMiddleware('create', args, async () => {
3498
4340
  if ((0, nested_write_js_1.hasRelationFields)(args.data, this.tableMeta)) {
3499
4341
  return this.nestedCreate(args);
@@ -3506,16 +4348,55 @@ class QueryInterface {
3506
4348
  // createMany, uses UNNEST for performance
3507
4349
  // -------------------------------------------------------------------------
3508
4350
  async createMany(args) {
4351
+ args = this.normalizeArgs(args);
3509
4352
  return this.executeWithMiddleware('createMany', args, async () => {
3510
- const deferred = this.buildCreateMany(args);
3511
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
3512
- return deferred.transform(result);
4353
+ const projection = this.resolveWriteProjection(args);
4354
+ const base = args;
4355
+ // One statement when the rows fit the engine's per-statement limits,
4356
+ // else chunks that do, all in one transaction so the rows land together
4357
+ // or not at all. (`buildCreateMany`, one statement for a pipeline,
4358
+ // refuses instead.)
4359
+ const { chunks, rowsPerChunk } = writesMod.planCreateMany(this.ctx, base, projection);
4360
+ const run = async () => {
4361
+ // An engine that applies two rows whose onConflict keys are equal only
4362
+ // under the column's collation (SQLite's NOCASE) as an insert and an
4363
+ // update of one row, in one statement, is caught by the transform (the
4364
+ // row comes back twice), so the statement runs in a transaction that
4365
+ // refusal rolls back. A reselect write already runs in one.
4366
+ const appliesRepeatedKeys = base.onConflict !== undefined &&
4367
+ this.dialect.capabilities.bulkUpsertAppliesRepeatedKeys &&
4368
+ this.dialect.resultStrategy !== 'reselect';
4369
+ if (chunks.length === 1 && !appliesRepeatedKeys) {
4370
+ return this.executeMutation(chunks[0], args.timeout);
4371
+ }
4372
+ const reselects = this.dialect.resultStrategy === 'reselect';
4373
+ return this.inOneTransaction(async (exec) => {
4374
+ const rows = [];
4375
+ for (const chunk of chunks) {
4376
+ const result = reselects && chunk.reselect
4377
+ ? await chunk.reselect(exec)
4378
+ : await exec(chunk.sql, chunk.params, chunk.preparedName);
4379
+ rows.push(...chunk.transform(result));
4380
+ }
4381
+ return rows;
4382
+ }, args.timeout, `createMany of ${base.data.length} rows`, `Insert at most ${rowsPerChunk} rows per call, which fits one statement, or pass a pool that can check ` +
4383
+ 'out a connection.');
4384
+ };
4385
+ if (base.onConflict === undefined)
4386
+ return run();
4387
+ try {
4388
+ return await run();
4389
+ }
4390
+ catch (err) {
4391
+ throw writesMod.explainConflictKeyCollision(this.ctx, base, err);
4392
+ }
3513
4393
  });
3514
4394
  }
3515
4395
  // -------------------------------------------------------------------------
3516
4396
  // update
3517
4397
  // -------------------------------------------------------------------------
3518
4398
  async update(args) {
4399
+ args = this.normalizeArgs(args);
3519
4400
  return this.executeWithMiddleware('update', args, async () => {
3520
4401
  if ((0, nested_write_js_1.hasRelationFields)(args.data, this.tableMeta)) {
3521
4402
  return this.nestedUpdate(args);
@@ -3628,6 +4509,7 @@ class QueryInterface {
3628
4509
  // delete
3629
4510
  // -------------------------------------------------------------------------
3630
4511
  async delete(args) {
4512
+ args = this.normalizeArgs(args);
3631
4513
  return this.executeWithMiddleware('delete', args, async () => {
3632
4514
  const deferred = this.buildDelete(args);
3633
4515
  return this.executeMutation(deferred, args.timeout);
@@ -3637,7 +4519,11 @@ class QueryInterface {
3637
4519
  // upsert, INSERT ... ON CONFLICT ... DO UPDATE
3638
4520
  // -------------------------------------------------------------------------
3639
4521
  async upsert(args) {
4522
+ args = this.normalizeArgs(args);
3640
4523
  return this.executeWithMiddleware('upsert', args, async () => {
4524
+ // First: the nested-write branch below decides before upsertLookupReason
4525
+ // would have refused it.
4526
+ writesMod.assertNoOptimisticLock(this.ctx, args, 'upsert');
3641
4527
  if (this.upsertNeedsLookup(args)) {
3642
4528
  return this.upsertLookupFirst(args);
3643
4529
  }
@@ -3652,6 +4538,7 @@ class QueryInterface {
3652
4538
  * an upsert or has to run the array inside a transaction.
3653
4539
  */
3654
4540
  upsertNeedsLookup(args) {
4541
+ args = this.normalizeArgs(args);
3655
4542
  return ((0, nested_write_js_1.hasRelationFields)((args.create ?? {}), this.tableMeta) ||
3656
4543
  (0, nested_write_js_1.hasRelationFields)((args.update ?? {}), this.tableMeta) ||
3657
4544
  writesMod.upsertLookupReason(this.ctx, args) !== null);
@@ -3680,7 +4567,11 @@ class QueryInterface {
3680
4567
  async upsertLookupFirst(args) {
3681
4568
  const where = (args.where ?? {});
3682
4569
  const create = (args.create ?? {});
3683
- const update = (args.update ?? {});
4570
+ // `expr.excluded(field)` in `update` reads the incoming row, which here is
4571
+ // `create`: resolved to create's value before the update compiles, as the
4572
+ // single-statement path does.
4573
+ const update = ((0, expr_js_1.resolveExcludedFromCreate)(this.ctx, (args.update ?? {}), create) ??
4574
+ {});
3684
4575
  (0, compound_unique_js_1.assertMutationWhereIdentifiesOneRow)(this.tableMeta, this.table, (0, compound_unique_js_1.expandCompoundUniqueWhere)(this.tableMeta, where), 'upsert');
3685
4576
  this.resolveWriteProjection(args);
3686
4577
  // An undefined option is an absent one on every call below.
@@ -3714,6 +4605,7 @@ class QueryInterface {
3714
4605
  // updateMany, UPDATE ... WHERE ... returning count
3715
4606
  // -------------------------------------------------------------------------
3716
4607
  async updateMany(args) {
4608
+ args = this.normalizeArgs(args);
3717
4609
  return this.executeWithMiddleware('updateMany', args, async () => {
3718
4610
  const deferred = this.buildUpdateMany(args);
3719
4611
  const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
@@ -3724,6 +4616,7 @@ class QueryInterface {
3724
4616
  // deleteMany, DELETE ... WHERE ... returning count
3725
4617
  // -------------------------------------------------------------------------
3726
4618
  async deleteMany(args) {
4619
+ args = this.normalizeArgs(args);
3727
4620
  return this.executeWithMiddleware('deleteMany', args, async () => {
3728
4621
  const deferred = this.buildDeleteMany(args);
3729
4622
  const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
@@ -3734,6 +4627,7 @@ class QueryInterface {
3734
4627
  // count
3735
4628
  // -------------------------------------------------------------------------
3736
4629
  async count(args) {
4630
+ args = this.normalizeArgs(args);
3737
4631
  return this.executeWithMiddleware('count', (args ?? {}), async () => {
3738
4632
  const deferred = this.buildCount(args);
3739
4633
  const result = await this.readWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
@@ -3741,6 +4635,7 @@ class QueryInterface {
3741
4635
  });
3742
4636
  }
3743
4637
  buildCount(args) {
4638
+ args = this.normalizeArgs(args);
3744
4639
  this.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args?.skipGlobalFilters);
3745
4640
  const effWhere = this.mergeGlobalFilter(args?.where);
3746
4641
  const hasWhere = effWhere !== undefined;
@@ -3778,6 +4673,7 @@ class QueryInterface {
3778
4673
  * that cannot be typed, so those columns are not projected onto the row type.
3779
4674
  */
3780
4675
  async groupBy(args) {
4676
+ args = this.normalizeArgs(args);
3781
4677
  return this.executeWithMiddleware('groupBy', args, async () => {
3782
4678
  const deferred = this.buildGroupBy(args);
3783
4679
  const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
@@ -3788,15 +4684,16 @@ class QueryInterface {
3788
4684
  // Aggregate / groupBy compilation (extracted to aggregates.ts).
3789
4685
  // ---------------------------------------------------------------------------
3790
4686
  buildGroupBy(args) {
3791
- return aggMod.buildGroupBy(this.ctx, args);
4687
+ return aggMod.buildGroupBy(this.ctx, this.normalizeArgs(args));
3792
4688
  }
3793
4689
  buildAggregate(args) {
3794
- return aggMod.buildAggregate(this.ctx, args);
4690
+ return aggMod.buildAggregate(this.ctx, this.normalizeArgs(args));
3795
4691
  }
3796
4692
  // -------------------------------------------------------------------------
3797
4693
  // aggregate, standalone aggregation without groupBy
3798
4694
  // -------------------------------------------------------------------------
3799
4695
  async aggregate(args) {
4696
+ args = this.normalizeArgs(args);
3800
4697
  return this.executeWithMiddleware('aggregate', args, async () => {
3801
4698
  const deferred = this.buildAggregate(args);
3802
4699
  const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
@@ -3914,14 +4811,15 @@ class QueryInterface {
3914
4811
  }
3915
4812
  return relationsMod.planFlattenWith(this.ctx, this.table, withClause, includePii);
3916
4813
  }
3917
- /** Dev-only once-only note that `'flatten'` was refused for the whole query. */
4814
+ /** Advice, once per reason: `'flatten'` was refused for the whole query. */
3918
4815
  warnFlattenBlocked(reason) {
3919
- if (process.env.NODE_ENV === 'production')
4816
+ const log = this.advice;
4817
+ if (!log)
3920
4818
  return;
3921
4819
  if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.flattenFallback, `${this.table}|query|${reason}`))
3922
4820
  return;
3923
- console.warn(`[turbine] relationLoadStrategy: 'flatten' did not engage on "${this.table}": ${reason}. ` +
3924
- 'Every relation loads via the correlated subquery instead (same rows, same values, different plan).');
4821
+ (0, logger_js_1.logTo)(log, 'warn', `relationLoadStrategy: 'flatten' did not engage on "${this.table}": ${reason}. ` +
4822
+ 'Every relation loads via the correlated subquery instead (same rows, same values, different plan).', { code: 'flatten-blocked', table: this.table, reason });
3925
4823
  }
3926
4824
  /**
3927
4825
  * Convert a field name to its snake_case column name (unquoted, for non-SQL
@@ -4056,8 +4954,8 @@ class QueryInterface {
4056
4954
  // To-many relation orderBy (`{ posts: { _count } }`) uses the same count
4057
4955
  // subquery as `_count`: mirror its global-filter params. Pick-row
4058
4956
  // ordering mirrors its full param chain (by-path / global filter /
4059
- // pick.where / pick.orderBy paths). To-one relation orderBy carries the
4060
- // target's global filter once per ordered column.
4957
+ // pick.where / pick.orderBy paths). To-one relation orderBy is mirrored
4958
+ // by running its build (see collectToOneRelationOrderParams).
4061
4959
  if (this.isRelationOrderByValue(dir)) {
4062
4960
  // Mirrors the build path's resolution, so a cache HIT binds params for
4063
4961
  // the same relation the cached SQL was built from.
@@ -4070,9 +4968,7 @@ class QueryInterface {
4070
4968
  this.collectRelationCountParams(relDef, params);
4071
4969
  }
4072
4970
  else if (relDef) {
4073
- for (const _col of Object.keys(dir)) {
4074
- this.collectTargetGlobalFilterAlias(relDef.to, params);
4075
- }
4971
+ relationsMod.collectToOneRelationOrderParams(this.ctx, key, dir, params);
4076
4972
  }
4077
4973
  }
4078
4974
  }
@@ -4144,10 +5040,10 @@ class QueryInterface {
4144
5040
  * one, is the acknowledgement, and the warning exists to surface an
4145
5041
  * unacknowledged trade rather than to nag.
4146
5042
  *
4147
- * NOT DEV-ONLY, unlike every other warning in this codebase, and deliberately
4148
- * so. Production is exactly where a destructive write commits and where the
4149
- * row cannot be recovered afterwards, so a warning that goes quiet under
4150
- * `NODE_ENV=production` is silent in the only place it matters. The cost is
5043
+ * NOT ADVICE: it fires whatever `diagnostics` says, deliberately. Production
5044
+ * is exactly where a destructive write commits and where the row cannot be
5045
+ * recovered afterwards, so a warning that goes quiet in production mode is
5046
+ * silent in the only place it matters. The cost is
4151
5047
  * bounded to the point of irrelevance: once per process per field, and only
4152
5048
  * on a row that actually held an infinity.
4153
5049
  *
@@ -4160,7 +5056,7 @@ class QueryInterface {
4160
5056
  return;
4161
5057
  if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.temporalInfinity, `${table}.${field}`))
4162
5058
  return;
4163
- console.warn(`[turbine] ${table}.${field} holds the Postgres value \`infinity\` (or \`-infinity\`). JavaScript has ` +
5059
+ (0, logger_js_1.logTo)(this.logger, 'warn', `${table}.${field} holds the Postgres value \`infinity\` (or \`-infinity\`). JavaScript has ` +
4164
5060
  'no Date for it, so it reads as the JS number `Infinity` / `-Infinity` on a field the generated ' +
4165
5061
  'types declare as `Date`: `.toISOString()` and `.getTime()` throw a TypeError on these rows, and ' +
4166
5062
  '`JSON.stringify` still renders them null because JSON has no infinity literal. The number is the ' +
@@ -4170,7 +5066,7 @@ class QueryInterface {
4170
5066
  "cannot tell infinity, -infinity and NULL apart. Set `temporalInfinity: 'preserve'` to confirm this " +
4171
5067
  "reading and silence the warning, or `'null'` to read null instead, accepting that a stored " +
4172
5068
  'infinity then looks exactly like a stored NULL and a read-modify-write over a nullable column ' +
4173
- 'stores SQL NULL, destroying the value with no error.');
5069
+ 'stores SQL NULL, destroying the value with no error.', { code: 'temporal-infinity', table, field });
4174
5070
  }
4175
5071
  /**
4176
5072
  * Apply the configured reading to the infinity elements of a temporal ARRAY
@@ -4250,6 +5146,64 @@ class QueryInterface {
4250
5146
  // UTC so results don't depend on the server's time zone.
4251
5147
  return this.utcTimestamps ? (0, utils_js_1.parseDbDate)(String(value)) : new Date(value);
4252
5148
  }
5149
+ /** The non-temporal decode kinds of `table`'s columns, by column and by field name. */
5150
+ getCellKinds(table, meta) {
5151
+ let kinds = this.cellKindCache.get(table);
5152
+ if (!kinds) {
5153
+ kinds = new Map();
5154
+ for (const column of meta.columns) {
5155
+ // Metadata may carry the type on the column or only in the table map.
5156
+ const pgType = column.pgType ?? meta.pgTypes?.[column.name];
5157
+ if (typeof pgType !== 'string')
5158
+ continue;
5159
+ let kind = cellKindOf(pgType, this.schema.enums);
5160
+ const decode = kind === CELL_PLAIN ? this.dialect.decodeColumnValue?.(pgType) : undefined;
5161
+ if (decode !== undefined) {
5162
+ kind = CELL_DIALECT;
5163
+ let decoders = this.dialectDecodeCache.get(table);
5164
+ if (!decoders) {
5165
+ decoders = new Map();
5166
+ this.dialectDecodeCache.set(table, decoders);
5167
+ }
5168
+ decoders.set(column.name, decode);
5169
+ decoders.set(column.field, decode);
5170
+ }
5171
+ if (kind === CELL_PLAIN)
5172
+ continue;
5173
+ kinds.set(column.name, kind);
5174
+ kinds.set(column.field, kind);
5175
+ }
5176
+ this.cellKindCache.set(table, kinds);
5177
+ }
5178
+ return kinds;
5179
+ }
5180
+ /** One cell's decode, for every kind but {@link CELL_PLAIN}. */
5181
+ decodeCell(kind, value, table, field) {
5182
+ switch (kind) {
5183
+ case CELL_TEMPORAL:
5184
+ return this.decodeTemporalCell(value, table, field);
5185
+ case CELL_FLOAT:
5186
+ return (0, utils_js_1.readJsonFloat)(value);
5187
+ case CELL_FLOAT_ARRAY:
5188
+ return (0, utils_js_1.readJsonFloatArray)(value);
5189
+ case CELL_TEXT_ARRAY:
5190
+ return (0, utils_js_1.readTextArray)(value);
5191
+ case CELL_DIALECT:
5192
+ return this.decodeDialectCell(value, table, field);
5193
+ default:
5194
+ return value;
5195
+ }
5196
+ }
5197
+ /**
5198
+ * A {@link CELL_DIALECT} cell: the dialect's decode for the column's declared
5199
+ * type, recorded by {@link getCellKinds} when it classified the column.
5200
+ */
5201
+ decodeDialectCell(value, table, field) {
5202
+ if (value === null || value === undefined)
5203
+ return value;
5204
+ const decode = this.dialectDecodeCache.get(table)?.get(field);
5205
+ return decode ? decode(value) : value;
5206
+ }
4253
5207
  /**
4254
5208
  * Build the decode plan for one exact column list, and remember it as this
4255
5209
  * table's most recent shape so the next row of the same result set hits the
@@ -4273,16 +5227,19 @@ class QueryInterface {
4273
5227
  // camelCase-keyed date fields, so nested json_build_object rows (whose
4274
5228
  // keys are already camelCase) get the same Date coercion as top-level rows.
4275
5229
  const camelDateFields = this.getCamelDateFields(table, meta);
5230
+ const cellKinds = this.getCellKinds(table, meta);
4276
5231
  const cols = keys.slice();
4277
5232
  const fields = [];
4278
- const dates = [];
5233
+ const kinds = [];
4279
5234
  for (const col of cols) {
4280
5235
  const field = reverseMap[col] ?? col; // fall back to raw col name, not regex
4281
5236
  fields.push(field);
4282
5237
  // Top-level rows are snake_case (dateCols); nested rows are camelCase.
4283
- dates.push(dateCols.has(col) || camelDateFields.has(field));
5238
+ kinds.push(dateCols.has(col) || camelDateFields.has(field)
5239
+ ? CELL_TEMPORAL
5240
+ : (cellKinds.get(col) ?? cellKinds.get(field) ?? CELL_PLAIN));
4284
5241
  }
4285
- plan = { cols, fields, dates };
5242
+ plan = { cols, fields, kinds };
4286
5243
  this.rowPlanCache.set(shapeKey, plan);
4287
5244
  }
4288
5245
  this.rowPlanLast.set(table, plan);
@@ -4326,11 +5283,16 @@ class QueryInterface {
4326
5283
  const plan = last !== undefined && QueryInterface.rowPlanMatches(last, keys)
4327
5284
  ? last
4328
5285
  : this.buildRowDecodePlan(table, meta, keys);
4329
- const { cols, fields, dates } = plan;
5286
+ const { cols, fields, kinds } = plan;
4330
5287
  for (let i = 0; i < cols.length; i++) {
4331
5288
  const field = fields[i];
4332
5289
  const value = row[cols[i]];
4333
- parsed[field] = dates[i] ? this.decodeTemporalCell(value, table, field) : value;
5290
+ const kind = kinds[i];
5291
+ // A float from the driver is already a number; only JSON's strings need reading.
5292
+ parsed[field] =
5293
+ kind === CELL_PLAIN || (kind === CELL_FLOAT && typeof value !== 'string')
5294
+ ? value
5295
+ : this.decodeCell(kind, value, table, field);
4334
5296
  }
4335
5297
  }
4336
5298
  else {