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
@@ -67,16 +67,22 @@
67
67
  * - Fluent relation chaining (`prisma.user.findUnique().posts()`).
68
68
  * - Accelerate / Pulse / driver-adapter preview features, the Mongo API, and the
69
69
  * `prisma migrate`/`db` CLI family (Turbine ships its own migrations).
70
- * - **Inclusive bare cursors whose field is not the sort key**, see
71
- * {@link translateCursor}. A bare Prisma cursor is inclusive; translating it
72
- * correctly needs the anchor row's sort-key value. When the cursor field is
73
- * the single `orderBy` field (or the cursor is the single-column PK with no
74
- * `orderBy`) it compiles to a `gte`/`lte` keyset predicate; **otherwise it
75
- * throws** an {@link UnsupportedFeatureError} rather than emit an off-by-one
76
- * page. Pair the cursor with `skip: 1` (Prisma's idiom) for the exact
77
- * exclusive-cursor + `offset` translation.
78
- * - **Negative `take`** (take-from-end) and **`skip` on a nested relation
79
- * include** throw, Turbine's `with` clause has no offset and no reverse-take.
70
+ * - **Negative `take`** (take-from-end) throws: Turbine has no reverse-take.
71
+ * A **`cursor` on a nested relation include** throws: `with` has no cursor
72
+ * (`take` / `skip` there page each parent's rows, as in Prisma).
73
+ * - **A filtered relation count** (`_count: { select: { posts: { where } } }`)
74
+ * and **`distinct` on a nested include** throw `UnsupportedFeatureError`:
75
+ * core counts and includes a relation whole, so honoring either would mean
76
+ * returning a wrong number or duplicate rows. The message names the
77
+ * alternative (a `count` / `findMany` on the related model).
78
+ * - **Nothing is accepted and dropped.** Every option on an included relation
79
+ * and every key under `_count` is translated, refused by name (the two
80
+ * above, `cursor`), or refused as unknown (`ValidationError`, with a
81
+ * suggestion): a misspelled `wher`, a Turbine spelling like `limit`, a
82
+ * relation `_count` does not know. The same holds for `$transaction` options
83
+ * (`maxWait` is accepted, warned about once, and not applied: the pool's
84
+ * `connectionTimeoutMs` bounds the wait) and for the keys of a nested write's
85
+ * `{ where, data }` / `{ where, create, update }` payloads.
80
86
  * - **`limit` on `updateMany` / `deleteMany`** (Prisma 6.7+) throws. Turbine has
81
87
  * no row-bounded mass mutation, and dropping a SAFETY BOUND with a warning
82
88
  * would turn "change at most 10 rows" into "change every matching row".
@@ -87,9 +93,10 @@
87
93
  * either (or any `include` on a write) THROWS, since a write returns its own
88
94
  * row only. `omit` on READS is translated to core's own `omit`, see
89
95
  * {@link translateOmit}.
90
- * - **`select` on `count`** and **`orderBy` / `cursor` / `take` / `skip` on
91
- * `aggregate`** are accepted and IGNORED (they are legitimate Prisma, so they
92
- * never warn); a plain number / the aggregate comes back.
96
+ * - **`take` / `skip` / `cursor` on `count` and `aggregate`** throw: Prisma
97
+ * computes those over one page of rows and Turbine has no paged count. An
98
+ * `orderBy` alone changes nothing there and is accepted. `count({ select })`
99
+ * returns Prisma's per-field object (`{ _all, field }`).
93
100
  *
94
101
  * ## Type dependencies (0.41.0)
95
102
  *
@@ -119,11 +126,15 @@
119
126
  * });
120
127
  * ```
121
128
  */
129
+ import { resolveDiagnostics } from './diagnostics.js';
122
130
  import { TurbineError, TurbineErrorCode, UnsupportedFeatureError, ValidationError, wrapPgError } from './errors.js';
131
+ import { CLIENT_LOG_CONTEXT, consoleLogger, logTo } from './logger.js';
123
132
  import { createManyShapeRuns } from './nested-write.js';
124
133
  import { fingerprintPrismaSchema } from './prisma-schema-fingerprint.js';
134
+ import { isPlainRecord, RELATION_FILTER_WRAPPER_SET, setOwn } from './query/filters.js';
125
135
  import { AGGREGATE_OPTIONS, applyNativeOptions, COUNT_OPTIONS, CREATE_MANY_OPTIONS, CREATE_OPTIONS, DELETE_MANY_OPTIONS, DELETE_OPTIONS, FIND_MANY_OPTIONS, FIND_UNIQUE_OPTIONS, GROUP_BY_OPTIONS, optionKeysOfKind, UNSAFE, UPDATE_MANY_OPTIONS, UPDATE_OPTIONS, UPSERT_OPTIONS, } from './query/index.js';
126
- import { markInternalCombinator, suggestKey } from './query/utils.js';
136
+ import { CURSOR_INCLUSIVE } from './query/normalize-args.js';
137
+ import { ownLookup, suggestKey } from './query/utils.js';
127
138
  import { shouldWarnOnce, WARN_NS } from './query/warn-registry.js';
128
139
  import { assertWhereDepth, MAX_WHERE_DEPTH } from './query/where-compile.js';
129
140
  // ---------------------------------------------------------------------------
@@ -233,19 +244,21 @@ function timeStringToDate(v) {
233
244
  return new Date(Date.UTC(1970, 0, 1, Number(m[1]), Number(m[2]), Number(m[3]), ms));
234
245
  }
235
246
  function buildLookups(ctx, mm) {
236
- const reverseFields = {};
247
+ // Keyed by field and relation names, so prototype-less: a field called
248
+ // `constructor` is an own key here, never Object.prototype's member.
249
+ const reverseFields = Object.create(null);
237
250
  let identityFields = true;
238
251
  for (const [prismaField, turbineField] of Object.entries(mm.fields)) {
239
252
  reverseFields[turbineField] = prismaField;
240
253
  if (prismaField !== turbineField)
241
254
  identityFields = false;
242
255
  }
243
- const reverseRelations = {};
256
+ const reverseRelations = Object.create(null);
244
257
  for (const [prismaRel, rel] of Object.entries(mm.relations)) {
245
258
  reverseRelations[rel.name] = { prismaName: prismaRel, cardinality: rel.cardinality };
246
259
  }
247
260
  const timeFields = new Set();
248
- for (const col of ctx.schema.tables[mm.table]?.columns ?? []) {
261
+ for (const col of ownLookup(ctx.schema.tables, mm.table)?.columns ?? []) {
249
262
  if (TIME_PG_TYPES.has(col.pgType))
250
263
  timeFields.add(col.field);
251
264
  }
@@ -260,62 +273,18 @@ function lookupsFor(ctx, mm) {
260
273
  return l;
261
274
  }
262
275
  /** Resolve a turbine relation's target Prisma model map (for nested translation). */
263
- /**
264
- * A `where` fragment no row satisfies, for Prisma's empty `OR`.
265
- *
266
- * `{ <key>: { in: [] } }`, which core compiles to `= ANY($n)` with an empty
267
- * array: false for every row, NULL-valued ones included, and one ordinary bound
268
- * param rather than a literal spliced into the SQL. The column is the first
269
- * primary-key column, else the table's first column, so it is always a real
270
- * column of this table and the predicate stays a plain scalar comparison the
271
- * cache fingerprints like any other.
272
- *
273
- * A table this layer cannot name a column of THROWS. The fallback used to be
274
- * `{}`, an empty `where` fragment - which is not "no rows" but its exact
275
- * opposite, EVERY row, and it would have been merged in silently beside the
276
- * caller's other predicates. A sentinel builder that cannot build a sentinel
277
- * has not succeeded, and the one shape it exists to compile is the one where
278
- * answering "all of them" instead of "none of them" is worst.
279
- */
280
- function matchNothing(ctx, mm) {
281
- const meta = ctx.schema.tables[mm.table];
282
- const column = meta?.primaryKey?.[0] ?? meta?.allColumns?.[0];
283
- if (!meta || !column) {
284
- throw new ValidationError(`An empty \`OR\` on model ${mm.table} means "no rows", and compiling that needs one real column ` +
285
- `of the table, but the schema metadata for "${mm.table}" lists none. Regenerate the client ` +
286
- '(`npx turbine generate`) so the table carries its columns, or drop the empty `OR`.');
287
- }
288
- return { [meta.reverseColumnMap?.[column] ?? column]: { in: [] } };
289
- }
290
276
  function relTargetModel(ctx, mm, turbineRel) {
291
- const rd = ctx.schema.tables[mm.table]?.relations?.[turbineRel];
277
+ const relations = ownLookup(ctx.schema.tables, mm.table)?.relations;
278
+ const rd = relations ? ownLookup(relations, turbineRel) : undefined;
292
279
  if (!rd)
293
280
  return undefined;
294
281
  const modelName = ctx.tableToModel.get(rd.to);
295
- return modelName ? ctx.map.models[modelName] : undefined;
282
+ return modelName ? ownLookup(ctx.map.models, modelName) : undefined;
296
283
  }
297
284
  // ---------------------------------------------------------------------------
298
285
  // Argument translation
299
286
  // ---------------------------------------------------------------------------
300
287
  const COMBINATORS = new Set(['AND', 'OR', 'NOT']);
301
- /**
302
- * The to-many half of the relation-filter wrapper list.
303
- *
304
- * DRIFT RISK, and this is the third copy of that list in the tree. The canonical
305
- * NAMED one is `RELATION_FILTER_WRAPPERS` in `cli/pii-predicate-guard.ts`
306
- * (`some`/`none`/`every`/`is`/`isNot`), and a second is inlined in
307
- * `query/where-compile.ts` as `'some' in x || 'every' in x || …`. This copy is
308
- * the worst of the three because it is SPLIT: the to-many quantifiers are here,
309
- * and `is` / `isNot` are two inline `k === …` tests in
310
- * {@link translateRelationFilter}, so half of it does not read as a list and a
311
- * grep for the list will not find it. A wrapper the compiler learns and this set
312
- * does not is a relation body that reaches core untranslated, i.e. with Prisma
313
- * field names still in it.
314
- *
315
- * The fix is an export from `query/filters.ts` that all three import; until that
316
- * exists, these comments are what hold them in step.
317
- */
318
- const RELATION_QUANTIFIERS = new Set(['some', 'every', 'none']);
319
288
  /**
320
289
  * Refuse a translation walk that has nested past {@link MAX_WHERE_DEPTH}.
321
290
  *
@@ -355,16 +324,8 @@ function assertTranslateDepth(depth, clause) {
355
324
  `query needs, and an unbounded walk over caller-supplied nesting is a stack-overflow surface, so it is ` +
356
325
  `refused. If this is a generated argument, flatten it: a single array of N entries is one level, not N.`);
357
326
  }
358
- /** Whether a value is a plain object usable as a compound-unique selector. */
359
- function isPlainObject(v) {
360
- return (typeof v === 'object' &&
361
- v !== null &&
362
- !Array.isArray(v) &&
363
- !(v instanceof Date) &&
364
- !(typeof Buffer !== 'undefined' && Buffer.isBuffer(v)));
365
- }
366
327
  function renameField(mm, prismaField) {
367
- return mm.fields[prismaField] ?? prismaField;
328
+ return ownLookup(mm.fields, prismaField) ?? prismaField;
368
329
  }
369
330
  /** The turbine arg surface each operation compiles into. */
370
331
  const OPERATION_TABLES = {
@@ -484,8 +445,8 @@ const KNOWN_KEYS = (() => {
484
445
  return out;
485
446
  })();
486
447
  /**
487
- * Dev-mode notice for a key on a delegate's args object that this operation has
488
- * no meaning for.
448
+ * Per-query advice for a key on a delegate's args object that this operation
449
+ * has no meaning for.
489
450
  *
490
451
  * An unrecognized key is silently ignored (JavaScript objects have no schema),
491
452
  * which makes a typo, a turbine spelling, and a genuinely missing feature all
@@ -497,13 +458,14 @@ const KNOWN_KEYS = (() => {
497
458
  * into a failing one on upgrade over one stray key, and a key from a Prisma
498
459
  * major newer than {@link PRISMA_ARG_KEYS} would be exactly that key. The whole
499
460
  * body is wrapped so a hostile or exotic args object (a Proxy whose `ownKeys`
500
- * throws) can never be the reason a query fails. Dev-only and once per
501
- * `model.operation.key` per process, like every other advisory here.
461
+ * throws) can never be the reason a query fails. Off when the client's
462
+ * `diagnostics` turn advice off, and once per `model.operation.key` per
463
+ * process, like every other advisory here.
502
464
  */
503
- function warnUnknownQueryOptions(model, op, args) {
504
- if (process.env.NODE_ENV === 'production')
465
+ function warnUnknownQueryOptions(log, model, op, args) {
466
+ if (!log.diagnostics.advice)
505
467
  return;
506
- if (!isPlainObject(args))
468
+ if (!isPlainRecord(args))
507
469
  return;
508
470
  try {
509
471
  const known = KNOWN_KEYS[op];
@@ -519,13 +481,19 @@ function warnUnknownQueryOptions(model, op, args) {
519
481
  if (!shouldWarnOnce(WARN_NS.unknownQueryOption, `${model}.${op}.${key}`))
520
482
  continue;
521
483
  if (alias) {
522
- console.warn(`[turbine] prisma-compat: "${key}" is Turbine's spelling and is ignored here;` +
523
- ` prisma-compat takes Prisma's "${alias}". (${model}.${op})`);
484
+ logTo(log.logger, 'warn', `prisma-compat: "${key}" is Turbine's spelling and is ignored here;` +
485
+ ` prisma-compat takes Prisma's "${alias}". (${model}.${op})`, { code: 'prisma-compat-native-spelling', table: model, operation: op, key, prismaKey: alias });
524
486
  continue;
525
487
  }
526
488
  const suggestion = suggestKey(key, known);
527
- console.warn(`[turbine] prisma-compat: unknown option "${key}" in ${model}.${op}(), it is ignored.` +
528
- (suggestion ? ` Did you mean "${suggestion}"?` : ''));
489
+ logTo(log.logger, 'warn', `prisma-compat: unknown option "${key}" in ${model}.${op}(), it is ignored.` +
490
+ (suggestion ? ` Did you mean "${suggestion}"?` : ''), {
491
+ code: 'prisma-compat-unknown-option',
492
+ table: model,
493
+ operation: op,
494
+ key,
495
+ ...(suggestion ? { suggestion } : {}),
496
+ });
529
497
  }
530
498
  }
531
499
  catch {
@@ -534,8 +502,8 @@ function warnUnknownQueryOptions(model, op, args) {
534
502
  }
535
503
  }
536
504
  /**
537
- * Dev-mode notice that the `prisma-map.ts` driving this client no longer matches
538
- * the Prisma schema it was generated from.
505
+ * Notice that the `prisma-map.ts` driving this client no longer matches the
506
+ * Prisma schema it was generated from.
539
507
  *
540
508
  * Nothing re-runs `turbine migrate-from-prisma`. When the schema moves on, the
541
509
  * map does not, and the adapter keeps translating the names it has: a model
@@ -550,19 +518,26 @@ function warnUnknownQueryOptions(model, op, args) {
550
518
  * - **Any failure is silence.** A missing file is the NORMAL deployed state
551
519
  * (nobody ships `prisma/` to production) and so are a bundled runtime with no
552
520
  * `node:fs` and a read that is refused. None of them is evidence of drift.
553
- * - **Production is skipped entirely**, before anything is imported or read.
554
- * - **Once per process per path**, via the shared registry, so a process that
555
- * builds a client per request says it once.
521
+ * - **Always on**, like every construction warning: a stale map loses models
522
+ * in production too. In production mode the file is READ once per process
523
+ * per path, so a process that builds a client per request does not read it
524
+ * per request; in development it is re-read per client, so a schema edited
525
+ * under a running dev server is still noticed.
526
+ * - **Warned once per process per path**, via the shared registry.
556
527
  *
557
528
  * @param map - the map under test; a map with no `source` (hand-written, or
558
529
  * emitted before 0.60) is skipped, since absence carries no information.
559
530
  */
560
- function warnStalePrismaMap(map) {
531
+ function warnStalePrismaMap(map, log) {
561
532
  const source = map.source;
562
533
  if (!source || typeof source.path !== 'string' || typeof source.hash !== 'string')
563
534
  return;
564
- if (typeof process === 'undefined' || process.env?.NODE_ENV === 'production')
535
+ // No `process`, no `node:fs`: an edge runtime has no file to compare.
536
+ if (typeof process === 'undefined')
565
537
  return;
538
+ if (log.diagnostics.mode === 'production' && !shouldWarnOnce(WARN_NS.stalePrismaMap, `checked:${source.path}`)) {
539
+ return;
540
+ }
566
541
  void (async () => {
567
542
  try {
568
543
  // Dynamic so a bundle that never reaches this line never needs `node:fs`.
@@ -572,9 +547,9 @@ function warnStalePrismaMap(map) {
572
547
  return;
573
548
  if (!shouldWarnOnce(WARN_NS.stalePrismaMap, source.path))
574
549
  return;
575
- console.warn(`[turbine] prisma-compat: ${source.path} has changed since prisma-map.ts was generated,` +
550
+ logTo(log.logger, 'warn', `prisma-compat: ${source.path} has changed since prisma-map.ts was generated,` +
576
551
  ' so any model, field, relation or compound-unique added or renamed since then is missing' +
577
- ' from the compat client. Re-run: turbine migrate-from-prisma');
552
+ ' from the compat client. Re-run: turbine migrate-from-prisma', { code: 'prisma-map-stale', path: source.path });
578
553
  }
579
554
  catch {
580
555
  // No file, no `node:fs`, no permission. None of these means the map is
@@ -590,7 +565,7 @@ function warnStalePrismaMap(map) {
590
565
  * bound turns "change at most 10 rows" into "change every row".
591
566
  */
592
567
  function refuseRowLimit(model, op, args) {
593
- if (!isPlainObject(args) || args.limit === undefined)
568
+ if (!isPlainRecord(args) || args.limit === undefined)
594
569
  return;
595
570
  throw new UnsupportedFeatureError(`\`limit\` on ${op} (row-bounded mass mutation)`, 'prisma-compat', `Turbine has no row-bounded ${op}; select the rows you mean with \`where\`` +
596
571
  ` (on ${model}.${op}), or page them and mutate by primary key.`);
@@ -624,33 +599,11 @@ function mapRelationLoadStrategy(value) {
624
599
  */
625
600
  function translateWhere(ctx, mm, where, depth = 0) {
626
601
  assertTranslateDepth(depth, 'where');
627
- if (!isPlainObject(where))
602
+ if (!isPlainRecord(where))
628
603
  return where;
629
604
  const out = {};
630
- // The empty-`OR` sentinel, held aside rather than merged into `out`. At most
631
- // one per level, since `OR` is a single object key.
632
- let matchNone;
633
605
  for (const [key, val] of Object.entries(where)) {
634
606
  if (COMBINATORS.has(key)) {
635
- // An EMPTY `OR` is false in Prisma (no branch can match), while an empty
636
- // `AND` / `NOT` is true. Core drops an empty combinator entirely, so the
637
- // same call returned EVERY row here: the classic trigger is
638
- // `OR: ids.map(...)` over a selection that turned out to be empty, i.e.
639
- // "none of these" answered with "all of them". Compiled to a predicate
640
- // that matches nothing so this layer keeps Prisma's meaning; core's own
641
- // semantics are unchanged and documented separately.
642
- //
643
- // Held aside, NOT `Object.assign`ed onto `out`: the sentinel is keyed on
644
- // a real column of the table, so merging it made two predicates on one
645
- // key collide and JavaScript's key ORDER decided which survived.
646
- // `{ OR: [], id: 5 }` kept the caller's `id = 5` and dropped the
647
- // sentinel (returning the row Prisma excludes); `{ id: 5, OR: [] }` kept
648
- // the sentinel and dropped the caller's `id = 5`. Same query, same
649
- // meaning, opposite predicate.
650
- if (key === 'OR' && Array.isArray(val) && val.length === 0) {
651
- matchNone = matchNothing(ctx, mm);
652
- continue;
653
- }
654
607
  // An `AND` / `OR` array of N conditions is ONE level, not N: the elements
655
608
  // are siblings, so they all translate at the same incremented depth.
656
609
  out[key] = Array.isArray(val)
@@ -659,18 +612,18 @@ function translateWhere(ctx, mm, where, depth = 0) {
659
612
  continue;
660
613
  }
661
614
  // Compound-unique selector (custom or default Prisma name).
662
- const compound = mm.compoundUniques[key];
663
- if (compound && isPlainObject(val) && !mm.fields[key] && !mm.relations[key]) {
615
+ const compound = ownLookup(mm.compoundUniques, key);
616
+ if (compound && isPlainRecord(val) && !ownLookup(mm.fields, key) && !ownLookup(mm.relations, key)) {
664
617
  const inner = {};
665
618
  for (const [pk, pv] of Object.entries(val))
666
- inner[renameField(mm, pk)] = pv;
619
+ setOwn(inner, renameField(mm, pk), pv);
667
620
  // Turbine's core derives the selector name from the underscore-join of the
668
621
  // member FIELD names, so re-key custom names onto that canonical form.
669
- out[compound.join('_')] = inner;
622
+ setOwn(out, compound.join('_'), inner);
670
623
  continue;
671
624
  }
672
625
  // Relation filter.
673
- const rel = mm.relations[key];
626
+ const rel = ownLookup(mm.relations, key);
674
627
  if (rel) {
675
628
  // Prisma spells "this to-one relation has no related row" as a bare
676
629
  // `null` on the relation key; core spells it `{ is: null }` and answers a
@@ -680,44 +633,28 @@ function translateWhere(ctx, mm, where, depth = 0) {
680
633
  // has no bare-null shape there, so core still refuses them by name rather
681
634
  // than this layer inventing a meaning.
682
635
  if (val === null && rel.cardinality === 'one') {
683
- out[rel.name] = { is: null };
636
+ setOwn(out, rel.name, { is: null });
684
637
  continue;
685
638
  }
686
639
  const target = relTargetModel(ctx, mm, rel.name);
687
- out[rel.name] = translateRelationFilter(ctx, target, val, depth + 1);
640
+ setOwn(out, rel.name, translateRelationFilter(ctx, target, val, depth + 1));
688
641
  continue;
689
642
  }
690
643
  // Scalar field, key renamed, value (literal or operator object) passes
691
644
  // through unchanged (Prisma operator names match Turbine's).
692
- out[renameField(mm, key)] = val;
645
+ setOwn(out, renameField(mm, key), val);
693
646
  }
694
- if (!matchNone)
695
- return out;
696
- // Nothing else at this level: the sentinel IS the where, no wrapper needed.
697
- if (Object.keys(out).length === 0)
698
- return matchNone;
699
- // Otherwise conjoin, so neither half can overwrite the other whatever order
700
- // the caller wrote the keys in. BRANDED, because an `AND` array is the shape
701
- // that makes a statement unnamed (a caller-written combinator has a
702
- // caller-sized arity); this one is Turbine's, with a fixed arity of two, and
703
- // an unbranded wrapper here would quietly take every compat query carrying an
704
- // empty `OR` off named prepared statements. The caller's own combinators sit
705
- // INSIDE `out` and are walked at the next level with their own brand check,
706
- // so they still count.
707
- return markInternalCombinator({ AND: [out, matchNone] });
647
+ return out;
708
648
  }
709
649
  function translateRelationFilter(ctx, target, val, depth) {
710
- if (!isPlainObject(val))
650
+ if (!isPlainRecord(val))
711
651
  return val;
712
652
  const keys = Object.keys(val);
713
- const hasQuantifier = keys.some((k) => RELATION_QUANTIFIERS.has(k) || k === 'is' || k === 'isNot');
653
+ const hasQuantifier = keys.some((k) => RELATION_FILTER_WRAPPER_SET.has(k));
714
654
  if (hasQuantifier) {
715
655
  const out = {};
716
656
  for (const [k, v] of Object.entries(val)) {
717
- out[k] =
718
- target && (RELATION_QUANTIFIERS.has(k) || k === 'is' || k === 'isNot')
719
- ? translateWhere(ctx, target, v, depth + 1)
720
- : v;
657
+ setOwn(out, k, target && RELATION_FILTER_WRAPPER_SET.has(k) ? translateWhere(ctx, target, v, depth + 1) : v);
721
658
  }
722
659
  return out;
723
660
  }
@@ -741,7 +678,7 @@ function translateOrderBy(ctx, mm, ob, depth = 0) {
741
678
  assertTranslateDepth(depth, 'orderBy');
742
679
  if (Array.isArray(ob))
743
680
  return ob.map((o) => translateOrderBy(ctx, mm, o, depth + 1));
744
- if (!isPlainObject(ob))
681
+ if (!isPlainRecord(ob))
745
682
  return ob;
746
683
  const out = {};
747
684
  for (const [key, val] of Object.entries(ob)) {
@@ -749,20 +686,23 @@ function translateOrderBy(ctx, mm, ob, depth = 0) {
749
686
  out._count = val;
750
687
  continue;
751
688
  }
752
- const rel = mm.relations[key];
689
+ const rel = ownLookup(mm.relations, key);
753
690
  if (rel) {
754
691
  const target = relTargetModel(ctx, mm, rel.name);
755
692
  out[rel.name] =
756
- isPlainObject(val) && !('_count' in val) && target ? translateOrderBy(ctx, target, val, depth + 1) : val;
693
+ isPlainRecord(val) && !('_count' in val) && target ? translateOrderBy(ctx, target, val, depth + 1) : val;
757
694
  continue;
758
695
  }
759
- out[renameField(mm, key)] = val;
696
+ setOwn(out, renameField(mm, key), val);
760
697
  }
761
698
  return out;
762
699
  }
763
- /** Map a Prisma `take` to a Turbine `limit`. Negative take is unsupported. */
700
+ /**
701
+ * Map a Prisma `take` to a Turbine `limit`. Negative take is unsupported. Any
702
+ * other value goes to core as written, which refuses a non-integer.
703
+ */
764
704
  function mapTake(take) {
765
- if (take < 0) {
705
+ if (typeof take === 'number' && take < 0) {
766
706
  throw new UnsupportedFeatureError('negative take (take-from-end pagination)', 'prisma-compat', 'Turbine has no reverse-take; reverse your orderBy and use a positive take instead.');
767
707
  }
768
708
  return take;
@@ -787,11 +727,11 @@ function translateOmit(ctx, mm, omit) {
787
727
  for (const [key, val] of Object.entries(omit)) {
788
728
  if (val === false || val == null)
789
729
  continue;
790
- if (mm.relations[key]) {
730
+ if (ownLookup(mm.relations, key)) {
791
731
  throw new ValidationError(`prisma-compat: \`omit\` on model "${modelName(ctx, mm)}" takes scalar fields, but "${key}" ` +
792
732
  'is a relation. Leave the relation out of `include` instead.');
793
733
  }
794
- out[renameField(mm, key)] = true;
734
+ setOwn(out, renameField(mm, key), true);
795
735
  any = true;
796
736
  }
797
737
  return any ? out : undefined;
@@ -828,15 +768,15 @@ function translateProjection(ctx, mm, args, depth = 0) {
828
768
  if (val === false || val == null)
829
769
  continue;
830
770
  if (key === '_count') {
831
- withClause._count = translateCountSelect(mm, val);
771
+ withClause._count = translateCountSelect(ctx, mm, val);
832
772
  hasWith = true;
833
773
  continue;
834
774
  }
835
- const rel = mm.relations[key];
775
+ const rel = ownLookup(mm.relations, key);
836
776
  if (!rel) {
837
777
  throw new ValidationError(`prisma-compat: unknown relation "${key}" in include on model "${modelName(ctx, mm)}".`);
838
778
  }
839
- withClause[rel.name] = translateWithOption(ctx, mm, rel.name, val, depth + 1);
779
+ setOwn(withClause, rel.name, translateWithOption(ctx, mm, rel.name, val, depth + 1));
840
780
  hasWith = true;
841
781
  }
842
782
  return { omit, with: hasWith ? withClause : undefined };
@@ -848,17 +788,17 @@ function translateProjection(ctx, mm, args, depth = 0) {
848
788
  if (val === false || val == null)
849
789
  continue;
850
790
  if (key === '_count') {
851
- withClause._count = translateCountSelect(mm, val);
791
+ withClause._count = translateCountSelect(ctx, mm, val);
852
792
  hasWith = true;
853
793
  continue;
854
794
  }
855
- const rel = mm.relations[key];
795
+ const rel = ownLookup(mm.relations, key);
856
796
  if (rel) {
857
- withClause[rel.name] = translateWithOption(ctx, mm, rel.name, val, depth + 1);
797
+ setOwn(withClause, rel.name, translateWithOption(ctx, mm, rel.name, val, depth + 1));
858
798
  hasWith = true;
859
799
  continue;
860
800
  }
861
- scalar[renameField(mm, key)] = true;
801
+ setOwn(scalar, renameField(mm, key), true);
862
802
  hasScalar = true;
863
803
  }
864
804
  return { select: hasScalar ? scalar : undefined, with: hasWith ? withClause : undefined };
@@ -940,7 +880,7 @@ function resolveWriteProjection(ctx, mm, op, args) {
940
880
  for (const [key, val] of Object.entries(select)) {
941
881
  if (val === false || val == null)
942
882
  continue;
943
- if (key === '_count' || mm.relations[key]) {
883
+ if (key === '_count' || ownLookup(mm.relations, key)) {
944
884
  throw new UnsupportedFeatureError(`select of "${key}" on ${op}`, 'prisma-compat', `"${key}" is a relation on model "${modelName(ctx, mm)}"; a write returns its own row only, so read ` +
945
885
  'it back with a separate findUnique inside the same $transaction.');
946
886
  }
@@ -957,7 +897,7 @@ function resolveWriteProjection(ctx, mm, op, args) {
957
897
  for (const [key, val] of Object.entries(omitArg)) {
958
898
  if (val === false || val == null)
959
899
  continue;
960
- if (mm.relations[key]) {
900
+ if (ownLookup(mm.relations, key)) {
961
901
  throw new ValidationError(`prisma-compat: \`omit\` on model "${modelName(ctx, mm)}" takes scalar fields, but "${key}" ` +
962
902
  'is a relation.');
963
903
  }
@@ -986,13 +926,13 @@ function coreWriteProjection(mm, proj) {
986
926
  }
987
927
  /** Apply a {@link WriteProjection} to one already-reshaped write result row. */
988
928
  function applyWriteProjection(proj, row) {
989
- if (!isPlainObject(row))
929
+ if (!isPlainRecord(row))
990
930
  return row;
991
931
  if (proj.pick) {
992
932
  const out = {};
993
933
  for (const key of proj.pick) {
994
- if (key in row)
995
- out[key] = row[key];
934
+ if (Object.hasOwn(row, key))
935
+ setOwn(out, key, row[key]);
996
936
  }
997
937
  return out;
998
938
  }
@@ -1004,9 +944,65 @@ function applyWriteProjection(proj, row) {
1004
944
  }
1005
945
  return row;
1006
946
  }
947
+ /** What a caller wrote, named for a message: `an array`, `null`, `a string`. */
948
+ function describeValue(value) {
949
+ if (value === undefined)
950
+ return 'undefined';
951
+ if (value === null)
952
+ return 'null';
953
+ if (Array.isArray(value))
954
+ return 'an array';
955
+ return typeof value === 'object' ? 'an object' : `a ${typeof value}`;
956
+ }
957
+ /** The Prisma spelling of a turbine relation name, for a message. */
958
+ function prismaRelationName(ctx, mm, turbineRel) {
959
+ return ownLookup(lookupsFor(ctx, mm).reverseRelations, turbineRel)?.prismaName ?? turbineRel;
960
+ }
961
+ /**
962
+ * The options Prisma takes on an included relation. `cursor` and `distinct` are
963
+ * here because they are Prisma's, and are refused by name (E017) rather than
964
+ * called unknown: Turbine's `with` clause cannot honor either.
965
+ */
966
+ const NESTED_RELATION_KEYS = new Set([
967
+ 'where',
968
+ 'orderBy',
969
+ 'take',
970
+ 'skip',
971
+ 'cursor',
972
+ 'distinct',
973
+ 'select',
974
+ 'include',
975
+ 'omit',
976
+ ]);
977
+ /**
978
+ * Refuse a key on an included relation's options that this layer does not
979
+ * translate. Every key it does not read used to be dropped, so a misspelled
980
+ * `wher` or a Turbine spelling (`limit`) returned unfiltered, unlimited rows
981
+ * with no signal. An `undefined` value is absent, as everywhere else.
982
+ */
983
+ function assertNestedRelationKeys(label, opts) {
984
+ for (const key of Object.keys(opts)) {
985
+ if (opts[key] === undefined || NESTED_RELATION_KEYS.has(key))
986
+ continue;
987
+ const alias = ALIAS_HINT[key];
988
+ if (alias) {
989
+ throw new ValidationError(`prisma-compat: "${key}" in the options of relation "${label}" is Turbine's spelling and would be ignored. ` +
990
+ `prisma-compat takes Prisma's "${alias}".`);
991
+ }
992
+ const suggestion = suggestKey(key, NESTED_RELATION_KEYS);
993
+ throw new ValidationError(`prisma-compat: unknown option "${key}" in the options of relation "${label}", it would be ignored.` +
994
+ (suggestion ? ` Did you mean "${suggestion}"?` : '') +
995
+ ` Valid options: ${[...NESTED_RELATION_KEYS].join(', ')}.`);
996
+ }
997
+ }
1007
998
  /**
1008
999
  * Translate a Prisma relation include payload into a Turbine `WithOptions`.
1009
1000
  *
1001
+ * Every option the payload carries is either translated, refused by name, or
1002
+ * refused as unknown: none is dropped. `cursor` and `distinct` are Prisma's
1003
+ * and have no per-relation form in a `with` clause (E017); any other key is
1004
+ * E003 (see {@link assertNestedRelationKeys}).
1005
+ *
1010
1006
  * `depth` is the relation hop count carried down from {@link translateProjection}
1011
1007
  * and is charged to the nested `where` / `orderBy` too, so a payload that pads
1012
1008
  * one arg with relation hops and then nests the other cannot spend two separate
@@ -1015,8 +1011,11 @@ function applyWriteProjection(proj, row) {
1015
1011
  function translateWithOption(ctx, mm, turbineRel, val, depth = 0) {
1016
1012
  if (val === true)
1017
1013
  return true;
1018
- if (!isPlainObject(val))
1019
- return true;
1014
+ const label = `${modelName(ctx, mm)}.${prismaRelationName(ctx, mm, turbineRel)}`;
1015
+ if (!isPlainRecord(val)) {
1016
+ throw new ValidationError(`prisma-compat: the value for relation "${label}" must be true or an options object, received ${describeValue(val)}.`);
1017
+ }
1018
+ assertNestedRelationKeys(label, val);
1020
1019
  const target = relTargetModel(ctx, mm, turbineRel);
1021
1020
  const opt = {};
1022
1021
  if (val.where !== undefined)
@@ -1025,10 +1024,26 @@ function translateWithOption(ctx, mm, turbineRel, val, depth = 0) {
1025
1024
  opt.orderBy = target ? translateOrderBy(ctx, target, val.orderBy, depth) : val.orderBy;
1026
1025
  if (val.take !== undefined)
1027
1026
  opt.limit = mapTake(val.take);
1028
- if (val.skip !== undefined) {
1029
- throw new UnsupportedFeatureError('skip (offset) on a nested relation include', 'prisma-compat', "Turbine's `with` clause has no offset, page the relation with a separate query.");
1030
- }
1031
- if (target && (val.select !== undefined || val.include !== undefined || val.omit !== undefined)) {
1027
+ // Per parent, as in Prisma; core refuses it on a to-one relation.
1028
+ if (val.skip !== undefined)
1029
+ opt.offset = val.skip;
1030
+ if (val.cursor !== undefined) {
1031
+ throw new UnsupportedFeatureError('cursor on a nested relation include', 'prisma-compat', "Turbine's `with` clause has no cursor. Page the relation with `take` / `skip`, or with a separate " +
1032
+ 'findMany on the related model.');
1033
+ }
1034
+ // `distinct: []` names no field and removes nothing, so it is not a request.
1035
+ if (val.distinct !== undefined && !(Array.isArray(val.distinct) && val.distinct.length === 0)) {
1036
+ throw new UnsupportedFeatureError(`distinct on a nested relation include ("${label}")`, 'prisma-compat', "Turbine's `with` clause has no distinct. Run a separate findMany on the related model with the same " +
1037
+ 'where / orderBy / distinct, or de-duplicate the included rows in application code.');
1038
+ }
1039
+ if (val.select !== undefined || val.include !== undefined || val.omit !== undefined) {
1040
+ // Without the target model there are no field names to translate with, and
1041
+ // dropping the projection would return every column of the child rows.
1042
+ if (!target) {
1043
+ throw new ValidationError(`prisma-compat: relation "${label}" points at a table that is not in the compat map, so its ` +
1044
+ '`select` / `include` / `omit` cannot be translated. Regenerate the map with ' +
1045
+ '`turbine migrate-from-prisma`, or drop the projection.');
1046
+ }
1032
1047
  const proj = translateProjection(ctx, target, val, depth);
1033
1048
  if (proj.select)
1034
1049
  opt.select = proj.select;
@@ -1041,30 +1056,104 @@ function translateWithOption(ctx, mm, turbineRel, val, depth = 0) {
1041
1056
  }
1042
1057
  return opt;
1043
1058
  }
1044
- /** Translate a Prisma `_count: { select: { rel: true } }` / `true` into Turbine `with._count`. */
1045
- function translateCountSelect(mm, val) {
1059
+ /** A count `where` that filters nothing: absent, or an object with no defined key. */
1060
+ function isUnfilteredCountWhere(where) {
1061
+ return where === undefined || (isPlainRecord(where) && Object.values(where).every((v) => v === undefined));
1062
+ }
1063
+ /**
1064
+ * Translate a Prisma `_count: { select: { rel: true } }` / `true` into Turbine
1065
+ * `with._count`.
1066
+ *
1067
+ * Core counts a relation whole: `with._count` takes `true` or `{ rel: true }`.
1068
+ * So a Prisma filtered count (`{ rel: { where } }`) is refused (E017) naming the
1069
+ * alternative, a relation that is not on the model is E003, and so is any key
1070
+ * but `select` under `_count` (a misspelled `selec` used to count EVERY relation,
1071
+ * and a misspelled relation counted nothing, both without a word).
1072
+ */
1073
+ function translateCountSelect(ctx, mm, val) {
1046
1074
  if (val === true)
1047
1075
  return true;
1048
- if (isPlainObject(val) && isPlainObject(val.select)) {
1049
- const out = {};
1050
- for (const [key, v] of Object.entries(val.select)) {
1051
- if (!v)
1076
+ const model = modelName(ctx, mm);
1077
+ if (!isPlainRecord(val)) {
1078
+ throw new ValidationError(`prisma-compat: \`_count\` on model "${model}" takes true or { select: { <relation>: true } }, ` +
1079
+ `received ${describeValue(val)}.`);
1080
+ }
1081
+ for (const key of Object.keys(val)) {
1082
+ if (key === 'select' || val[key] === undefined)
1083
+ continue;
1084
+ const suggestion = suggestKey(key, ['select']);
1085
+ throw new ValidationError(`prisma-compat: unknown key "${key}" in \`_count\` on model "${model}", it would be ignored.` +
1086
+ (suggestion ? ` Did you mean "${suggestion}"?` : '') +
1087
+ ' The only key is "select".');
1088
+ }
1089
+ if (val.select === undefined)
1090
+ return true;
1091
+ if (!isPlainRecord(val.select)) {
1092
+ throw new ValidationError(`prisma-compat: \`_count.select\` on model "${model}" takes { <relation>: true }, ` +
1093
+ `received ${describeValue(val.select)}.`);
1094
+ }
1095
+ const out = {};
1096
+ for (const [key, v] of Object.entries(val.select)) {
1097
+ if (v === undefined || v === false || v === null)
1098
+ continue;
1099
+ const rel = ownLookup(mm.relations, key);
1100
+ if (!rel) {
1101
+ const known = Object.keys(mm.relations);
1102
+ const suggestion = suggestKey(key, known);
1103
+ throw new ValidationError(`prisma-compat: unknown relation "${key}" in \`_count.select\` on model "${model}", it would not be counted.` +
1104
+ (suggestion ? ` Did you mean "${suggestion}"?` : '') +
1105
+ ` Relations: ${known.join(', ') || '(none)'}.`);
1106
+ }
1107
+ if (v === true) {
1108
+ setOwn(out, rel.name, true);
1109
+ continue;
1110
+ }
1111
+ if (!isPlainRecord(v)) {
1112
+ throw new ValidationError(`prisma-compat: \`_count.select.${key}\` on model "${model}" takes true, false or { where }, ` +
1113
+ `received ${describeValue(v)}.`);
1114
+ }
1115
+ for (const k of Object.keys(v)) {
1116
+ if (k === 'where' || v[k] === undefined)
1052
1117
  continue;
1053
- const rel = mm.relations[key];
1054
- if (rel)
1055
- out[rel.name] = true;
1118
+ const hint = suggestKey(k, ['where']);
1119
+ throw new ValidationError(`prisma-compat: unknown key "${k}" in \`_count.select.${key}\` on model "${model}", it would be ignored.` +
1120
+ (hint ? ` Did you mean "${hint}"?` : '') +
1121
+ ' The only key is "where".');
1056
1122
  }
1057
- return out;
1123
+ if (!isUnfilteredCountWhere(v.where)) {
1124
+ const target = relTargetModel(ctx, mm, rel.name);
1125
+ const targetName = target ? modelName(ctx, target) : undefined;
1126
+ const delegate = targetName ? (prismaPropertyAlias(targetName) ?? targetName) : '<model>';
1127
+ throw new UnsupportedFeatureError(`a filtered relation count (\`_count.select.${key}.where\` on model "${model}")`, 'prisma-compat', 'Turbine counts every row of a relation, so the filter would be dropped and the number would be wrong. ' +
1128
+ `Count the filtered rows through the related model instead: prisma.${delegate}.count({ where: ... }) ` +
1129
+ `with the foreign key and your filter, or prisma.${delegate}.groupBy({ by: [<foreign key>], _count: true }) ` +
1130
+ 'for many parents at once.');
1131
+ }
1132
+ setOwn(out, rel.name, true);
1058
1133
  }
1059
- return true;
1134
+ return out;
1060
1135
  }
1061
1136
  function modelName(ctx, mm) {
1062
1137
  return ctx.tableToModel.get(mm.table) ?? mm.table;
1063
1138
  }
1064
1139
  /**
1065
- * Assemble Turbine findMany-family args from Prisma read args. Order matters:
1066
- * `where` / `orderBy` are translated first so the cursor step (which may inject
1067
- * a keyset predicate) operates in the translated turbine-field space.
1140
+ * A Prisma field list that is written either as one name or as an array of
1141
+ * names (`distinct`, groupBy `by`), renamed into turbine fields. Anything else
1142
+ * is refused: a bare string used to be dropped (`distinct: 'status'` returned
1143
+ * every row, and `by: 'status'` crashed with a TypeError).
1144
+ */
1145
+ function translateFieldList(ctx, mm, option, val) {
1146
+ const names = typeof val === 'string' ? [val] : val;
1147
+ if (!Array.isArray(names) || names.some((n) => typeof n !== 'string')) {
1148
+ throw new ValidationError(`prisma-compat: \`${option}\` on model "${modelName(ctx, mm)}" takes a field name or an array of field names, ` +
1149
+ `received ${describeValue(val)}.`);
1150
+ }
1151
+ return names.map((f) => renameField(mm, f));
1152
+ }
1153
+ /**
1154
+ * Assemble Turbine findMany-family args from Prisma read args. `where` /
1155
+ * `orderBy` are translated first, so the cursor step works in turbine-field
1156
+ * space.
1068
1157
  */
1069
1158
  function translateReadArgs(ctx, mm, prismaArgs, kind) {
1070
1159
  const t = {};
@@ -1086,19 +1175,11 @@ function translateReadArgs(ctx, mm, prismaArgs, kind) {
1086
1175
  t.omit = proj.omit;
1087
1176
  if (proj.with)
1088
1177
  t.with = proj.with;
1089
- if (Array.isArray(prismaArgs.distinct)) {
1090
- t.distinct = prismaArgs.distinct.map((f) => renameField(mm, f));
1091
- }
1178
+ if (prismaArgs.distinct !== undefined)
1179
+ t.distinct = translateFieldList(ctx, mm, 'distinct', prismaArgs.distinct);
1092
1180
  if (prismaArgs.relationLoadStrategy !== undefined) {
1093
1181
  t.relationLoadStrategy = mapRelationLoadStrategy(prismaArgs.relationLoadStrategy);
1094
1182
  }
1095
- // ORDER IS LOAD-BEARING: translateCursor MUST run BEFORE applyImplicitPkOrder.
1096
- // The cursor translation reads `t.orderBy` to decide the seek direction and to
1097
- // validate that a bare inclusive cursor names the sort key; it must see the
1098
- // USER's ordering, never an ordering this layer invented. Swap these two lines
1099
- // and a bare inclusive cursor on a non-PK field silently compiles against the
1100
- // injected primary-key order instead of throwing. See the guard test
1101
- // "cursor decisions are made against the user's own orderBy".
1102
1183
  translateCursor(ctx, mm, prismaArgs, t);
1103
1184
  applyImplicitPkOrder(ctx, mm, t, kind);
1104
1185
  return t;
@@ -1145,15 +1226,9 @@ function translateReadArgs(ctx, mm, prismaArgs, kind) {
1145
1226
  * with `column "id" does not exist`. Injecting also lifts the `LIMIT` outside
1146
1227
  * the `DISTINCT ON`, so the inner scan stops being limited. `distinct` reads
1147
1228
  * keep the exact SQL they emitted before this ordering existed.
1148
- * - `cursor`: NOT excluded here, unlike core. Core is a general-purpose layer
1149
- * whose cursor callers chose their own ordering; this layer's contract is
1150
- * Prisma parity, and Prisma pairs cursor pagination with the same implicit
1151
- * primary-key order. The injection is safe because it runs strictly AFTER
1152
- * {@link translateCursor}: the cursor comparison direction was already
1153
- * resolved from the user's own `orderBy` (ascending by default, matching an
1154
- * ascending primary key), and the bare-inclusive-cursor branch has already
1155
- * set or demanded an `orderBy`, so only the exclusive `cursor` + `skip`
1156
- * branch reaches here. It gains an `ORDER BY <pk> ASC` and nothing else.
1229
+ * - `cursor`: not excluded, and harmless either way. Core orders every cursor
1230
+ * page itself (the caller's order plus a primary-key tiebreaker), and with no
1231
+ * `orderBy` that is exactly the `ORDER BY <pk> ASC` written here.
1157
1232
  */
1158
1233
  function applyImplicitPkOrder(ctx, mm, t, kind) {
1159
1234
  if (kind === 'unique')
@@ -1164,134 +1239,59 @@ function applyImplicitPkOrder(ctx, mm, t, kind) {
1164
1239
  return;
1165
1240
  if (kind === 'many' && t.limit === undefined && t.offset === undefined)
1166
1241
  return;
1167
- const meta = ctx.schema.tables[mm.table];
1242
+ const meta = ownLookup(ctx.schema.tables, mm.table);
1168
1243
  const pk = meta?.primaryKey ?? [];
1169
1244
  if (pk.length === 0)
1170
1245
  return;
1171
1246
  const fields = pk.map((col) => meta?.reverseColumnMap?.[col] ?? col);
1172
1247
  t.orderBy = fields.length === 1 ? { [fields[0]]: 'asc' } : fields.map((f) => ({ [f]: 'asc' }));
1173
1248
  }
1174
- /** turbine field names of the model's single-column primary key, if any. */
1175
- function singleColumnPkField(ctx, mm) {
1176
- const pk = ctx.schema.tables[mm.table]?.primaryKey;
1177
- if (pk && pk.length === 1) {
1178
- const col = pk[0];
1179
- return ctx.schema.tables[mm.table]?.reverseColumnMap?.[col] ?? col;
1180
- }
1181
- return undefined;
1182
- }
1183
1249
  /**
1184
1250
  * Translate Prisma cursor + take/skip onto Turbine args (operating on already
1185
1251
  * translated `t.where` / `t.orderBy` in turbine-field space).
1186
1252
  *
1187
1253
  * - **No cursor:** `skip`→`offset`, `take`→`limit`.
1188
- * - **Cursor + `skip: n` (n≥1):** the idiomatic exclusive-pagination case →
1189
- * Turbine's exclusive `cursor` + `offset: n-1` (skip:1 → offset 0, an exact
1190
- * match).
1191
- * - **Bare inclusive cursor** (skip absent/0): a Prisma cursor is INCLUSIVE, so
1192
- * it needs the anchor's sort-key value. Compiled to a `gte`/`lte` keyset
1193
- * predicate merged into `where` ONLY when the cursor is single-field AND that
1194
- * field is the single `orderBy` field (or, with no `orderBy`, is the
1195
- * single-column PK). Any other shape THROWS rather than emit a wrong page -
1196
- * pair the cursor with `skip: 1` for the exact exclusive translation.
1254
+ * - **Cursor:** core's cursor with the {@link CURSOR_INCLUSIVE} flag, plus
1255
+ * `offset: skip`. That is Prisma's definition exactly: the page starts AT the
1256
+ * cursor row, in the query's order (core completes it with a primary-key
1257
+ * tiebreaker), and `skip` counts from there, so the `skip: 1` idiom drops the
1258
+ * cursor row. Core places the page from the row the cursor names by a unique
1259
+ * key, which is what a Prisma cursor always is, so a cursor whose row has
1260
+ * been deleted gives an empty page, as it does in Prisma.
1197
1261
  */
1198
- function translateCursor(ctx, mm, prismaArgs, t) {
1262
+ function translateCursor(_ctx, mm, prismaArgs, t) {
1199
1263
  const cursor = prismaArgs.cursor;
1200
1264
  const skip = prismaArgs.skip;
1201
1265
  const take = prismaArgs.take;
1202
1266
  if (take !== undefined)
1203
1267
  t.limit = mapTake(take);
1204
- if (cursor === undefined) {
1205
- if (skip !== undefined)
1206
- t.offset = skip;
1268
+ if (skip !== undefined)
1269
+ t.offset = skip;
1270
+ if (cursor === undefined)
1271
+ return;
1272
+ // A cursor that is not an object (`null`) reaches core as written, which
1273
+ // refuses it: read as "no cursor" it would return the first page.
1274
+ if (!isPlainRecord(cursor)) {
1275
+ t.cursor = cursor;
1207
1276
  return;
1208
1277
  }
1209
1278
  // Translate cursor field names (and expand a compound-unique selector cursor).
1279
+ // An `undefined` value is carried through rather than dropped, so a cursor
1280
+ // whose every value was `undefined` reaches core's args seam as one and is
1281
+ // refused there (E003), instead of becoming `{}` and paging from the start.
1210
1282
  const tcursor = {};
1211
1283
  for (const [key, val] of Object.entries(cursor)) {
1212
- if (val === undefined)
1213
- continue;
1214
- const compound = mm.compoundUniques[key];
1215
- if (compound && isPlainObject(val)) {
1284
+ const compound = ownLookup(mm.compoundUniques, key);
1285
+ if (compound && isPlainRecord(val)) {
1216
1286
  for (const [pk, pv] of Object.entries(val))
1217
- tcursor[renameField(mm, pk)] = pv;
1287
+ setOwn(tcursor, renameField(mm, pk), pv);
1218
1288
  }
1219
1289
  else {
1220
- tcursor[renameField(mm, key)] = val;
1221
- }
1222
- }
1223
- const cursorFields = Object.keys(tcursor);
1224
- if (skip !== undefined && skip >= 1) {
1225
- t.cursor = tcursor;
1226
- t.offset = skip - 1;
1227
- return;
1228
- }
1229
- // Bare inclusive cursor (skip absent or 0).
1230
- if (cursorFields.length !== 1) {
1231
- throw new UnsupportedFeatureError('inclusive multi-field cursor without skip', 'prisma-compat', 'Pair the cursor with `skip: 1` (the Prisma idiom) so it maps to an exact exclusive cursor.');
1232
- }
1233
- const field = cursorFields[0];
1234
- const value = tcursor[field];
1235
- const obEntries = orderByPairs(t.orderBy);
1236
- let desc = false;
1237
- if (obEntries.length === 0) {
1238
- const pkField = singleColumnPkField(ctx, mm);
1239
- if (!pkField || field !== pkField) {
1240
- throw new UnsupportedFeatureError('inclusive cursor without orderBy on a non-PK field', 'prisma-compat', `A bare Prisma cursor is inclusive; translating it needs the cursor field "${field}" to be the single-column primary key, or to be the single orderBy field. Pair the cursor with \`skip: 1\`.`);
1241
- }
1242
- t.orderBy = { [field]: 'asc' };
1243
- }
1244
- else {
1245
- if (obEntries.length !== 1 || obEntries[0][0] !== field) {
1246
- throw new UnsupportedFeatureError('inclusive cursor whose field is not the sort key', 'prisma-compat', `A bare Prisma cursor is inclusive; translating it needs the cursor field "${field}" to be the single orderBy field. Order by "${field}", or pair the cursor with \`skip: 1\`.`);
1290
+ setOwn(tcursor, renameField(mm, key), val);
1247
1291
  }
1248
- desc = obEntries[0][1];
1249
1292
  }
1250
- const op = desc ? 'lte' : 'gte';
1251
- t.where = mergeKeyset(t.where ?? {}, field, op, value);
1252
- }
1253
- /**
1254
- * Flatten a Turbine orderBy (object, or an array of them) into [field, isDesc]
1255
- * pairs.
1256
- *
1257
- * EVERY array element is read. Reading only an array of length one returned
1258
- * `[]` for a two-element array, and `[]` is also what "no orderBy at all"
1259
- * returns, so {@link translateCursor} took its no-orderBy branch: it found a
1260
- * bare cursor on the primary key, wrote `orderBy = { id: 'asc' }` over the
1261
- * caller's array, and returned the rows following the anchor in PK order
1262
- * instead of in the caller's order. The single-sort-key check downstream is
1263
- * what refuses a shape a bare (inclusive) cursor cannot express, and it can
1264
- * only fire if the pairs it counts are all there.
1265
- */
1266
- function orderByPairs(ob) {
1267
- const entries = Array.isArray(ob) ? ob : [ob];
1268
- const out = [];
1269
- for (const entry of entries) {
1270
- if (!isPlainObject(entry))
1271
- continue;
1272
- for (const [k, v] of Object.entries(entry)) {
1273
- const dir = isPlainObject(v) ? v.sort : v;
1274
- out.push([k, dir === 'desc']);
1275
- }
1276
- }
1277
- return out;
1278
- }
1279
- /** Merge a `{ field: { gte|lte: value } }` keyset predicate into a where object. */
1280
- function mergeKeyset(where, field, op, value) {
1281
- const merged = { ...where };
1282
- const existing = merged[field];
1283
- if (isPlainObject(existing)) {
1284
- merged[field] = { ...existing, [op]: value };
1285
- }
1286
- else if (existing !== undefined) {
1287
- const prevAnd = merged.AND;
1288
- const andList = Array.isArray(prevAnd) ? prevAnd : prevAnd !== undefined ? [prevAnd] : [];
1289
- merged.AND = [...andList, { [field]: { [op]: value } }];
1290
- }
1291
- else {
1292
- merged[field] = { [op]: value };
1293
- }
1294
- return merged;
1293
+ t.cursor = tcursor;
1294
+ t[CURSOR_INCLUSIVE] = true;
1295
1295
  }
1296
1296
  // --- write-data translation (nested writes) -------------------------------
1297
1297
  const NESTED_WRITE_OPS = new Set([
@@ -1315,20 +1315,36 @@ const NESTED_WRITE_OPS = new Set([
1315
1315
  */
1316
1316
  function translateWriteData(ctx, mm, data, depth = 0) {
1317
1317
  assertTranslateDepth(depth, 'data');
1318
- if (!isPlainObject(data))
1318
+ if (!isPlainRecord(data))
1319
1319
  return data;
1320
1320
  const out = {};
1321
1321
  for (const [key, val] of Object.entries(data)) {
1322
- const rel = mm.relations[key];
1323
- if (rel && isPlainObject(val) && Object.keys(val).some((k) => NESTED_WRITE_OPS.has(k))) {
1322
+ if (isExpressionNode(val)) {
1323
+ // The field names inside an expression are Turbine's, and this layer
1324
+ // renames only the keys it can see, so one would be compiled against
1325
+ // the wrong names wherever a Prisma field is mapped.
1326
+ throw new UnsupportedFeatureError(`an update expression (expr / col()) in "${key}" on model "${modelName(ctx, mm)}"`, 'prisma-compat', 'Expressions are a Turbine-native API: run this write through the Turbine client, where field names ' +
1327
+ 'are its own, or compute the value in JavaScript.');
1328
+ }
1329
+ const rel = ownLookup(mm.relations, key);
1330
+ if (rel && isPlainRecord(val) && Object.keys(val).some((k) => NESTED_WRITE_OPS.has(k))) {
1324
1331
  const target = relTargetModel(ctx, mm, rel.name);
1325
- out[rel.name] = translateNestedWrite(ctx, target, val, depth + 1);
1332
+ setOwn(out, rel.name, translateNestedWrite(ctx, target, val, depth + 1));
1326
1333
  continue;
1327
1334
  }
1328
- out[renameField(mm, key)] = val;
1335
+ setOwn(out, renameField(mm, key), val);
1329
1336
  }
1330
1337
  return out;
1331
1338
  }
1339
+ /**
1340
+ * An update expression (`expr.*`) or column reference (`col()`), read by the
1341
+ * brands' registry keys so this shim imports neither builder module.
1342
+ */
1343
+ function isExpressionNode(value) {
1344
+ return (typeof value === 'object' &&
1345
+ value !== null &&
1346
+ (Symbol.for('turbine-orm.sqlExpr') in value || Symbol.for('turbine-orm.columnRef') in value));
1347
+ }
1332
1348
  /**
1333
1349
  * One relation's nested-write ops. `depth` is threaded into EVERY payload
1334
1350
  * translator, the `where` ones included, so the budget is spent on total
@@ -1356,10 +1372,8 @@ function translateNestedWrite(ctx, target, ops, depth) {
1356
1372
  break;
1357
1373
  case 'deleteMany':
1358
1374
  case 'updateMany':
1359
- out[op] = mapMaybeArray(payload, (p) => translateWhereDataPair(ctx, target, p, depth + 1));
1360
- break;
1361
1375
  case 'update':
1362
- out[op] = mapMaybeArray(payload, (p) => translateWhereDataPair(ctx, target, p, depth + 1));
1376
+ out[op] = mapMaybeArray(payload, (p) => translateWhereDataPair(ctx, target, op, p, depth + 1));
1363
1377
  break;
1364
1378
  case 'connectOrCreate':
1365
1379
  out[op] = mapMaybeArray(payload, (p) => translateConnectOrCreate(ctx, target, p, depth + 1));
@@ -1368,7 +1382,7 @@ function translateNestedWrite(ctx, target, ops, depth) {
1368
1382
  out[op] = mapMaybeArray(payload, (p) => translateUpsertNested(ctx, target, p, depth + 1));
1369
1383
  break;
1370
1384
  default:
1371
- out[op] = payload;
1385
+ setOwn(out, op, payload);
1372
1386
  }
1373
1387
  }
1374
1388
  return out;
@@ -1376,11 +1390,27 @@ function translateNestedWrite(ctx, target, ops, depth) {
1376
1390
  function mapMaybeArray(val, fn) {
1377
1391
  return Array.isArray(val) ? val.map(fn) : fn(val);
1378
1392
  }
1393
+ /**
1394
+ * Refuse a key on a nested write's `{ where, data }` / `{ where, create, update }`
1395
+ * payload that this layer does not read: it was dropped, so the part of the
1396
+ * write the caller meant never ran. An `undefined` value is absent.
1397
+ */
1398
+ function assertNestedPayloadKeys(op, p, allowed) {
1399
+ for (const key of Object.keys(p)) {
1400
+ if (p[key] === undefined || allowed.includes(key))
1401
+ continue;
1402
+ const suggestion = suggestKey(key, allowed);
1403
+ throw new ValidationError(`prisma-compat: unknown key "${key}" in a nested ${op}, it would be ignored.` +
1404
+ (suggestion ? ` Did you mean "${suggestion}"?` : '') +
1405
+ ` Valid keys: ${allowed.join(', ')}.`);
1406
+ }
1407
+ }
1379
1408
  /** A `{ where?, data }` pair (nested update/updateMany), or a bare data object. */
1380
- function translateWhereDataPair(ctx, target, p, depth) {
1381
- if (!isPlainObject(p))
1409
+ function translateWhereDataPair(ctx, target, op, p, depth) {
1410
+ if (!isPlainRecord(p))
1382
1411
  return p;
1383
1412
  if ('data' in p || 'where' in p) {
1413
+ assertNestedPayloadKeys(op, p, ['where', 'data']);
1384
1414
  const out = {};
1385
1415
  if (p.where !== undefined)
1386
1416
  out.where = target ? translateWhere(ctx, target, p.where, depth) : p.where;
@@ -1391,8 +1421,9 @@ function translateWhereDataPair(ctx, target, p, depth) {
1391
1421
  return target ? translateWriteData(ctx, target, p, depth) : p;
1392
1422
  }
1393
1423
  function translateConnectOrCreate(ctx, target, p, depth) {
1394
- if (!isPlainObject(p))
1424
+ if (!isPlainRecord(p))
1395
1425
  return p;
1426
+ assertNestedPayloadKeys('connectOrCreate', p, ['where', 'create']);
1396
1427
  const out = {};
1397
1428
  if (p.where !== undefined)
1398
1429
  out.where = target ? translateWhere(ctx, target, p.where, depth) : p.where;
@@ -1401,8 +1432,9 @@ function translateConnectOrCreate(ctx, target, p, depth) {
1401
1432
  return out;
1402
1433
  }
1403
1434
  function translateUpsertNested(ctx, target, p, depth) {
1404
- if (!isPlainObject(p))
1435
+ if (!isPlainRecord(p))
1405
1436
  return p;
1437
+ assertNestedPayloadKeys('upsert', p, ['where', 'create', 'update']);
1406
1438
  const out = {};
1407
1439
  if (p.where !== undefined)
1408
1440
  out.where = target ? translateWhere(ctx, target, p.where, depth) : p.where;
@@ -1414,8 +1446,24 @@ function translateUpsertNested(ctx, target, p, depth) {
1414
1446
  }
1415
1447
  // --- aggregate / groupBy translation --------------------------------------
1416
1448
  const AGG_FIELD_BLOCKS = ['_sum', '_avg', '_min', '_max'];
1449
+ /**
1450
+ * Prisma's `count` and `aggregate` with `take` / `skip` / `cursor` compute over
1451
+ * that PAGE of rows. Turbine has no paged count or aggregate, and answering
1452
+ * over every matching row instead is a different number, so it is refused.
1453
+ * `orderBy` on its own changes no count or aggregate and is accepted.
1454
+ */
1455
+ function refusePagedAggregate(op, args) {
1456
+ const paged = ['take', 'skip', 'cursor'].filter((k) => args[k] !== undefined);
1457
+ if (paged.length === 0)
1458
+ return;
1459
+ throw new UnsupportedFeatureError(`${paged.join(' / ')} on ${op}`, 'prisma-compat', `Prisma computes this ${op} over one page of rows, and Turbine has no paged ${op}. Fetch the page with ` +
1460
+ `findMany (same where / orderBy / ${paged.join(' / ')}) and compute it from the rows, or drop ` +
1461
+ `${paged.join(' / ')} to ${op} every matching row.`);
1462
+ }
1417
1463
  function translateAggregateArgs(ctx, mm, args, isGroupBy) {
1418
1464
  const t = {};
1465
+ if (!isGroupBy)
1466
+ refusePagedAggregate('aggregate', args);
1419
1467
  applyNativeOptions(isGroupBy ? GROUP_BY_OPTIONS : AGGREGATE_OPTIONS, args, t);
1420
1468
  if (args.where !== undefined)
1421
1469
  t.where = translateWhere(ctx, mm, args.where);
@@ -1426,17 +1474,19 @@ function translateAggregateArgs(ctx, mm, args, isGroupBy) {
1426
1474
  t[block] = renameAggBlock(mm, args[block], false);
1427
1475
  }
1428
1476
  if (isGroupBy) {
1429
- if (Array.isArray(args.by))
1430
- t.by = args.by.map((f) => renameField(mm, f));
1477
+ if (args.by === undefined) {
1478
+ throw new ValidationError(`prisma-compat: groupBy on model "${modelName(ctx, mm)}" requires \`by\`, a field name or an array of field names.`);
1479
+ }
1480
+ t.by = translateFieldList(ctx, mm, 'by', args.by);
1431
1481
  if (args.orderBy !== undefined)
1432
1482
  t.orderBy = translateOrderBy(ctx, mm, args.orderBy);
1433
1483
  if (args.having !== undefined)
1434
1484
  t.having = renameHaving(mm, args.having);
1435
1485
  if (args.distinctOn !== undefined)
1436
1486
  t.distinctOn = translateDistinctOn(ctx, mm, args.distinctOn);
1437
- if (typeof args.take === 'number')
1487
+ if (args.take !== undefined)
1438
1488
  t.limit = mapTake(args.take);
1439
- if (typeof args.skip === 'number')
1489
+ if (args.skip !== undefined)
1440
1490
  t.offset = args.skip;
1441
1491
  }
1442
1492
  return t;
@@ -1449,7 +1499,7 @@ function translateAggregateArgs(ctx, mm, args, isGroupBy) {
1449
1499
  * silently wrong on one with a `@map`.
1450
1500
  */
1451
1501
  function translateDistinctOn(ctx, mm, val) {
1452
- if (!isPlainObject(val))
1502
+ if (!isPlainRecord(val))
1453
1503
  return val;
1454
1504
  const out = { ...val };
1455
1505
  if (Array.isArray(val.columns))
@@ -1460,7 +1510,7 @@ function translateDistinctOn(ctx, mm, val) {
1460
1510
  }
1461
1511
  /** Rename field keys inside an aggregate block; `_count`'s `_all` passes through. */
1462
1512
  function renameAggBlock(mm, block, isCount) {
1463
- if (block === true || !isPlainObject(block))
1513
+ if (block === true || !isPlainRecord(block))
1464
1514
  return block;
1465
1515
  const out = {};
1466
1516
  for (const [key, val] of Object.entries(block)) {
@@ -1468,7 +1518,7 @@ function renameAggBlock(mm, block, isCount) {
1468
1518
  out._all = val;
1469
1519
  continue;
1470
1520
  }
1471
- out[renameField(mm, key)] = val;
1521
+ setOwn(out, renameField(mm, key), val);
1472
1522
  }
1473
1523
  return out;
1474
1524
  }
@@ -1483,7 +1533,7 @@ function renameAggBlock(mm, block, isCount) {
1483
1533
  */
1484
1534
  function renameHaving(mm, having, depth = 0) {
1485
1535
  assertTranslateDepth(depth, 'having');
1486
- if (!isPlainObject(having))
1536
+ if (!isPlainRecord(having))
1487
1537
  return having;
1488
1538
  const out = {};
1489
1539
  for (const [key, val] of Object.entries(having)) {
@@ -1495,7 +1545,7 @@ function renameHaving(mm, having, depth = 0) {
1495
1545
  out._count = val;
1496
1546
  continue;
1497
1547
  }
1498
- out[renameField(mm, key)] = val;
1548
+ setOwn(out, renameField(mm, key), val);
1499
1549
  }
1500
1550
  return out;
1501
1551
  }
@@ -1509,7 +1559,7 @@ function reshapeRowOrNull(ctx, mm, row) {
1509
1559
  return row == null ? null : reshapeRow(ctx, mm, row);
1510
1560
  }
1511
1561
  function reshapeRow(ctx, mm, row) {
1512
- if (!isPlainObject(row))
1562
+ if (!isPlainRecord(row))
1513
1563
  return row;
1514
1564
  const l = lookupsFor(ctx, mm);
1515
1565
  const out = {};
@@ -1518,7 +1568,7 @@ function reshapeRow(ctx, mm, row) {
1518
1568
  out._count = reshapeCount(l, val);
1519
1569
  continue;
1520
1570
  }
1521
- const rel = l.reverseRelations[key];
1571
+ const rel = ownLookup(l.reverseRelations, key);
1522
1572
  if (rel) {
1523
1573
  const target = relTargetModel(ctx, mm, key);
1524
1574
  let rv;
@@ -1528,25 +1578,25 @@ function reshapeRow(ctx, mm, row) {
1528
1578
  // map's cardinality is authoritative → first element or null.
1529
1579
  rv = rel.cardinality === 'one' ? (mapped.length ? mapped[0] : null) : mapped;
1530
1580
  }
1531
- else if (isPlainObject(val)) {
1581
+ else if (isPlainRecord(val)) {
1532
1582
  rv = target ? reshapeRow(ctx, target, val) : val;
1533
1583
  }
1534
1584
  else {
1535
1585
  rv = val; // null to-one
1536
1586
  }
1537
- out[rel.prismaName] = rv;
1587
+ setOwn(out, rel.prismaName, rv);
1538
1588
  continue;
1539
1589
  }
1540
1590
  let sv = val;
1541
1591
  if (typeof sv === 'string' && l.timeFields.has(key)) {
1542
1592
  sv = timeStringToDate(sv) ?? sv;
1543
1593
  }
1544
- out[l.identityFields ? key : (l.reverseFields[key] ?? key)] = sv;
1594
+ setOwn(out, l.identityFields ? key : (ownLookup(l.reverseFields, key) ?? key), sv);
1545
1595
  }
1546
1596
  return out;
1547
1597
  }
1548
1598
  function reshapeCount(l, count) {
1549
- if (!isPlainObject(count))
1599
+ if (!isPlainRecord(count))
1550
1600
  return count;
1551
1601
  const out = {};
1552
1602
  for (const [key, val] of Object.entries(count)) {
@@ -1554,13 +1604,13 @@ function reshapeCount(l, count) {
1554
1604
  out._all = val;
1555
1605
  continue;
1556
1606
  }
1557
- out[l.reverseRelations[key]?.prismaName ?? key] = val;
1607
+ setOwn(out, ownLookup(l.reverseRelations, key)?.prismaName ?? key, val);
1558
1608
  }
1559
1609
  return out;
1560
1610
  }
1561
1611
  /** Reshape an aggregate result: field keys inside blocks → prisma names. */
1562
1612
  function reshapeAggregate(ctx, mm, res) {
1563
- if (!isPlainObject(res))
1613
+ if (!isPlainRecord(res))
1564
1614
  return res;
1565
1615
  const l = lookupsFor(ctx, mm);
1566
1616
  const out = {};
@@ -1577,12 +1627,12 @@ function reshapeAggregate(ctx, mm, res) {
1577
1627
  if (typeof sv === 'string' && l.timeFields.has(key)) {
1578
1628
  sv = timeStringToDate(sv) ?? sv;
1579
1629
  }
1580
- out[l.identityFields ? key : (l.reverseFields[key] ?? key)] = sv;
1630
+ setOwn(out, l.identityFields ? key : (ownLookup(l.reverseFields, key) ?? key), sv);
1581
1631
  }
1582
1632
  return out;
1583
1633
  }
1584
1634
  function reshapeAggFieldBlock(l, block, isCount) {
1585
- if (!isPlainObject(block))
1635
+ if (!isPlainRecord(block))
1586
1636
  return block;
1587
1637
  const out = {};
1588
1638
  for (const [key, val] of Object.entries(block)) {
@@ -1590,13 +1640,13 @@ function reshapeAggFieldBlock(l, block, isCount) {
1590
1640
  out._all = val;
1591
1641
  continue;
1592
1642
  }
1593
- out[l.reverseFields[key] ?? key] = val;
1643
+ setOwn(out, ownLookup(l.reverseFields, key) ?? key, val);
1594
1644
  }
1595
1645
  return out;
1596
1646
  }
1597
1647
  /** Reshape a groupBy row: by-field keys + aggregate blocks → prisma names. */
1598
1648
  function reshapeGroupRow(ctx, mm, row) {
1599
- if (!isPlainObject(row))
1649
+ if (!isPlainRecord(row))
1600
1650
  return row;
1601
1651
  const l = lookupsFor(ctx, mm);
1602
1652
  const out = {};
@@ -1613,7 +1663,7 @@ function reshapeGroupRow(ctx, mm, row) {
1613
1663
  if (typeof sv === 'string' && l.timeFields.has(key)) {
1614
1664
  sv = timeStringToDate(sv) ?? sv;
1615
1665
  }
1616
- out[l.identityFields ? key : (l.reverseFields[key] ?? key)] = sv;
1666
+ setOwn(out, l.identityFields ? key : (ownLookup(l.reverseFields, key) ?? key), sv);
1617
1667
  }
1618
1668
  return out;
1619
1669
  }
@@ -1654,6 +1704,55 @@ class CompatPromise {
1654
1704
  function batchableOf(v) {
1655
1705
  return v instanceof CompatPromise ? v[COMPAT_DEFERRED] : undefined;
1656
1706
  }
1707
+ /** Warn-once namespace for a `$transaction` option this layer accepts and does not apply. */
1708
+ const TX_OPTION_WARN_NS = 'prismaCompatTxOption';
1709
+ /**
1710
+ * Every `$transaction` option this layer understands: Prisma's three, plus the
1711
+ * Turbine-native ones it forwards to core as written.
1712
+ */
1713
+ const TX_OPTION_KEYS = new Set([
1714
+ 'isolationLevel',
1715
+ 'timeout',
1716
+ 'maxWait',
1717
+ 'sessionContext',
1718
+ 'allowBuiltinSettings',
1719
+ 'readOnly',
1720
+ ]);
1721
+ /**
1722
+ * Check a `$transaction` options object and drop the one key core has no use
1723
+ * for. A key that is not an option used to be forwarded and ignored, so a
1724
+ * misspelled `isolationLevl` ran the transaction at the default isolation.
1725
+ * `maxWait` bounds how long Prisma waits for a pooled connection; here the
1726
+ * pool's own `connectionTimeoutMs` does that, so it is accepted, warned about
1727
+ * once, and not forwarded.
1728
+ */
1729
+ function resolveTxOptions(log, options) {
1730
+ if (options === undefined || options === null)
1731
+ return undefined;
1732
+ if (!isPlainRecord(options)) {
1733
+ throw new ValidationError(`prisma-compat: $transaction options must be an object, received ${describeValue(options)}.`);
1734
+ }
1735
+ const out = {};
1736
+ for (const [key, value] of Object.entries(options)) {
1737
+ if (value === undefined)
1738
+ continue;
1739
+ if (!TX_OPTION_KEYS.has(key)) {
1740
+ const suggestion = suggestKey(key, TX_OPTION_KEYS);
1741
+ throw new ValidationError(`prisma-compat: unknown $transaction option "${key}", it would be ignored.` +
1742
+ (suggestion ? ` Did you mean "${suggestion}"?` : '') +
1743
+ ` Valid options: ${[...TX_OPTION_KEYS].join(', ')}.`);
1744
+ }
1745
+ if (key === 'maxWait') {
1746
+ if (log.diagnostics.advice && shouldWarnOnce(TX_OPTION_WARN_NS, key)) {
1747
+ logTo(log.logger, 'warn', 'prisma-compat: the $transaction option "maxWait" has no effect here. Waiting for a pooled connection ' +
1748
+ "is bounded by the client's `connectionTimeoutMs` instead.", { code: 'prisma-compat-unknown-option', operation: '$transaction', key });
1749
+ }
1750
+ continue;
1751
+ }
1752
+ out[key] = value;
1753
+ }
1754
+ return out;
1755
+ }
1657
1756
  /**
1658
1757
  * Prisma's client-property spelling of a model name: first letter lowercased
1659
1758
  * (`model User` -> `prisma.user`). Returns null when the spelling is identical
@@ -1699,7 +1798,7 @@ function clientDefaultValue(kind) {
1699
1798
  /** Fill missing create-side client defaults (uuid / cuid / now / updatedAt), by Prisma field name. */
1700
1799
  function applyCreateDefaults(mm, data) {
1701
1800
  const cd = mm.clientDefaults;
1702
- if (!cd || !isPlainObject(data))
1801
+ if (!cd || !isPlainRecord(data))
1703
1802
  return data;
1704
1803
  let out = data;
1705
1804
  for (const [field, kind] of Object.entries(cd)) {
@@ -1714,7 +1813,7 @@ function applyCreateDefaults(mm, data) {
1714
1813
  /** Touch @updatedAt fields on the update side (Prisma sets them on every update). */
1715
1814
  function applyUpdateTouch(mm, data) {
1716
1815
  const cd = mm.clientDefaults;
1717
- if (!cd || !isPlainObject(data))
1816
+ if (!cd || !isPlainRecord(data))
1718
1817
  return data;
1719
1818
  let out = data;
1720
1819
  for (const [field, kind] of Object.entries(cd)) {
@@ -1728,8 +1827,8 @@ function applyUpdateTouch(mm, data) {
1728
1827
  }
1729
1828
  /** Whether TRANSLATED write data carries relation keys (nested-write shapes). */
1730
1829
  function hasNestedKeys(ctx, mm, data) {
1731
- const rels = ctx.schema.tables[mm.table]?.relations;
1732
- if (!rels || !isPlainObject(data))
1830
+ const rels = ownLookup(ctx.schema.tables, mm.table)?.relations;
1831
+ if (!rels || !isPlainRecord(data))
1733
1832
  return false;
1734
1833
  return Object.keys(data).some((k) => {
1735
1834
  if (!Object.hasOwn(rels, k))
@@ -1790,7 +1889,7 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1790
1889
  // itself, for the same reason (see the comment above): a compat promise that
1791
1890
  // is built and never awaited must produce no output at all.
1792
1891
  const translate = () => {
1793
- warnUnknownQueryOptions(modelName(ctx, mm), op, rawArgs);
1892
+ warnUnknownQueryOptions(ctx.log, modelName(ctx, mm), op, rawArgs);
1794
1893
  return translateRaw();
1795
1894
  };
1796
1895
  const batchable = batch
@@ -1857,10 +1956,15 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1857
1956
  });
1858
1957
  },
1859
1958
  createMany: (args) => defer('createMany', args, () => {
1959
+ // Prisma takes one row object or an array of them. Anything else used
1960
+ // to become zero rows, so the call "succeeded" with `count: 0`.
1860
1961
  const data = args.data;
1861
- const rows = Array.isArray(data)
1862
- ? data.map((d) => translateWriteData(ctx, mm, applyCreateDefaults(mm, d)))
1863
- : [];
1962
+ const list = Array.isArray(data) ? data : isPlainRecord(data) ? [data] : undefined;
1963
+ if (!list) {
1964
+ throw new ValidationError(`prisma-compat: createMany on model "${modelName(ctx, mm)}" requires \`data\`, a row object or an ` +
1965
+ `array of them, received ${describeValue(data)}.`);
1966
+ }
1967
+ const rows = list.map((d) => translateWriteData(ctx, mm, applyCreateDefaults(mm, d)));
1864
1968
  const t = { data: rows };
1865
1969
  applyNativeOptions(CREATE_MANY_OPTIONS, args, t);
1866
1970
  if (args.skipDuplicates)
@@ -1898,7 +2002,7 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1898
2002
  // `optimisticLock.field` is a FIELD NAME, so it is renamed rather than
1899
2003
  // copied: a blind passthrough would send the Prisma spelling into core
1900
2004
  // and break on any model whose column is `@map`ped.
1901
- if (isPlainObject(a.optimisticLock)) {
2005
+ if (isPlainRecord(a.optimisticLock)) {
1902
2006
  t.optimisticLock = {
1903
2007
  ...a.optimisticLock,
1904
2008
  field: renameField(mm, String(a.optimisticLock.field)),
@@ -1974,13 +2078,31 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1974
2078
  execInTx: async (table, t) => shape(await table(mm.table).upsert(t)),
1975
2079
  });
1976
2080
  },
1977
- count: (args = {}) => defer('count', args, () => {
1978
- const t = {};
1979
- applyNativeOptions(COUNT_OPTIONS, args, t);
1980
- if (args.where !== undefined)
1981
- t.where = translateWhere(ctx, mm, args.where);
1982
- return t;
1983
- }, (qi, t) => qi.count(t), { build: (qi, t) => qi.buildCount(t), reshape: (raw) => raw }),
2081
+ count: (args = {}) => {
2082
+ const a = args;
2083
+ // `count({ select })` answers per field (`{ _all, title }`, a non-null
2084
+ // count per field), which is core's aggregate `_count` record form.
2085
+ if (isPlainRecord(a.select)) {
2086
+ const countOf = (raw) => reshapeAggregate(ctx, mm, raw)._count;
2087
+ return defer('count', args, () => {
2088
+ refusePagedAggregate('count', a);
2089
+ const t = {};
2090
+ applyNativeOptions(AGGREGATE_OPTIONS, a, t);
2091
+ if (a.where !== undefined)
2092
+ t.where = translateWhere(ctx, mm, a.where);
2093
+ t._count = renameAggBlock(mm, a.select, true);
2094
+ return t;
2095
+ }, (qi, t) => qi.aggregate(t).then(countOf), { build: (qi, t) => qi.buildAggregate(t), reshape: countOf });
2096
+ }
2097
+ return defer('count', args, () => {
2098
+ refusePagedAggregate('count', a);
2099
+ const t = {};
2100
+ applyNativeOptions(COUNT_OPTIONS, a, t);
2101
+ if (a.where !== undefined)
2102
+ t.where = translateWhere(ctx, mm, a.where);
2103
+ return t;
2104
+ }, (qi, t) => qi.count(t), { build: (qi, t) => qi.buildCount(t), reshape: (raw) => raw });
2105
+ },
1984
2106
  aggregate: (args) => defer('aggregate', args, () => translateAggregateArgs(ctx, mm, args, false), (qi, t) => qi.aggregate(t).then((r) => reshapeAggregate(ctx, mm, r)), { build: (qi, t) => qi.buildAggregate(t), reshape: (raw) => reshapeAggregate(ctx, mm, raw) }),
1985
2107
  groupBy: (args) => defer('groupBy', args, () => translateAggregateArgs(ctx, mm, args, true), (qi, t) => qi.groupBy(t).then((rows) => rows.map((r) => reshapeGroupRow(ctx, mm, r))), {
1986
2108
  build: (qi, t) => qi.buildGroupBy(t),
@@ -2095,8 +2217,9 @@ const JUNCTION_WARN_NS = 'prismaCompatJunction';
2095
2217
  * `$transaction` must not turn the transaction surface into a delegate).
2096
2218
  *
2097
2219
  * Every skip except the mapped-table one loses a capability the caller might
2098
- * expect, so those warn once per junction name in dev; a table that a model
2099
- * already maps to is not a loss (the model IS that table's accessor).
2220
+ * expect, so those warn once per junction name (a construction warning, so
2221
+ * always on); a table that a model already maps to is not a loss (the model IS
2222
+ * that table's accessor).
2100
2223
  */
2101
2224
  function junctionModels(ctx, map, tableToModel) {
2102
2225
  const out = [];
@@ -2117,16 +2240,16 @@ function junctionModels(ctx, map, tableToModel) {
2117
2240
  if (rel.type !== 'manyToMany')
2118
2241
  continue;
2119
2242
  const name = rel.through?.table;
2120
- if (!name || !ctx.schema.tables[name] || tableToModel.has(name))
2243
+ if (!name || !ownLookup(ctx.schema.tables, name) || tableToModel.has(name))
2121
2244
  continue;
2122
2245
  if (registered.has(name))
2123
2246
  continue;
2124
2247
  if (seen.has(name)) {
2125
- if (process.env.NODE_ENV !== 'production' && shouldWarnOnce(JUNCTION_WARN_NS, name)) {
2126
- console.warn(`[turbine] prisma-compat: the many-to-many junction table "${name}" collides with an ` +
2248
+ if (shouldWarnOnce(JUNCTION_WARN_NS, name)) {
2249
+ logTo(ctx.log.logger, 'warn', `prisma-compat: the many-to-many junction table "${name}" collides with an ` +
2127
2250
  'existing client member of the same name, so no junction accessor was created for it. ' +
2128
2251
  'Reach its rows through the owning model relation (a nested connect / disconnect / set on the ' +
2129
- 'related model), or through $queryRaw / $executeRaw.');
2252
+ 'related model), or through $queryRaw / $executeRaw.', { code: 'prisma-compat-junction-collision', table: name });
2130
2253
  }
2131
2254
  continue;
2132
2255
  }
@@ -2261,15 +2384,19 @@ export function createPrismaCompatClient(client, map, options = {}) {
2261
2384
  // the real client satisfies it structurally (the cast just relaxes the strict
2262
2385
  // callback-param variance on `$transaction`).
2263
2386
  const db = client;
2264
- // Fire-and-forget, dev-only, once per process: is this map still describing
2265
- // the schema on disk? Nothing else in the system ever asks.
2266
- warnStalePrismaMap(map);
2387
+ // Log through the wrapped client's logger and diagnostics. A stand-in client
2388
+ // without them (a test stub) gets the console and the environment's mode.
2389
+ const log = client[CLIENT_LOG_CONTEXT] ?? { logger: consoleLogger, diagnostics: resolveDiagnostics() };
2390
+ // Fire-and-forget, once per process: is this map still describing the schema
2391
+ // on disk? Nothing else in the system ever asks.
2392
+ warnStalePrismaMap(map, log);
2267
2393
  const tableToModel = new Map();
2268
2394
  for (const [prismaModel, mm] of Object.entries(map.models))
2269
2395
  tableToModel.set(mm.table, prismaModel);
2270
2396
  const ctx = {
2271
2397
  map,
2272
2398
  schema: db.schema,
2399
+ log,
2273
2400
  tableToModel,
2274
2401
  lookups: new Map(),
2275
2402
  options: {
@@ -2361,7 +2488,15 @@ export function createPrismaCompatClient(client, map, options = {}) {
2361
2488
  }
2362
2489
  const base = {
2363
2490
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
2364
- $transaction: ((arg, txOptions) => {
2491
+ $transaction: ((arg, rawTxOptions) => {
2492
+ // Rejected, never thrown: Prisma's $transaction is always thenable.
2493
+ let txOptions;
2494
+ try {
2495
+ txOptions = resolveTxOptions(ctx.log, rawTxOptions);
2496
+ }
2497
+ catch (err) {
2498
+ return Promise.reject(decorate(err, ctx.options.prismaErrorCodes));
2499
+ }
2365
2500
  // Array (lazy batch) form. Wrapped so validation/build errors REJECT the
2366
2501
  // returned promise (Prisma's $transaction is always thenable) rather than
2367
2502
  // throwing synchronously.
@@ -2388,7 +2523,7 @@ export function createPrismaCompatClient(client, map, options = {}) {
2388
2523
  }, txOptions);
2389
2524
  }
2390
2525
  const deferreds = batchables.map((b) => b.build());
2391
- const results = (await db.$transaction(deferreds));
2526
+ const results = (await db.$transaction(deferreds, txOptions));
2392
2527
  return results.map((raw, i) => batchables[i].reshape(raw));
2393
2528
  }
2394
2529
  catch (err) {
@@ -2401,10 +2536,10 @@ export function createPrismaCompatClient(client, map, options = {}) {
2401
2536
  return db.$transaction((tx) => {
2402
2537
  const txDelegates = {};
2403
2538
  for (const [prismaModel, mm] of delegateModels) {
2404
- txDelegates[prismaModel] = extendDelegate(makeDelegate(ctx, mm, () => tx.table(mm.table), (fn) => fn((n) => tx.table(n))), prismaModel, exts);
2539
+ setOwn(txDelegates, prismaModel, extendDelegate(makeDelegate(ctx, mm, () => tx.table(mm.table), (fn) => fn((n) => tx.table(n))), prismaModel, exts));
2405
2540
  const alias = prismaPropertyAlias(prismaModel);
2406
- if (alias && !(alias in map.models) && !(alias in txDelegates)) {
2407
- txDelegates[alias] = txDelegates[prismaModel];
2541
+ if (alias && !Object.hasOwn(map.models, alias) && !Object.hasOwn(txDelegates, alias)) {
2542
+ setOwn(txDelegates, alias, ownLookup(txDelegates, prismaModel));
2408
2543
  }
2409
2544
  }
2410
2545
  // Raw SQL on the transaction's own connection. Prisma's tx client
@@ -2431,13 +2566,13 @@ export function createPrismaCompatClient(client, map, options = {}) {
2431
2566
  // key from the map, so no dynamic-access proxy is needed.
2432
2567
  const result = { ...base };
2433
2568
  for (const [prismaModel, delegate] of delegates)
2434
- result[prismaModel] = delegate;
2569
+ setOwn(result, prismaModel, delegate);
2435
2570
  // Prisma-spelling aliases (`prisma.user` for `model User`). Skipped when the
2436
2571
  // lowercased name is itself a model or already taken (never shadow a real key).
2437
2572
  for (const [prismaModel, delegate] of delegates) {
2438
2573
  const alias = prismaPropertyAlias(prismaModel);
2439
- if (alias && !(alias in result))
2440
- result[alias] = delegate;
2574
+ if (alias && !Object.hasOwn(result, alias))
2575
+ setOwn(result, alias, delegate);
2441
2576
  }
2442
2577
  // Extension `client` members last: every name was checked against the real
2443
2578
  // client keys in applyExtension, so this can never overwrite a delegate.