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
@@ -68,16 +68,22 @@
68
68
  * - Fluent relation chaining (`prisma.user.findUnique().posts()`).
69
69
  * - Accelerate / Pulse / driver-adapter preview features, the Mongo API, and the
70
70
  * `prisma migrate`/`db` CLI family (Turbine ships its own migrations).
71
- * - **Inclusive bare cursors whose field is not the sort key**, see
72
- * {@link translateCursor}. A bare Prisma cursor is inclusive; translating it
73
- * correctly needs the anchor row's sort-key value. When the cursor field is
74
- * the single `orderBy` field (or the cursor is the single-column PK with no
75
- * `orderBy`) it compiles to a `gte`/`lte` keyset predicate; **otherwise it
76
- * throws** an {@link UnsupportedFeatureError} rather than emit an off-by-one
77
- * page. Pair the cursor with `skip: 1` (Prisma's idiom) for the exact
78
- * exclusive-cursor + `offset` translation.
79
- * - **Negative `take`** (take-from-end) and **`skip` on a nested relation
80
- * include** throw, Turbine's `with` clause has no offset and no reverse-take.
71
+ * - **Negative `take`** (take-from-end) throws: Turbine has no reverse-take.
72
+ * A **`cursor` on a nested relation include** throws: `with` has no cursor
73
+ * (`take` / `skip` there page each parent's rows, as in Prisma).
74
+ * - **A filtered relation count** (`_count: { select: { posts: { where } } }`)
75
+ * and **`distinct` on a nested include** throw `UnsupportedFeatureError`:
76
+ * core counts and includes a relation whole, so honoring either would mean
77
+ * returning a wrong number or duplicate rows. The message names the
78
+ * alternative (a `count` / `findMany` on the related model).
79
+ * - **Nothing is accepted and dropped.** Every option on an included relation
80
+ * and every key under `_count` is translated, refused by name (the two
81
+ * above, `cursor`), or refused as unknown (`ValidationError`, with a
82
+ * suggestion): a misspelled `wher`, a Turbine spelling like `limit`, a
83
+ * relation `_count` does not know. The same holds for `$transaction` options
84
+ * (`maxWait` is accepted, warned about once, and not applied: the pool's
85
+ * `connectionTimeoutMs` bounds the wait) and for the keys of a nested write's
86
+ * `{ where, data }` / `{ where, create, update }` payloads.
81
87
  * - **`limit` on `updateMany` / `deleteMany`** (Prisma 6.7+) throws. Turbine has
82
88
  * no row-bounded mass mutation, and dropping a SAFETY BOUND with a warning
83
89
  * would turn "change at most 10 rows" into "change every matching row".
@@ -88,9 +94,10 @@
88
94
  * either (or any `include` on a write) THROWS, since a write returns its own
89
95
  * row only. `omit` on READS is translated to core's own `omit`, see
90
96
  * {@link translateOmit}.
91
- * - **`select` on `count`** and **`orderBy` / `cursor` / `take` / `skip` on
92
- * `aggregate`** are accepted and IGNORED (they are legitimate Prisma, so they
93
- * never warn); a plain number / the aggregate comes back.
97
+ * - **`take` / `skip` / `cursor` on `count` and `aggregate`** throw: Prisma
98
+ * computes those over one page of rows and Turbine has no paged count. An
99
+ * `orderBy` alone changes nothing there and is accepted. `count({ select })`
100
+ * returns Prisma's per-field object (`{ _all, field }`).
94
101
  *
95
102
  * ## Type dependencies (0.41.0)
96
103
  *
@@ -156,10 +163,14 @@ var __importStar = (this && this.__importStar) || (function () {
156
163
  Object.defineProperty(exports, "__esModule", { value: true });
157
164
  exports.CLIENT_RESERVED_KEYS = exports.COMPAT_DEFERRED = exports.PRISMA_ARG_KEYS = exports.Prisma = void 0;
158
165
  exports.createPrismaCompatClient = createPrismaCompatClient;
166
+ const diagnostics_js_1 = require("./diagnostics.js");
159
167
  const errors_js_1 = require("./errors.js");
168
+ const logger_js_1 = require("./logger.js");
160
169
  const nested_write_js_1 = require("./nested-write.js");
161
170
  const prisma_schema_fingerprint_js_1 = require("./prisma-schema-fingerprint.js");
171
+ const filters_js_1 = require("./query/filters.js");
162
172
  const index_js_1 = require("./query/index.js");
173
+ const normalize_args_js_1 = require("./query/normalize-args.js");
163
174
  const utils_js_1 = require("./query/utils.js");
164
175
  const warn_registry_js_1 = require("./query/warn-registry.js");
165
176
  const where_compile_js_1 = require("./query/where-compile.js");
@@ -270,19 +281,21 @@ function timeStringToDate(v) {
270
281
  return new Date(Date.UTC(1970, 0, 1, Number(m[1]), Number(m[2]), Number(m[3]), ms));
271
282
  }
272
283
  function buildLookups(ctx, mm) {
273
- const reverseFields = {};
284
+ // Keyed by field and relation names, so prototype-less: a field called
285
+ // `constructor` is an own key here, never Object.prototype's member.
286
+ const reverseFields = Object.create(null);
274
287
  let identityFields = true;
275
288
  for (const [prismaField, turbineField] of Object.entries(mm.fields)) {
276
289
  reverseFields[turbineField] = prismaField;
277
290
  if (prismaField !== turbineField)
278
291
  identityFields = false;
279
292
  }
280
- const reverseRelations = {};
293
+ const reverseRelations = Object.create(null);
281
294
  for (const [prismaRel, rel] of Object.entries(mm.relations)) {
282
295
  reverseRelations[rel.name] = { prismaName: prismaRel, cardinality: rel.cardinality };
283
296
  }
284
297
  const timeFields = new Set();
285
- for (const col of ctx.schema.tables[mm.table]?.columns ?? []) {
298
+ for (const col of (0, utils_js_1.ownLookup)(ctx.schema.tables, mm.table)?.columns ?? []) {
286
299
  if (TIME_PG_TYPES.has(col.pgType))
287
300
  timeFields.add(col.field);
288
301
  }
@@ -297,62 +310,18 @@ function lookupsFor(ctx, mm) {
297
310
  return l;
298
311
  }
299
312
  /** Resolve a turbine relation's target Prisma model map (for nested translation). */
300
- /**
301
- * A `where` fragment no row satisfies, for Prisma's empty `OR`.
302
- *
303
- * `{ <key>: { in: [] } }`, which core compiles to `= ANY($n)` with an empty
304
- * array: false for every row, NULL-valued ones included, and one ordinary bound
305
- * param rather than a literal spliced into the SQL. The column is the first
306
- * primary-key column, else the table's first column, so it is always a real
307
- * column of this table and the predicate stays a plain scalar comparison the
308
- * cache fingerprints like any other.
309
- *
310
- * A table this layer cannot name a column of THROWS. The fallback used to be
311
- * `{}`, an empty `where` fragment - which is not "no rows" but its exact
312
- * opposite, EVERY row, and it would have been merged in silently beside the
313
- * caller's other predicates. A sentinel builder that cannot build a sentinel
314
- * has not succeeded, and the one shape it exists to compile is the one where
315
- * answering "all of them" instead of "none of them" is worst.
316
- */
317
- function matchNothing(ctx, mm) {
318
- const meta = ctx.schema.tables[mm.table];
319
- const column = meta?.primaryKey?.[0] ?? meta?.allColumns?.[0];
320
- if (!meta || !column) {
321
- throw new errors_js_1.ValidationError(`An empty \`OR\` on model ${mm.table} means "no rows", and compiling that needs one real column ` +
322
- `of the table, but the schema metadata for "${mm.table}" lists none. Regenerate the client ` +
323
- '(`npx turbine generate`) so the table carries its columns, or drop the empty `OR`.');
324
- }
325
- return { [meta.reverseColumnMap?.[column] ?? column]: { in: [] } };
326
- }
327
313
  function relTargetModel(ctx, mm, turbineRel) {
328
- const rd = ctx.schema.tables[mm.table]?.relations?.[turbineRel];
314
+ const relations = (0, utils_js_1.ownLookup)(ctx.schema.tables, mm.table)?.relations;
315
+ const rd = relations ? (0, utils_js_1.ownLookup)(relations, turbineRel) : undefined;
329
316
  if (!rd)
330
317
  return undefined;
331
318
  const modelName = ctx.tableToModel.get(rd.to);
332
- return modelName ? ctx.map.models[modelName] : undefined;
319
+ return modelName ? (0, utils_js_1.ownLookup)(ctx.map.models, modelName) : undefined;
333
320
  }
334
321
  // ---------------------------------------------------------------------------
335
322
  // Argument translation
336
323
  // ---------------------------------------------------------------------------
337
324
  const COMBINATORS = new Set(['AND', 'OR', 'NOT']);
338
- /**
339
- * The to-many half of the relation-filter wrapper list.
340
- *
341
- * DRIFT RISK, and this is the third copy of that list in the tree. The canonical
342
- * NAMED one is `RELATION_FILTER_WRAPPERS` in `cli/pii-predicate-guard.ts`
343
- * (`some`/`none`/`every`/`is`/`isNot`), and a second is inlined in
344
- * `query/where-compile.ts` as `'some' in x || 'every' in x || …`. This copy is
345
- * the worst of the three because it is SPLIT: the to-many quantifiers are here,
346
- * and `is` / `isNot` are two inline `k === …` tests in
347
- * {@link translateRelationFilter}, so half of it does not read as a list and a
348
- * grep for the list will not find it. A wrapper the compiler learns and this set
349
- * does not is a relation body that reaches core untranslated, i.e. with Prisma
350
- * field names still in it.
351
- *
352
- * The fix is an export from `query/filters.ts` that all three import; until that
353
- * exists, these comments are what hold them in step.
354
- */
355
- const RELATION_QUANTIFIERS = new Set(['some', 'every', 'none']);
356
325
  /**
357
326
  * Refuse a translation walk that has nested past {@link MAX_WHERE_DEPTH}.
358
327
  *
@@ -392,16 +361,8 @@ function assertTranslateDepth(depth, clause) {
392
361
  `query needs, and an unbounded walk over caller-supplied nesting is a stack-overflow surface, so it is ` +
393
362
  `refused. If this is a generated argument, flatten it: a single array of N entries is one level, not N.`);
394
363
  }
395
- /** Whether a value is a plain object usable as a compound-unique selector. */
396
- function isPlainObject(v) {
397
- return (typeof v === 'object' &&
398
- v !== null &&
399
- !Array.isArray(v) &&
400
- !(v instanceof Date) &&
401
- !(typeof Buffer !== 'undefined' && Buffer.isBuffer(v)));
402
- }
403
364
  function renameField(mm, prismaField) {
404
- return mm.fields[prismaField] ?? prismaField;
365
+ return (0, utils_js_1.ownLookup)(mm.fields, prismaField) ?? prismaField;
405
366
  }
406
367
  /** The turbine arg surface each operation compiles into. */
407
368
  const OPERATION_TABLES = {
@@ -521,8 +482,8 @@ const KNOWN_KEYS = (() => {
521
482
  return out;
522
483
  })();
523
484
  /**
524
- * Dev-mode notice for a key on a delegate's args object that this operation has
525
- * no meaning for.
485
+ * Per-query advice for a key on a delegate's args object that this operation
486
+ * has no meaning for.
526
487
  *
527
488
  * An unrecognized key is silently ignored (JavaScript objects have no schema),
528
489
  * which makes a typo, a turbine spelling, and a genuinely missing feature all
@@ -534,13 +495,14 @@ const KNOWN_KEYS = (() => {
534
495
  * into a failing one on upgrade over one stray key, and a key from a Prisma
535
496
  * major newer than {@link PRISMA_ARG_KEYS} would be exactly that key. The whole
536
497
  * body is wrapped so a hostile or exotic args object (a Proxy whose `ownKeys`
537
- * throws) can never be the reason a query fails. Dev-only and once per
538
- * `model.operation.key` per process, like every other advisory here.
498
+ * throws) can never be the reason a query fails. Off when the client's
499
+ * `diagnostics` turn advice off, and once per `model.operation.key` per
500
+ * process, like every other advisory here.
539
501
  */
540
- function warnUnknownQueryOptions(model, op, args) {
541
- if (process.env.NODE_ENV === 'production')
502
+ function warnUnknownQueryOptions(log, model, op, args) {
503
+ if (!log.diagnostics.advice)
542
504
  return;
543
- if (!isPlainObject(args))
505
+ if (!(0, filters_js_1.isPlainRecord)(args))
544
506
  return;
545
507
  try {
546
508
  const known = KNOWN_KEYS[op];
@@ -556,13 +518,19 @@ function warnUnknownQueryOptions(model, op, args) {
556
518
  if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.unknownQueryOption, `${model}.${op}.${key}`))
557
519
  continue;
558
520
  if (alias) {
559
- console.warn(`[turbine] prisma-compat: "${key}" is Turbine's spelling and is ignored here;` +
560
- ` prisma-compat takes Prisma's "${alias}". (${model}.${op})`);
521
+ (0, logger_js_1.logTo)(log.logger, 'warn', `prisma-compat: "${key}" is Turbine's spelling and is ignored here;` +
522
+ ` prisma-compat takes Prisma's "${alias}". (${model}.${op})`, { code: 'prisma-compat-native-spelling', table: model, operation: op, key, prismaKey: alias });
561
523
  continue;
562
524
  }
563
525
  const suggestion = (0, utils_js_1.suggestKey)(key, known);
564
- console.warn(`[turbine] prisma-compat: unknown option "${key}" in ${model}.${op}(), it is ignored.` +
565
- (suggestion ? ` Did you mean "${suggestion}"?` : ''));
526
+ (0, logger_js_1.logTo)(log.logger, 'warn', `prisma-compat: unknown option "${key}" in ${model}.${op}(), it is ignored.` +
527
+ (suggestion ? ` Did you mean "${suggestion}"?` : ''), {
528
+ code: 'prisma-compat-unknown-option',
529
+ table: model,
530
+ operation: op,
531
+ key,
532
+ ...(suggestion ? { suggestion } : {}),
533
+ });
566
534
  }
567
535
  }
568
536
  catch {
@@ -571,8 +539,8 @@ function warnUnknownQueryOptions(model, op, args) {
571
539
  }
572
540
  }
573
541
  /**
574
- * Dev-mode notice that the `prisma-map.ts` driving this client no longer matches
575
- * the Prisma schema it was generated from.
542
+ * Notice that the `prisma-map.ts` driving this client no longer matches the
543
+ * Prisma schema it was generated from.
576
544
  *
577
545
  * Nothing re-runs `turbine migrate-from-prisma`. When the schema moves on, the
578
546
  * map does not, and the adapter keeps translating the names it has: a model
@@ -587,19 +555,26 @@ function warnUnknownQueryOptions(model, op, args) {
587
555
  * - **Any failure is silence.** A missing file is the NORMAL deployed state
588
556
  * (nobody ships `prisma/` to production) and so are a bundled runtime with no
589
557
  * `node:fs` and a read that is refused. None of them is evidence of drift.
590
- * - **Production is skipped entirely**, before anything is imported or read.
591
- * - **Once per process per path**, via the shared registry, so a process that
592
- * builds a client per request says it once.
558
+ * - **Always on**, like every construction warning: a stale map loses models
559
+ * in production too. In production mode the file is READ once per process
560
+ * per path, so a process that builds a client per request does not read it
561
+ * per request; in development it is re-read per client, so a schema edited
562
+ * under a running dev server is still noticed.
563
+ * - **Warned once per process per path**, via the shared registry.
593
564
  *
594
565
  * @param map - the map under test; a map with no `source` (hand-written, or
595
566
  * emitted before 0.60) is skipped, since absence carries no information.
596
567
  */
597
- function warnStalePrismaMap(map) {
568
+ function warnStalePrismaMap(map, log) {
598
569
  const source = map.source;
599
570
  if (!source || typeof source.path !== 'string' || typeof source.hash !== 'string')
600
571
  return;
601
- if (typeof process === 'undefined' || process.env?.NODE_ENV === 'production')
572
+ // No `process`, no `node:fs`: an edge runtime has no file to compare.
573
+ if (typeof process === 'undefined')
602
574
  return;
575
+ if (log.diagnostics.mode === 'production' && !(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.stalePrismaMap, `checked:${source.path}`)) {
576
+ return;
577
+ }
603
578
  void (async () => {
604
579
  try {
605
580
  // Dynamic so a bundle that never reaches this line never needs `node:fs`.
@@ -609,9 +584,9 @@ function warnStalePrismaMap(map) {
609
584
  return;
610
585
  if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.stalePrismaMap, source.path))
611
586
  return;
612
- console.warn(`[turbine] prisma-compat: ${source.path} has changed since prisma-map.ts was generated,` +
587
+ (0, logger_js_1.logTo)(log.logger, 'warn', `prisma-compat: ${source.path} has changed since prisma-map.ts was generated,` +
613
588
  ' so any model, field, relation or compound-unique added or renamed since then is missing' +
614
- ' from the compat client. Re-run: turbine migrate-from-prisma');
589
+ ' from the compat client. Re-run: turbine migrate-from-prisma', { code: 'prisma-map-stale', path: source.path });
615
590
  }
616
591
  catch {
617
592
  // No file, no `node:fs`, no permission. None of these means the map is
@@ -627,7 +602,7 @@ function warnStalePrismaMap(map) {
627
602
  * bound turns "change at most 10 rows" into "change every row".
628
603
  */
629
604
  function refuseRowLimit(model, op, args) {
630
- if (!isPlainObject(args) || args.limit === undefined)
605
+ if (!(0, filters_js_1.isPlainRecord)(args) || args.limit === undefined)
631
606
  return;
632
607
  throw new errors_js_1.UnsupportedFeatureError(`\`limit\` on ${op} (row-bounded mass mutation)`, 'prisma-compat', `Turbine has no row-bounded ${op}; select the rows you mean with \`where\`` +
633
608
  ` (on ${model}.${op}), or page them and mutate by primary key.`);
@@ -661,33 +636,11 @@ function mapRelationLoadStrategy(value) {
661
636
  */
662
637
  function translateWhere(ctx, mm, where, depth = 0) {
663
638
  assertTranslateDepth(depth, 'where');
664
- if (!isPlainObject(where))
639
+ if (!(0, filters_js_1.isPlainRecord)(where))
665
640
  return where;
666
641
  const out = {};
667
- // The empty-`OR` sentinel, held aside rather than merged into `out`. At most
668
- // one per level, since `OR` is a single object key.
669
- let matchNone;
670
642
  for (const [key, val] of Object.entries(where)) {
671
643
  if (COMBINATORS.has(key)) {
672
- // An EMPTY `OR` is false in Prisma (no branch can match), while an empty
673
- // `AND` / `NOT` is true. Core drops an empty combinator entirely, so the
674
- // same call returned EVERY row here: the classic trigger is
675
- // `OR: ids.map(...)` over a selection that turned out to be empty, i.e.
676
- // "none of these" answered with "all of them". Compiled to a predicate
677
- // that matches nothing so this layer keeps Prisma's meaning; core's own
678
- // semantics are unchanged and documented separately.
679
- //
680
- // Held aside, NOT `Object.assign`ed onto `out`: the sentinel is keyed on
681
- // a real column of the table, so merging it made two predicates on one
682
- // key collide and JavaScript's key ORDER decided which survived.
683
- // `{ OR: [], id: 5 }` kept the caller's `id = 5` and dropped the
684
- // sentinel (returning the row Prisma excludes); `{ id: 5, OR: [] }` kept
685
- // the sentinel and dropped the caller's `id = 5`. Same query, same
686
- // meaning, opposite predicate.
687
- if (key === 'OR' && Array.isArray(val) && val.length === 0) {
688
- matchNone = matchNothing(ctx, mm);
689
- continue;
690
- }
691
644
  // An `AND` / `OR` array of N conditions is ONE level, not N: the elements
692
645
  // are siblings, so they all translate at the same incremented depth.
693
646
  out[key] = Array.isArray(val)
@@ -696,18 +649,18 @@ function translateWhere(ctx, mm, where, depth = 0) {
696
649
  continue;
697
650
  }
698
651
  // Compound-unique selector (custom or default Prisma name).
699
- const compound = mm.compoundUniques[key];
700
- if (compound && isPlainObject(val) && !mm.fields[key] && !mm.relations[key]) {
652
+ const compound = (0, utils_js_1.ownLookup)(mm.compoundUniques, key);
653
+ if (compound && (0, filters_js_1.isPlainRecord)(val) && !(0, utils_js_1.ownLookup)(mm.fields, key) && !(0, utils_js_1.ownLookup)(mm.relations, key)) {
701
654
  const inner = {};
702
655
  for (const [pk, pv] of Object.entries(val))
703
- inner[renameField(mm, pk)] = pv;
656
+ (0, filters_js_1.setOwn)(inner, renameField(mm, pk), pv);
704
657
  // Turbine's core derives the selector name from the underscore-join of the
705
658
  // member FIELD names, so re-key custom names onto that canonical form.
706
- out[compound.join('_')] = inner;
659
+ (0, filters_js_1.setOwn)(out, compound.join('_'), inner);
707
660
  continue;
708
661
  }
709
662
  // Relation filter.
710
- const rel = mm.relations[key];
663
+ const rel = (0, utils_js_1.ownLookup)(mm.relations, key);
711
664
  if (rel) {
712
665
  // Prisma spells "this to-one relation has no related row" as a bare
713
666
  // `null` on the relation key; core spells it `{ is: null }` and answers a
@@ -717,44 +670,28 @@ function translateWhere(ctx, mm, where, depth = 0) {
717
670
  // has no bare-null shape there, so core still refuses them by name rather
718
671
  // than this layer inventing a meaning.
719
672
  if (val === null && rel.cardinality === 'one') {
720
- out[rel.name] = { is: null };
673
+ (0, filters_js_1.setOwn)(out, rel.name, { is: null });
721
674
  continue;
722
675
  }
723
676
  const target = relTargetModel(ctx, mm, rel.name);
724
- out[rel.name] = translateRelationFilter(ctx, target, val, depth + 1);
677
+ (0, filters_js_1.setOwn)(out, rel.name, translateRelationFilter(ctx, target, val, depth + 1));
725
678
  continue;
726
679
  }
727
680
  // Scalar field, key renamed, value (literal or operator object) passes
728
681
  // through unchanged (Prisma operator names match Turbine's).
729
- out[renameField(mm, key)] = val;
682
+ (0, filters_js_1.setOwn)(out, renameField(mm, key), val);
730
683
  }
731
- if (!matchNone)
732
- return out;
733
- // Nothing else at this level: the sentinel IS the where, no wrapper needed.
734
- if (Object.keys(out).length === 0)
735
- return matchNone;
736
- // Otherwise conjoin, so neither half can overwrite the other whatever order
737
- // the caller wrote the keys in. BRANDED, because an `AND` array is the shape
738
- // that makes a statement unnamed (a caller-written combinator has a
739
- // caller-sized arity); this one is Turbine's, with a fixed arity of two, and
740
- // an unbranded wrapper here would quietly take every compat query carrying an
741
- // empty `OR` off named prepared statements. The caller's own combinators sit
742
- // INSIDE `out` and are walked at the next level with their own brand check,
743
- // so they still count.
744
- return (0, utils_js_1.markInternalCombinator)({ AND: [out, matchNone] });
684
+ return out;
745
685
  }
746
686
  function translateRelationFilter(ctx, target, val, depth) {
747
- if (!isPlainObject(val))
687
+ if (!(0, filters_js_1.isPlainRecord)(val))
748
688
  return val;
749
689
  const keys = Object.keys(val);
750
- const hasQuantifier = keys.some((k) => RELATION_QUANTIFIERS.has(k) || k === 'is' || k === 'isNot');
690
+ const hasQuantifier = keys.some((k) => filters_js_1.RELATION_FILTER_WRAPPER_SET.has(k));
751
691
  if (hasQuantifier) {
752
692
  const out = {};
753
693
  for (const [k, v] of Object.entries(val)) {
754
- out[k] =
755
- target && (RELATION_QUANTIFIERS.has(k) || k === 'is' || k === 'isNot')
756
- ? translateWhere(ctx, target, v, depth + 1)
757
- : v;
694
+ (0, filters_js_1.setOwn)(out, k, target && filters_js_1.RELATION_FILTER_WRAPPER_SET.has(k) ? translateWhere(ctx, target, v, depth + 1) : v);
758
695
  }
759
696
  return out;
760
697
  }
@@ -778,7 +715,7 @@ function translateOrderBy(ctx, mm, ob, depth = 0) {
778
715
  assertTranslateDepth(depth, 'orderBy');
779
716
  if (Array.isArray(ob))
780
717
  return ob.map((o) => translateOrderBy(ctx, mm, o, depth + 1));
781
- if (!isPlainObject(ob))
718
+ if (!(0, filters_js_1.isPlainRecord)(ob))
782
719
  return ob;
783
720
  const out = {};
784
721
  for (const [key, val] of Object.entries(ob)) {
@@ -786,20 +723,23 @@ function translateOrderBy(ctx, mm, ob, depth = 0) {
786
723
  out._count = val;
787
724
  continue;
788
725
  }
789
- const rel = mm.relations[key];
726
+ const rel = (0, utils_js_1.ownLookup)(mm.relations, key);
790
727
  if (rel) {
791
728
  const target = relTargetModel(ctx, mm, rel.name);
792
729
  out[rel.name] =
793
- isPlainObject(val) && !('_count' in val) && target ? translateOrderBy(ctx, target, val, depth + 1) : val;
730
+ (0, filters_js_1.isPlainRecord)(val) && !('_count' in val) && target ? translateOrderBy(ctx, target, val, depth + 1) : val;
794
731
  continue;
795
732
  }
796
- out[renameField(mm, key)] = val;
733
+ (0, filters_js_1.setOwn)(out, renameField(mm, key), val);
797
734
  }
798
735
  return out;
799
736
  }
800
- /** Map a Prisma `take` to a Turbine `limit`. Negative take is unsupported. */
737
+ /**
738
+ * Map a Prisma `take` to a Turbine `limit`. Negative take is unsupported. Any
739
+ * other value goes to core as written, which refuses a non-integer.
740
+ */
801
741
  function mapTake(take) {
802
- if (take < 0) {
742
+ if (typeof take === 'number' && take < 0) {
803
743
  throw new errors_js_1.UnsupportedFeatureError('negative take (take-from-end pagination)', 'prisma-compat', 'Turbine has no reverse-take; reverse your orderBy and use a positive take instead.');
804
744
  }
805
745
  return take;
@@ -824,11 +764,11 @@ function translateOmit(ctx, mm, omit) {
824
764
  for (const [key, val] of Object.entries(omit)) {
825
765
  if (val === false || val == null)
826
766
  continue;
827
- if (mm.relations[key]) {
767
+ if ((0, utils_js_1.ownLookup)(mm.relations, key)) {
828
768
  throw new errors_js_1.ValidationError(`prisma-compat: \`omit\` on model "${modelName(ctx, mm)}" takes scalar fields, but "${key}" ` +
829
769
  'is a relation. Leave the relation out of `include` instead.');
830
770
  }
831
- out[renameField(mm, key)] = true;
771
+ (0, filters_js_1.setOwn)(out, renameField(mm, key), true);
832
772
  any = true;
833
773
  }
834
774
  return any ? out : undefined;
@@ -865,15 +805,15 @@ function translateProjection(ctx, mm, args, depth = 0) {
865
805
  if (val === false || val == null)
866
806
  continue;
867
807
  if (key === '_count') {
868
- withClause._count = translateCountSelect(mm, val);
808
+ withClause._count = translateCountSelect(ctx, mm, val);
869
809
  hasWith = true;
870
810
  continue;
871
811
  }
872
- const rel = mm.relations[key];
812
+ const rel = (0, utils_js_1.ownLookup)(mm.relations, key);
873
813
  if (!rel) {
874
814
  throw new errors_js_1.ValidationError(`prisma-compat: unknown relation "${key}" in include on model "${modelName(ctx, mm)}".`);
875
815
  }
876
- withClause[rel.name] = translateWithOption(ctx, mm, rel.name, val, depth + 1);
816
+ (0, filters_js_1.setOwn)(withClause, rel.name, translateWithOption(ctx, mm, rel.name, val, depth + 1));
877
817
  hasWith = true;
878
818
  }
879
819
  return { omit, with: hasWith ? withClause : undefined };
@@ -885,17 +825,17 @@ function translateProjection(ctx, mm, args, depth = 0) {
885
825
  if (val === false || val == null)
886
826
  continue;
887
827
  if (key === '_count') {
888
- withClause._count = translateCountSelect(mm, val);
828
+ withClause._count = translateCountSelect(ctx, mm, val);
889
829
  hasWith = true;
890
830
  continue;
891
831
  }
892
- const rel = mm.relations[key];
832
+ const rel = (0, utils_js_1.ownLookup)(mm.relations, key);
893
833
  if (rel) {
894
- withClause[rel.name] = translateWithOption(ctx, mm, rel.name, val, depth + 1);
834
+ (0, filters_js_1.setOwn)(withClause, rel.name, translateWithOption(ctx, mm, rel.name, val, depth + 1));
895
835
  hasWith = true;
896
836
  continue;
897
837
  }
898
- scalar[renameField(mm, key)] = true;
838
+ (0, filters_js_1.setOwn)(scalar, renameField(mm, key), true);
899
839
  hasScalar = true;
900
840
  }
901
841
  return { select: hasScalar ? scalar : undefined, with: hasWith ? withClause : undefined };
@@ -977,7 +917,7 @@ function resolveWriteProjection(ctx, mm, op, args) {
977
917
  for (const [key, val] of Object.entries(select)) {
978
918
  if (val === false || val == null)
979
919
  continue;
980
- if (key === '_count' || mm.relations[key]) {
920
+ if (key === '_count' || (0, utils_js_1.ownLookup)(mm.relations, key)) {
981
921
  throw new errors_js_1.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 ` +
982
922
  'it back with a separate findUnique inside the same $transaction.');
983
923
  }
@@ -994,7 +934,7 @@ function resolveWriteProjection(ctx, mm, op, args) {
994
934
  for (const [key, val] of Object.entries(omitArg)) {
995
935
  if (val === false || val == null)
996
936
  continue;
997
- if (mm.relations[key]) {
937
+ if ((0, utils_js_1.ownLookup)(mm.relations, key)) {
998
938
  throw new errors_js_1.ValidationError(`prisma-compat: \`omit\` on model "${modelName(ctx, mm)}" takes scalar fields, but "${key}" ` +
999
939
  'is a relation.');
1000
940
  }
@@ -1023,13 +963,13 @@ function coreWriteProjection(mm, proj) {
1023
963
  }
1024
964
  /** Apply a {@link WriteProjection} to one already-reshaped write result row. */
1025
965
  function applyWriteProjection(proj, row) {
1026
- if (!isPlainObject(row))
966
+ if (!(0, filters_js_1.isPlainRecord)(row))
1027
967
  return row;
1028
968
  if (proj.pick) {
1029
969
  const out = {};
1030
970
  for (const key of proj.pick) {
1031
- if (key in row)
1032
- out[key] = row[key];
971
+ if (Object.hasOwn(row, key))
972
+ (0, filters_js_1.setOwn)(out, key, row[key]);
1033
973
  }
1034
974
  return out;
1035
975
  }
@@ -1041,9 +981,65 @@ function applyWriteProjection(proj, row) {
1041
981
  }
1042
982
  return row;
1043
983
  }
984
+ /** What a caller wrote, named for a message: `an array`, `null`, `a string`. */
985
+ function describeValue(value) {
986
+ if (value === undefined)
987
+ return 'undefined';
988
+ if (value === null)
989
+ return 'null';
990
+ if (Array.isArray(value))
991
+ return 'an array';
992
+ return typeof value === 'object' ? 'an object' : `a ${typeof value}`;
993
+ }
994
+ /** The Prisma spelling of a turbine relation name, for a message. */
995
+ function prismaRelationName(ctx, mm, turbineRel) {
996
+ return (0, utils_js_1.ownLookup)(lookupsFor(ctx, mm).reverseRelations, turbineRel)?.prismaName ?? turbineRel;
997
+ }
998
+ /**
999
+ * The options Prisma takes on an included relation. `cursor` and `distinct` are
1000
+ * here because they are Prisma's, and are refused by name (E017) rather than
1001
+ * called unknown: Turbine's `with` clause cannot honor either.
1002
+ */
1003
+ const NESTED_RELATION_KEYS = new Set([
1004
+ 'where',
1005
+ 'orderBy',
1006
+ 'take',
1007
+ 'skip',
1008
+ 'cursor',
1009
+ 'distinct',
1010
+ 'select',
1011
+ 'include',
1012
+ 'omit',
1013
+ ]);
1014
+ /**
1015
+ * Refuse a key on an included relation's options that this layer does not
1016
+ * translate. Every key it does not read used to be dropped, so a misspelled
1017
+ * `wher` or a Turbine spelling (`limit`) returned unfiltered, unlimited rows
1018
+ * with no signal. An `undefined` value is absent, as everywhere else.
1019
+ */
1020
+ function assertNestedRelationKeys(label, opts) {
1021
+ for (const key of Object.keys(opts)) {
1022
+ if (opts[key] === undefined || NESTED_RELATION_KEYS.has(key))
1023
+ continue;
1024
+ const alias = ALIAS_HINT[key];
1025
+ if (alias) {
1026
+ throw new errors_js_1.ValidationError(`prisma-compat: "${key}" in the options of relation "${label}" is Turbine's spelling and would be ignored. ` +
1027
+ `prisma-compat takes Prisma's "${alias}".`);
1028
+ }
1029
+ const suggestion = (0, utils_js_1.suggestKey)(key, NESTED_RELATION_KEYS);
1030
+ throw new errors_js_1.ValidationError(`prisma-compat: unknown option "${key}" in the options of relation "${label}", it would be ignored.` +
1031
+ (suggestion ? ` Did you mean "${suggestion}"?` : '') +
1032
+ ` Valid options: ${[...NESTED_RELATION_KEYS].join(', ')}.`);
1033
+ }
1034
+ }
1044
1035
  /**
1045
1036
  * Translate a Prisma relation include payload into a Turbine `WithOptions`.
1046
1037
  *
1038
+ * Every option the payload carries is either translated, refused by name, or
1039
+ * refused as unknown: none is dropped. `cursor` and `distinct` are Prisma's
1040
+ * and have no per-relation form in a `with` clause (E017); any other key is
1041
+ * E003 (see {@link assertNestedRelationKeys}).
1042
+ *
1047
1043
  * `depth` is the relation hop count carried down from {@link translateProjection}
1048
1044
  * and is charged to the nested `where` / `orderBy` too, so a payload that pads
1049
1045
  * one arg with relation hops and then nests the other cannot spend two separate
@@ -1052,8 +1048,11 @@ function applyWriteProjection(proj, row) {
1052
1048
  function translateWithOption(ctx, mm, turbineRel, val, depth = 0) {
1053
1049
  if (val === true)
1054
1050
  return true;
1055
- if (!isPlainObject(val))
1056
- return true;
1051
+ const label = `${modelName(ctx, mm)}.${prismaRelationName(ctx, mm, turbineRel)}`;
1052
+ if (!(0, filters_js_1.isPlainRecord)(val)) {
1053
+ throw new errors_js_1.ValidationError(`prisma-compat: the value for relation "${label}" must be true or an options object, received ${describeValue(val)}.`);
1054
+ }
1055
+ assertNestedRelationKeys(label, val);
1057
1056
  const target = relTargetModel(ctx, mm, turbineRel);
1058
1057
  const opt = {};
1059
1058
  if (val.where !== undefined)
@@ -1062,10 +1061,26 @@ function translateWithOption(ctx, mm, turbineRel, val, depth = 0) {
1062
1061
  opt.orderBy = target ? translateOrderBy(ctx, target, val.orderBy, depth) : val.orderBy;
1063
1062
  if (val.take !== undefined)
1064
1063
  opt.limit = mapTake(val.take);
1065
- if (val.skip !== undefined) {
1066
- throw new errors_js_1.UnsupportedFeatureError('skip (offset) on a nested relation include', 'prisma-compat', "Turbine's `with` clause has no offset, page the relation with a separate query.");
1067
- }
1068
- if (target && (val.select !== undefined || val.include !== undefined || val.omit !== undefined)) {
1064
+ // Per parent, as in Prisma; core refuses it on a to-one relation.
1065
+ if (val.skip !== undefined)
1066
+ opt.offset = val.skip;
1067
+ if (val.cursor !== undefined) {
1068
+ throw new errors_js_1.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 " +
1069
+ 'findMany on the related model.');
1070
+ }
1071
+ // `distinct: []` names no field and removes nothing, so it is not a request.
1072
+ if (val.distinct !== undefined && !(Array.isArray(val.distinct) && val.distinct.length === 0)) {
1073
+ throw new errors_js_1.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 " +
1074
+ 'where / orderBy / distinct, or de-duplicate the included rows in application code.');
1075
+ }
1076
+ if (val.select !== undefined || val.include !== undefined || val.omit !== undefined) {
1077
+ // Without the target model there are no field names to translate with, and
1078
+ // dropping the projection would return every column of the child rows.
1079
+ if (!target) {
1080
+ throw new errors_js_1.ValidationError(`prisma-compat: relation "${label}" points at a table that is not in the compat map, so its ` +
1081
+ '`select` / `include` / `omit` cannot be translated. Regenerate the map with ' +
1082
+ '`turbine migrate-from-prisma`, or drop the projection.');
1083
+ }
1069
1084
  const proj = translateProjection(ctx, target, val, depth);
1070
1085
  if (proj.select)
1071
1086
  opt.select = proj.select;
@@ -1078,30 +1093,104 @@ function translateWithOption(ctx, mm, turbineRel, val, depth = 0) {
1078
1093
  }
1079
1094
  return opt;
1080
1095
  }
1081
- /** Translate a Prisma `_count: { select: { rel: true } }` / `true` into Turbine `with._count`. */
1082
- function translateCountSelect(mm, val) {
1096
+ /** A count `where` that filters nothing: absent, or an object with no defined key. */
1097
+ function isUnfilteredCountWhere(where) {
1098
+ return where === undefined || ((0, filters_js_1.isPlainRecord)(where) && Object.values(where).every((v) => v === undefined));
1099
+ }
1100
+ /**
1101
+ * Translate a Prisma `_count: { select: { rel: true } }` / `true` into Turbine
1102
+ * `with._count`.
1103
+ *
1104
+ * Core counts a relation whole: `with._count` takes `true` or `{ rel: true }`.
1105
+ * So a Prisma filtered count (`{ rel: { where } }`) is refused (E017) naming the
1106
+ * alternative, a relation that is not on the model is E003, and so is any key
1107
+ * but `select` under `_count` (a misspelled `selec` used to count EVERY relation,
1108
+ * and a misspelled relation counted nothing, both without a word).
1109
+ */
1110
+ function translateCountSelect(ctx, mm, val) {
1083
1111
  if (val === true)
1084
1112
  return true;
1085
- if (isPlainObject(val) && isPlainObject(val.select)) {
1086
- const out = {};
1087
- for (const [key, v] of Object.entries(val.select)) {
1088
- if (!v)
1113
+ const model = modelName(ctx, mm);
1114
+ if (!(0, filters_js_1.isPlainRecord)(val)) {
1115
+ throw new errors_js_1.ValidationError(`prisma-compat: \`_count\` on model "${model}" takes true or { select: { <relation>: true } }, ` +
1116
+ `received ${describeValue(val)}.`);
1117
+ }
1118
+ for (const key of Object.keys(val)) {
1119
+ if (key === 'select' || val[key] === undefined)
1120
+ continue;
1121
+ const suggestion = (0, utils_js_1.suggestKey)(key, ['select']);
1122
+ throw new errors_js_1.ValidationError(`prisma-compat: unknown key "${key}" in \`_count\` on model "${model}", it would be ignored.` +
1123
+ (suggestion ? ` Did you mean "${suggestion}"?` : '') +
1124
+ ' The only key is "select".');
1125
+ }
1126
+ if (val.select === undefined)
1127
+ return true;
1128
+ if (!(0, filters_js_1.isPlainRecord)(val.select)) {
1129
+ throw new errors_js_1.ValidationError(`prisma-compat: \`_count.select\` on model "${model}" takes { <relation>: true }, ` +
1130
+ `received ${describeValue(val.select)}.`);
1131
+ }
1132
+ const out = {};
1133
+ for (const [key, v] of Object.entries(val.select)) {
1134
+ if (v === undefined || v === false || v === null)
1135
+ continue;
1136
+ const rel = (0, utils_js_1.ownLookup)(mm.relations, key);
1137
+ if (!rel) {
1138
+ const known = Object.keys(mm.relations);
1139
+ const suggestion = (0, utils_js_1.suggestKey)(key, known);
1140
+ throw new errors_js_1.ValidationError(`prisma-compat: unknown relation "${key}" in \`_count.select\` on model "${model}", it would not be counted.` +
1141
+ (suggestion ? ` Did you mean "${suggestion}"?` : '') +
1142
+ ` Relations: ${known.join(', ') || '(none)'}.`);
1143
+ }
1144
+ if (v === true) {
1145
+ (0, filters_js_1.setOwn)(out, rel.name, true);
1146
+ continue;
1147
+ }
1148
+ if (!(0, filters_js_1.isPlainRecord)(v)) {
1149
+ throw new errors_js_1.ValidationError(`prisma-compat: \`_count.select.${key}\` on model "${model}" takes true, false or { where }, ` +
1150
+ `received ${describeValue(v)}.`);
1151
+ }
1152
+ for (const k of Object.keys(v)) {
1153
+ if (k === 'where' || v[k] === undefined)
1089
1154
  continue;
1090
- const rel = mm.relations[key];
1091
- if (rel)
1092
- out[rel.name] = true;
1155
+ const hint = (0, utils_js_1.suggestKey)(k, ['where']);
1156
+ throw new errors_js_1.ValidationError(`prisma-compat: unknown key "${k}" in \`_count.select.${key}\` on model "${model}", it would be ignored.` +
1157
+ (hint ? ` Did you mean "${hint}"?` : '') +
1158
+ ' The only key is "where".');
1093
1159
  }
1094
- return out;
1160
+ if (!isUnfilteredCountWhere(v.where)) {
1161
+ const target = relTargetModel(ctx, mm, rel.name);
1162
+ const targetName = target ? modelName(ctx, target) : undefined;
1163
+ const delegate = targetName ? (prismaPropertyAlias(targetName) ?? targetName) : '<model>';
1164
+ throw new errors_js_1.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. ' +
1165
+ `Count the filtered rows through the related model instead: prisma.${delegate}.count({ where: ... }) ` +
1166
+ `with the foreign key and your filter, or prisma.${delegate}.groupBy({ by: [<foreign key>], _count: true }) ` +
1167
+ 'for many parents at once.');
1168
+ }
1169
+ (0, filters_js_1.setOwn)(out, rel.name, true);
1095
1170
  }
1096
- return true;
1171
+ return out;
1097
1172
  }
1098
1173
  function modelName(ctx, mm) {
1099
1174
  return ctx.tableToModel.get(mm.table) ?? mm.table;
1100
1175
  }
1101
1176
  /**
1102
- * Assemble Turbine findMany-family args from Prisma read args. Order matters:
1103
- * `where` / `orderBy` are translated first so the cursor step (which may inject
1104
- * a keyset predicate) operates in the translated turbine-field space.
1177
+ * A Prisma field list that is written either as one name or as an array of
1178
+ * names (`distinct`, groupBy `by`), renamed into turbine fields. Anything else
1179
+ * is refused: a bare string used to be dropped (`distinct: 'status'` returned
1180
+ * every row, and `by: 'status'` crashed with a TypeError).
1181
+ */
1182
+ function translateFieldList(ctx, mm, option, val) {
1183
+ const names = typeof val === 'string' ? [val] : val;
1184
+ if (!Array.isArray(names) || names.some((n) => typeof n !== 'string')) {
1185
+ throw new errors_js_1.ValidationError(`prisma-compat: \`${option}\` on model "${modelName(ctx, mm)}" takes a field name or an array of field names, ` +
1186
+ `received ${describeValue(val)}.`);
1187
+ }
1188
+ return names.map((f) => renameField(mm, f));
1189
+ }
1190
+ /**
1191
+ * Assemble Turbine findMany-family args from Prisma read args. `where` /
1192
+ * `orderBy` are translated first, so the cursor step works in turbine-field
1193
+ * space.
1105
1194
  */
1106
1195
  function translateReadArgs(ctx, mm, prismaArgs, kind) {
1107
1196
  const t = {};
@@ -1123,19 +1212,11 @@ function translateReadArgs(ctx, mm, prismaArgs, kind) {
1123
1212
  t.omit = proj.omit;
1124
1213
  if (proj.with)
1125
1214
  t.with = proj.with;
1126
- if (Array.isArray(prismaArgs.distinct)) {
1127
- t.distinct = prismaArgs.distinct.map((f) => renameField(mm, f));
1128
- }
1215
+ if (prismaArgs.distinct !== undefined)
1216
+ t.distinct = translateFieldList(ctx, mm, 'distinct', prismaArgs.distinct);
1129
1217
  if (prismaArgs.relationLoadStrategy !== undefined) {
1130
1218
  t.relationLoadStrategy = mapRelationLoadStrategy(prismaArgs.relationLoadStrategy);
1131
1219
  }
1132
- // ORDER IS LOAD-BEARING: translateCursor MUST run BEFORE applyImplicitPkOrder.
1133
- // The cursor translation reads `t.orderBy` to decide the seek direction and to
1134
- // validate that a bare inclusive cursor names the sort key; it must see the
1135
- // USER's ordering, never an ordering this layer invented. Swap these two lines
1136
- // and a bare inclusive cursor on a non-PK field silently compiles against the
1137
- // injected primary-key order instead of throwing. See the guard test
1138
- // "cursor decisions are made against the user's own orderBy".
1139
1220
  translateCursor(ctx, mm, prismaArgs, t);
1140
1221
  applyImplicitPkOrder(ctx, mm, t, kind);
1141
1222
  return t;
@@ -1182,15 +1263,9 @@ function translateReadArgs(ctx, mm, prismaArgs, kind) {
1182
1263
  * with `column "id" does not exist`. Injecting also lifts the `LIMIT` outside
1183
1264
  * the `DISTINCT ON`, so the inner scan stops being limited. `distinct` reads
1184
1265
  * keep the exact SQL they emitted before this ordering existed.
1185
- * - `cursor`: NOT excluded here, unlike core. Core is a general-purpose layer
1186
- * whose cursor callers chose their own ordering; this layer's contract is
1187
- * Prisma parity, and Prisma pairs cursor pagination with the same implicit
1188
- * primary-key order. The injection is safe because it runs strictly AFTER
1189
- * {@link translateCursor}: the cursor comparison direction was already
1190
- * resolved from the user's own `orderBy` (ascending by default, matching an
1191
- * ascending primary key), and the bare-inclusive-cursor branch has already
1192
- * set or demanded an `orderBy`, so only the exclusive `cursor` + `skip`
1193
- * branch reaches here. It gains an `ORDER BY <pk> ASC` and nothing else.
1266
+ * - `cursor`: not excluded, and harmless either way. Core orders every cursor
1267
+ * page itself (the caller's order plus a primary-key tiebreaker), and with no
1268
+ * `orderBy` that is exactly the `ORDER BY <pk> ASC` written here.
1194
1269
  */
1195
1270
  function applyImplicitPkOrder(ctx, mm, t, kind) {
1196
1271
  if (kind === 'unique')
@@ -1201,134 +1276,59 @@ function applyImplicitPkOrder(ctx, mm, t, kind) {
1201
1276
  return;
1202
1277
  if (kind === 'many' && t.limit === undefined && t.offset === undefined)
1203
1278
  return;
1204
- const meta = ctx.schema.tables[mm.table];
1279
+ const meta = (0, utils_js_1.ownLookup)(ctx.schema.tables, mm.table);
1205
1280
  const pk = meta?.primaryKey ?? [];
1206
1281
  if (pk.length === 0)
1207
1282
  return;
1208
1283
  const fields = pk.map((col) => meta?.reverseColumnMap?.[col] ?? col);
1209
1284
  t.orderBy = fields.length === 1 ? { [fields[0]]: 'asc' } : fields.map((f) => ({ [f]: 'asc' }));
1210
1285
  }
1211
- /** turbine field names of the model's single-column primary key, if any. */
1212
- function singleColumnPkField(ctx, mm) {
1213
- const pk = ctx.schema.tables[mm.table]?.primaryKey;
1214
- if (pk && pk.length === 1) {
1215
- const col = pk[0];
1216
- return ctx.schema.tables[mm.table]?.reverseColumnMap?.[col] ?? col;
1217
- }
1218
- return undefined;
1219
- }
1220
1286
  /**
1221
1287
  * Translate Prisma cursor + take/skip onto Turbine args (operating on already
1222
1288
  * translated `t.where` / `t.orderBy` in turbine-field space).
1223
1289
  *
1224
1290
  * - **No cursor:** `skip`→`offset`, `take`→`limit`.
1225
- * - **Cursor + `skip: n` (n≥1):** the idiomatic exclusive-pagination case →
1226
- * Turbine's exclusive `cursor` + `offset: n-1` (skip:1 → offset 0, an exact
1227
- * match).
1228
- * - **Bare inclusive cursor** (skip absent/0): a Prisma cursor is INCLUSIVE, so
1229
- * it needs the anchor's sort-key value. Compiled to a `gte`/`lte` keyset
1230
- * predicate merged into `where` ONLY when the cursor is single-field AND that
1231
- * field is the single `orderBy` field (or, with no `orderBy`, is the
1232
- * single-column PK). Any other shape THROWS rather than emit a wrong page -
1233
- * pair the cursor with `skip: 1` for the exact exclusive translation.
1291
+ * - **Cursor:** core's cursor with the {@link CURSOR_INCLUSIVE} flag, plus
1292
+ * `offset: skip`. That is Prisma's definition exactly: the page starts AT the
1293
+ * cursor row, in the query's order (core completes it with a primary-key
1294
+ * tiebreaker), and `skip` counts from there, so the `skip: 1` idiom drops the
1295
+ * cursor row. Core places the page from the row the cursor names by a unique
1296
+ * key, which is what a Prisma cursor always is, so a cursor whose row has
1297
+ * been deleted gives an empty page, as it does in Prisma.
1234
1298
  */
1235
- function translateCursor(ctx, mm, prismaArgs, t) {
1299
+ function translateCursor(_ctx, mm, prismaArgs, t) {
1236
1300
  const cursor = prismaArgs.cursor;
1237
1301
  const skip = prismaArgs.skip;
1238
1302
  const take = prismaArgs.take;
1239
1303
  if (take !== undefined)
1240
1304
  t.limit = mapTake(take);
1241
- if (cursor === undefined) {
1242
- if (skip !== undefined)
1243
- t.offset = skip;
1305
+ if (skip !== undefined)
1306
+ t.offset = skip;
1307
+ if (cursor === undefined)
1308
+ return;
1309
+ // A cursor that is not an object (`null`) reaches core as written, which
1310
+ // refuses it: read as "no cursor" it would return the first page.
1311
+ if (!(0, filters_js_1.isPlainRecord)(cursor)) {
1312
+ t.cursor = cursor;
1244
1313
  return;
1245
1314
  }
1246
1315
  // Translate cursor field names (and expand a compound-unique selector cursor).
1316
+ // An `undefined` value is carried through rather than dropped, so a cursor
1317
+ // whose every value was `undefined` reaches core's args seam as one and is
1318
+ // refused there (E003), instead of becoming `{}` and paging from the start.
1247
1319
  const tcursor = {};
1248
1320
  for (const [key, val] of Object.entries(cursor)) {
1249
- if (val === undefined)
1250
- continue;
1251
- const compound = mm.compoundUniques[key];
1252
- if (compound && isPlainObject(val)) {
1321
+ const compound = (0, utils_js_1.ownLookup)(mm.compoundUniques, key);
1322
+ if (compound && (0, filters_js_1.isPlainRecord)(val)) {
1253
1323
  for (const [pk, pv] of Object.entries(val))
1254
- tcursor[renameField(mm, pk)] = pv;
1324
+ (0, filters_js_1.setOwn)(tcursor, renameField(mm, pk), pv);
1255
1325
  }
1256
1326
  else {
1257
- tcursor[renameField(mm, key)] = val;
1258
- }
1259
- }
1260
- const cursorFields = Object.keys(tcursor);
1261
- if (skip !== undefined && skip >= 1) {
1262
- t.cursor = tcursor;
1263
- t.offset = skip - 1;
1264
- return;
1265
- }
1266
- // Bare inclusive cursor (skip absent or 0).
1267
- if (cursorFields.length !== 1) {
1268
- throw new errors_js_1.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.');
1269
- }
1270
- const field = cursorFields[0];
1271
- const value = tcursor[field];
1272
- const obEntries = orderByPairs(t.orderBy);
1273
- let desc = false;
1274
- if (obEntries.length === 0) {
1275
- const pkField = singleColumnPkField(ctx, mm);
1276
- if (!pkField || field !== pkField) {
1277
- throw new errors_js_1.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\`.`);
1278
- }
1279
- t.orderBy = { [field]: 'asc' };
1280
- }
1281
- else {
1282
- if (obEntries.length !== 1 || obEntries[0][0] !== field) {
1283
- throw new errors_js_1.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\`.`);
1327
+ (0, filters_js_1.setOwn)(tcursor, renameField(mm, key), val);
1284
1328
  }
1285
- desc = obEntries[0][1];
1286
1329
  }
1287
- const op = desc ? 'lte' : 'gte';
1288
- t.where = mergeKeyset(t.where ?? {}, field, op, value);
1289
- }
1290
- /**
1291
- * Flatten a Turbine orderBy (object, or an array of them) into [field, isDesc]
1292
- * pairs.
1293
- *
1294
- * EVERY array element is read. Reading only an array of length one returned
1295
- * `[]` for a two-element array, and `[]` is also what "no orderBy at all"
1296
- * returns, so {@link translateCursor} took its no-orderBy branch: it found a
1297
- * bare cursor on the primary key, wrote `orderBy = { id: 'asc' }` over the
1298
- * caller's array, and returned the rows following the anchor in PK order
1299
- * instead of in the caller's order. The single-sort-key check downstream is
1300
- * what refuses a shape a bare (inclusive) cursor cannot express, and it can
1301
- * only fire if the pairs it counts are all there.
1302
- */
1303
- function orderByPairs(ob) {
1304
- const entries = Array.isArray(ob) ? ob : [ob];
1305
- const out = [];
1306
- for (const entry of entries) {
1307
- if (!isPlainObject(entry))
1308
- continue;
1309
- for (const [k, v] of Object.entries(entry)) {
1310
- const dir = isPlainObject(v) ? v.sort : v;
1311
- out.push([k, dir === 'desc']);
1312
- }
1313
- }
1314
- return out;
1315
- }
1316
- /** Merge a `{ field: { gte|lte: value } }` keyset predicate into a where object. */
1317
- function mergeKeyset(where, field, op, value) {
1318
- const merged = { ...where };
1319
- const existing = merged[field];
1320
- if (isPlainObject(existing)) {
1321
- merged[field] = { ...existing, [op]: value };
1322
- }
1323
- else if (existing !== undefined) {
1324
- const prevAnd = merged.AND;
1325
- const andList = Array.isArray(prevAnd) ? prevAnd : prevAnd !== undefined ? [prevAnd] : [];
1326
- merged.AND = [...andList, { [field]: { [op]: value } }];
1327
- }
1328
- else {
1329
- merged[field] = { [op]: value };
1330
- }
1331
- return merged;
1330
+ t.cursor = tcursor;
1331
+ t[normalize_args_js_1.CURSOR_INCLUSIVE] = true;
1332
1332
  }
1333
1333
  // --- write-data translation (nested writes) -------------------------------
1334
1334
  const NESTED_WRITE_OPS = new Set([
@@ -1352,20 +1352,36 @@ const NESTED_WRITE_OPS = new Set([
1352
1352
  */
1353
1353
  function translateWriteData(ctx, mm, data, depth = 0) {
1354
1354
  assertTranslateDepth(depth, 'data');
1355
- if (!isPlainObject(data))
1355
+ if (!(0, filters_js_1.isPlainRecord)(data))
1356
1356
  return data;
1357
1357
  const out = {};
1358
1358
  for (const [key, val] of Object.entries(data)) {
1359
- const rel = mm.relations[key];
1360
- if (rel && isPlainObject(val) && Object.keys(val).some((k) => NESTED_WRITE_OPS.has(k))) {
1359
+ if (isExpressionNode(val)) {
1360
+ // The field names inside an expression are Turbine's, and this layer
1361
+ // renames only the keys it can see, so one would be compiled against
1362
+ // the wrong names wherever a Prisma field is mapped.
1363
+ throw new errors_js_1.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 ' +
1364
+ 'are its own, or compute the value in JavaScript.');
1365
+ }
1366
+ const rel = (0, utils_js_1.ownLookup)(mm.relations, key);
1367
+ if (rel && (0, filters_js_1.isPlainRecord)(val) && Object.keys(val).some((k) => NESTED_WRITE_OPS.has(k))) {
1361
1368
  const target = relTargetModel(ctx, mm, rel.name);
1362
- out[rel.name] = translateNestedWrite(ctx, target, val, depth + 1);
1369
+ (0, filters_js_1.setOwn)(out, rel.name, translateNestedWrite(ctx, target, val, depth + 1));
1363
1370
  continue;
1364
1371
  }
1365
- out[renameField(mm, key)] = val;
1372
+ (0, filters_js_1.setOwn)(out, renameField(mm, key), val);
1366
1373
  }
1367
1374
  return out;
1368
1375
  }
1376
+ /**
1377
+ * An update expression (`expr.*`) or column reference (`col()`), read by the
1378
+ * brands' registry keys so this shim imports neither builder module.
1379
+ */
1380
+ function isExpressionNode(value) {
1381
+ return (typeof value === 'object' &&
1382
+ value !== null &&
1383
+ (Symbol.for('turbine-orm.sqlExpr') in value || Symbol.for('turbine-orm.columnRef') in value));
1384
+ }
1369
1385
  /**
1370
1386
  * One relation's nested-write ops. `depth` is threaded into EVERY payload
1371
1387
  * translator, the `where` ones included, so the budget is spent on total
@@ -1393,10 +1409,8 @@ function translateNestedWrite(ctx, target, ops, depth) {
1393
1409
  break;
1394
1410
  case 'deleteMany':
1395
1411
  case 'updateMany':
1396
- out[op] = mapMaybeArray(payload, (p) => translateWhereDataPair(ctx, target, p, depth + 1));
1397
- break;
1398
1412
  case 'update':
1399
- out[op] = mapMaybeArray(payload, (p) => translateWhereDataPair(ctx, target, p, depth + 1));
1413
+ out[op] = mapMaybeArray(payload, (p) => translateWhereDataPair(ctx, target, op, p, depth + 1));
1400
1414
  break;
1401
1415
  case 'connectOrCreate':
1402
1416
  out[op] = mapMaybeArray(payload, (p) => translateConnectOrCreate(ctx, target, p, depth + 1));
@@ -1405,7 +1419,7 @@ function translateNestedWrite(ctx, target, ops, depth) {
1405
1419
  out[op] = mapMaybeArray(payload, (p) => translateUpsertNested(ctx, target, p, depth + 1));
1406
1420
  break;
1407
1421
  default:
1408
- out[op] = payload;
1422
+ (0, filters_js_1.setOwn)(out, op, payload);
1409
1423
  }
1410
1424
  }
1411
1425
  return out;
@@ -1413,11 +1427,27 @@ function translateNestedWrite(ctx, target, ops, depth) {
1413
1427
  function mapMaybeArray(val, fn) {
1414
1428
  return Array.isArray(val) ? val.map(fn) : fn(val);
1415
1429
  }
1430
+ /**
1431
+ * Refuse a key on a nested write's `{ where, data }` / `{ where, create, update }`
1432
+ * payload that this layer does not read: it was dropped, so the part of the
1433
+ * write the caller meant never ran. An `undefined` value is absent.
1434
+ */
1435
+ function assertNestedPayloadKeys(op, p, allowed) {
1436
+ for (const key of Object.keys(p)) {
1437
+ if (p[key] === undefined || allowed.includes(key))
1438
+ continue;
1439
+ const suggestion = (0, utils_js_1.suggestKey)(key, allowed);
1440
+ throw new errors_js_1.ValidationError(`prisma-compat: unknown key "${key}" in a nested ${op}, it would be ignored.` +
1441
+ (suggestion ? ` Did you mean "${suggestion}"?` : '') +
1442
+ ` Valid keys: ${allowed.join(', ')}.`);
1443
+ }
1444
+ }
1416
1445
  /** A `{ where?, data }` pair (nested update/updateMany), or a bare data object. */
1417
- function translateWhereDataPair(ctx, target, p, depth) {
1418
- if (!isPlainObject(p))
1446
+ function translateWhereDataPair(ctx, target, op, p, depth) {
1447
+ if (!(0, filters_js_1.isPlainRecord)(p))
1419
1448
  return p;
1420
1449
  if ('data' in p || 'where' in p) {
1450
+ assertNestedPayloadKeys(op, p, ['where', 'data']);
1421
1451
  const out = {};
1422
1452
  if (p.where !== undefined)
1423
1453
  out.where = target ? translateWhere(ctx, target, p.where, depth) : p.where;
@@ -1428,8 +1458,9 @@ function translateWhereDataPair(ctx, target, p, depth) {
1428
1458
  return target ? translateWriteData(ctx, target, p, depth) : p;
1429
1459
  }
1430
1460
  function translateConnectOrCreate(ctx, target, p, depth) {
1431
- if (!isPlainObject(p))
1461
+ if (!(0, filters_js_1.isPlainRecord)(p))
1432
1462
  return p;
1463
+ assertNestedPayloadKeys('connectOrCreate', p, ['where', 'create']);
1433
1464
  const out = {};
1434
1465
  if (p.where !== undefined)
1435
1466
  out.where = target ? translateWhere(ctx, target, p.where, depth) : p.where;
@@ -1438,8 +1469,9 @@ function translateConnectOrCreate(ctx, target, p, depth) {
1438
1469
  return out;
1439
1470
  }
1440
1471
  function translateUpsertNested(ctx, target, p, depth) {
1441
- if (!isPlainObject(p))
1472
+ if (!(0, filters_js_1.isPlainRecord)(p))
1442
1473
  return p;
1474
+ assertNestedPayloadKeys('upsert', p, ['where', 'create', 'update']);
1443
1475
  const out = {};
1444
1476
  if (p.where !== undefined)
1445
1477
  out.where = target ? translateWhere(ctx, target, p.where, depth) : p.where;
@@ -1451,8 +1483,24 @@ function translateUpsertNested(ctx, target, p, depth) {
1451
1483
  }
1452
1484
  // --- aggregate / groupBy translation --------------------------------------
1453
1485
  const AGG_FIELD_BLOCKS = ['_sum', '_avg', '_min', '_max'];
1486
+ /**
1487
+ * Prisma's `count` and `aggregate` with `take` / `skip` / `cursor` compute over
1488
+ * that PAGE of rows. Turbine has no paged count or aggregate, and answering
1489
+ * over every matching row instead is a different number, so it is refused.
1490
+ * `orderBy` on its own changes no count or aggregate and is accepted.
1491
+ */
1492
+ function refusePagedAggregate(op, args) {
1493
+ const paged = ['take', 'skip', 'cursor'].filter((k) => args[k] !== undefined);
1494
+ if (paged.length === 0)
1495
+ return;
1496
+ throw new errors_js_1.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 ` +
1497
+ `findMany (same where / orderBy / ${paged.join(' / ')}) and compute it from the rows, or drop ` +
1498
+ `${paged.join(' / ')} to ${op} every matching row.`);
1499
+ }
1454
1500
  function translateAggregateArgs(ctx, mm, args, isGroupBy) {
1455
1501
  const t = {};
1502
+ if (!isGroupBy)
1503
+ refusePagedAggregate('aggregate', args);
1456
1504
  (0, index_js_1.applyNativeOptions)(isGroupBy ? index_js_1.GROUP_BY_OPTIONS : index_js_1.AGGREGATE_OPTIONS, args, t);
1457
1505
  if (args.where !== undefined)
1458
1506
  t.where = translateWhere(ctx, mm, args.where);
@@ -1463,17 +1511,19 @@ function translateAggregateArgs(ctx, mm, args, isGroupBy) {
1463
1511
  t[block] = renameAggBlock(mm, args[block], false);
1464
1512
  }
1465
1513
  if (isGroupBy) {
1466
- if (Array.isArray(args.by))
1467
- t.by = args.by.map((f) => renameField(mm, f));
1514
+ if (args.by === undefined) {
1515
+ throw new errors_js_1.ValidationError(`prisma-compat: groupBy on model "${modelName(ctx, mm)}" requires \`by\`, a field name or an array of field names.`);
1516
+ }
1517
+ t.by = translateFieldList(ctx, mm, 'by', args.by);
1468
1518
  if (args.orderBy !== undefined)
1469
1519
  t.orderBy = translateOrderBy(ctx, mm, args.orderBy);
1470
1520
  if (args.having !== undefined)
1471
1521
  t.having = renameHaving(mm, args.having);
1472
1522
  if (args.distinctOn !== undefined)
1473
1523
  t.distinctOn = translateDistinctOn(ctx, mm, args.distinctOn);
1474
- if (typeof args.take === 'number')
1524
+ if (args.take !== undefined)
1475
1525
  t.limit = mapTake(args.take);
1476
- if (typeof args.skip === 'number')
1526
+ if (args.skip !== undefined)
1477
1527
  t.offset = args.skip;
1478
1528
  }
1479
1529
  return t;
@@ -1486,7 +1536,7 @@ function translateAggregateArgs(ctx, mm, args, isGroupBy) {
1486
1536
  * silently wrong on one with a `@map`.
1487
1537
  */
1488
1538
  function translateDistinctOn(ctx, mm, val) {
1489
- if (!isPlainObject(val))
1539
+ if (!(0, filters_js_1.isPlainRecord)(val))
1490
1540
  return val;
1491
1541
  const out = { ...val };
1492
1542
  if (Array.isArray(val.columns))
@@ -1497,7 +1547,7 @@ function translateDistinctOn(ctx, mm, val) {
1497
1547
  }
1498
1548
  /** Rename field keys inside an aggregate block; `_count`'s `_all` passes through. */
1499
1549
  function renameAggBlock(mm, block, isCount) {
1500
- if (block === true || !isPlainObject(block))
1550
+ if (block === true || !(0, filters_js_1.isPlainRecord)(block))
1501
1551
  return block;
1502
1552
  const out = {};
1503
1553
  for (const [key, val] of Object.entries(block)) {
@@ -1505,7 +1555,7 @@ function renameAggBlock(mm, block, isCount) {
1505
1555
  out._all = val;
1506
1556
  continue;
1507
1557
  }
1508
- out[renameField(mm, key)] = val;
1558
+ (0, filters_js_1.setOwn)(out, renameField(mm, key), val);
1509
1559
  }
1510
1560
  return out;
1511
1561
  }
@@ -1520,7 +1570,7 @@ function renameAggBlock(mm, block, isCount) {
1520
1570
  */
1521
1571
  function renameHaving(mm, having, depth = 0) {
1522
1572
  assertTranslateDepth(depth, 'having');
1523
- if (!isPlainObject(having))
1573
+ if (!(0, filters_js_1.isPlainRecord)(having))
1524
1574
  return having;
1525
1575
  const out = {};
1526
1576
  for (const [key, val] of Object.entries(having)) {
@@ -1532,7 +1582,7 @@ function renameHaving(mm, having, depth = 0) {
1532
1582
  out._count = val;
1533
1583
  continue;
1534
1584
  }
1535
- out[renameField(mm, key)] = val;
1585
+ (0, filters_js_1.setOwn)(out, renameField(mm, key), val);
1536
1586
  }
1537
1587
  return out;
1538
1588
  }
@@ -1546,7 +1596,7 @@ function reshapeRowOrNull(ctx, mm, row) {
1546
1596
  return row == null ? null : reshapeRow(ctx, mm, row);
1547
1597
  }
1548
1598
  function reshapeRow(ctx, mm, row) {
1549
- if (!isPlainObject(row))
1599
+ if (!(0, filters_js_1.isPlainRecord)(row))
1550
1600
  return row;
1551
1601
  const l = lookupsFor(ctx, mm);
1552
1602
  const out = {};
@@ -1555,7 +1605,7 @@ function reshapeRow(ctx, mm, row) {
1555
1605
  out._count = reshapeCount(l, val);
1556
1606
  continue;
1557
1607
  }
1558
- const rel = l.reverseRelations[key];
1608
+ const rel = (0, utils_js_1.ownLookup)(l.reverseRelations, key);
1559
1609
  if (rel) {
1560
1610
  const target = relTargetModel(ctx, mm, key);
1561
1611
  let rv;
@@ -1565,25 +1615,25 @@ function reshapeRow(ctx, mm, row) {
1565
1615
  // map's cardinality is authoritative → first element or null.
1566
1616
  rv = rel.cardinality === 'one' ? (mapped.length ? mapped[0] : null) : mapped;
1567
1617
  }
1568
- else if (isPlainObject(val)) {
1618
+ else if ((0, filters_js_1.isPlainRecord)(val)) {
1569
1619
  rv = target ? reshapeRow(ctx, target, val) : val;
1570
1620
  }
1571
1621
  else {
1572
1622
  rv = val; // null to-one
1573
1623
  }
1574
- out[rel.prismaName] = rv;
1624
+ (0, filters_js_1.setOwn)(out, rel.prismaName, rv);
1575
1625
  continue;
1576
1626
  }
1577
1627
  let sv = val;
1578
1628
  if (typeof sv === 'string' && l.timeFields.has(key)) {
1579
1629
  sv = timeStringToDate(sv) ?? sv;
1580
1630
  }
1581
- out[l.identityFields ? key : (l.reverseFields[key] ?? key)] = sv;
1631
+ (0, filters_js_1.setOwn)(out, l.identityFields ? key : ((0, utils_js_1.ownLookup)(l.reverseFields, key) ?? key), sv);
1582
1632
  }
1583
1633
  return out;
1584
1634
  }
1585
1635
  function reshapeCount(l, count) {
1586
- if (!isPlainObject(count))
1636
+ if (!(0, filters_js_1.isPlainRecord)(count))
1587
1637
  return count;
1588
1638
  const out = {};
1589
1639
  for (const [key, val] of Object.entries(count)) {
@@ -1591,13 +1641,13 @@ function reshapeCount(l, count) {
1591
1641
  out._all = val;
1592
1642
  continue;
1593
1643
  }
1594
- out[l.reverseRelations[key]?.prismaName ?? key] = val;
1644
+ (0, filters_js_1.setOwn)(out, (0, utils_js_1.ownLookup)(l.reverseRelations, key)?.prismaName ?? key, val);
1595
1645
  }
1596
1646
  return out;
1597
1647
  }
1598
1648
  /** Reshape an aggregate result: field keys inside blocks → prisma names. */
1599
1649
  function reshapeAggregate(ctx, mm, res) {
1600
- if (!isPlainObject(res))
1650
+ if (!(0, filters_js_1.isPlainRecord)(res))
1601
1651
  return res;
1602
1652
  const l = lookupsFor(ctx, mm);
1603
1653
  const out = {};
@@ -1614,12 +1664,12 @@ function reshapeAggregate(ctx, mm, res) {
1614
1664
  if (typeof sv === 'string' && l.timeFields.has(key)) {
1615
1665
  sv = timeStringToDate(sv) ?? sv;
1616
1666
  }
1617
- out[l.identityFields ? key : (l.reverseFields[key] ?? key)] = sv;
1667
+ (0, filters_js_1.setOwn)(out, l.identityFields ? key : ((0, utils_js_1.ownLookup)(l.reverseFields, key) ?? key), sv);
1618
1668
  }
1619
1669
  return out;
1620
1670
  }
1621
1671
  function reshapeAggFieldBlock(l, block, isCount) {
1622
- if (!isPlainObject(block))
1672
+ if (!(0, filters_js_1.isPlainRecord)(block))
1623
1673
  return block;
1624
1674
  const out = {};
1625
1675
  for (const [key, val] of Object.entries(block)) {
@@ -1627,13 +1677,13 @@ function reshapeAggFieldBlock(l, block, isCount) {
1627
1677
  out._all = val;
1628
1678
  continue;
1629
1679
  }
1630
- out[l.reverseFields[key] ?? key] = val;
1680
+ (0, filters_js_1.setOwn)(out, (0, utils_js_1.ownLookup)(l.reverseFields, key) ?? key, val);
1631
1681
  }
1632
1682
  return out;
1633
1683
  }
1634
1684
  /** Reshape a groupBy row: by-field keys + aggregate blocks → prisma names. */
1635
1685
  function reshapeGroupRow(ctx, mm, row) {
1636
- if (!isPlainObject(row))
1686
+ if (!(0, filters_js_1.isPlainRecord)(row))
1637
1687
  return row;
1638
1688
  const l = lookupsFor(ctx, mm);
1639
1689
  const out = {};
@@ -1650,7 +1700,7 @@ function reshapeGroupRow(ctx, mm, row) {
1650
1700
  if (typeof sv === 'string' && l.timeFields.has(key)) {
1651
1701
  sv = timeStringToDate(sv) ?? sv;
1652
1702
  }
1653
- out[l.identityFields ? key : (l.reverseFields[key] ?? key)] = sv;
1703
+ (0, filters_js_1.setOwn)(out, l.identityFields ? key : ((0, utils_js_1.ownLookup)(l.reverseFields, key) ?? key), sv);
1654
1704
  }
1655
1705
  return out;
1656
1706
  }
@@ -1691,6 +1741,55 @@ class CompatPromise {
1691
1741
  function batchableOf(v) {
1692
1742
  return v instanceof CompatPromise ? v[exports.COMPAT_DEFERRED] : undefined;
1693
1743
  }
1744
+ /** Warn-once namespace for a `$transaction` option this layer accepts and does not apply. */
1745
+ const TX_OPTION_WARN_NS = 'prismaCompatTxOption';
1746
+ /**
1747
+ * Every `$transaction` option this layer understands: Prisma's three, plus the
1748
+ * Turbine-native ones it forwards to core as written.
1749
+ */
1750
+ const TX_OPTION_KEYS = new Set([
1751
+ 'isolationLevel',
1752
+ 'timeout',
1753
+ 'maxWait',
1754
+ 'sessionContext',
1755
+ 'allowBuiltinSettings',
1756
+ 'readOnly',
1757
+ ]);
1758
+ /**
1759
+ * Check a `$transaction` options object and drop the one key core has no use
1760
+ * for. A key that is not an option used to be forwarded and ignored, so a
1761
+ * misspelled `isolationLevl` ran the transaction at the default isolation.
1762
+ * `maxWait` bounds how long Prisma waits for a pooled connection; here the
1763
+ * pool's own `connectionTimeoutMs` does that, so it is accepted, warned about
1764
+ * once, and not forwarded.
1765
+ */
1766
+ function resolveTxOptions(log, options) {
1767
+ if (options === undefined || options === null)
1768
+ return undefined;
1769
+ if (!(0, filters_js_1.isPlainRecord)(options)) {
1770
+ throw new errors_js_1.ValidationError(`prisma-compat: $transaction options must be an object, received ${describeValue(options)}.`);
1771
+ }
1772
+ const out = {};
1773
+ for (const [key, value] of Object.entries(options)) {
1774
+ if (value === undefined)
1775
+ continue;
1776
+ if (!TX_OPTION_KEYS.has(key)) {
1777
+ const suggestion = (0, utils_js_1.suggestKey)(key, TX_OPTION_KEYS);
1778
+ throw new errors_js_1.ValidationError(`prisma-compat: unknown $transaction option "${key}", it would be ignored.` +
1779
+ (suggestion ? ` Did you mean "${suggestion}"?` : '') +
1780
+ ` Valid options: ${[...TX_OPTION_KEYS].join(', ')}.`);
1781
+ }
1782
+ if (key === 'maxWait') {
1783
+ if (log.diagnostics.advice && (0, warn_registry_js_1.shouldWarnOnce)(TX_OPTION_WARN_NS, key)) {
1784
+ (0, logger_js_1.logTo)(log.logger, 'warn', 'prisma-compat: the $transaction option "maxWait" has no effect here. Waiting for a pooled connection ' +
1785
+ "is bounded by the client's `connectionTimeoutMs` instead.", { code: 'prisma-compat-unknown-option', operation: '$transaction', key });
1786
+ }
1787
+ continue;
1788
+ }
1789
+ out[key] = value;
1790
+ }
1791
+ return out;
1792
+ }
1694
1793
  /**
1695
1794
  * Prisma's client-property spelling of a model name: first letter lowercased
1696
1795
  * (`model User` -> `prisma.user`). Returns null when the spelling is identical
@@ -1736,7 +1835,7 @@ function clientDefaultValue(kind) {
1736
1835
  /** Fill missing create-side client defaults (uuid / cuid / now / updatedAt), by Prisma field name. */
1737
1836
  function applyCreateDefaults(mm, data) {
1738
1837
  const cd = mm.clientDefaults;
1739
- if (!cd || !isPlainObject(data))
1838
+ if (!cd || !(0, filters_js_1.isPlainRecord)(data))
1740
1839
  return data;
1741
1840
  let out = data;
1742
1841
  for (const [field, kind] of Object.entries(cd)) {
@@ -1751,7 +1850,7 @@ function applyCreateDefaults(mm, data) {
1751
1850
  /** Touch @updatedAt fields on the update side (Prisma sets them on every update). */
1752
1851
  function applyUpdateTouch(mm, data) {
1753
1852
  const cd = mm.clientDefaults;
1754
- if (!cd || !isPlainObject(data))
1853
+ if (!cd || !(0, filters_js_1.isPlainRecord)(data))
1755
1854
  return data;
1756
1855
  let out = data;
1757
1856
  for (const [field, kind] of Object.entries(cd)) {
@@ -1765,8 +1864,8 @@ function applyUpdateTouch(mm, data) {
1765
1864
  }
1766
1865
  /** Whether TRANSLATED write data carries relation keys (nested-write shapes). */
1767
1866
  function hasNestedKeys(ctx, mm, data) {
1768
- const rels = ctx.schema.tables[mm.table]?.relations;
1769
- if (!rels || !isPlainObject(data))
1867
+ const rels = (0, utils_js_1.ownLookup)(ctx.schema.tables, mm.table)?.relations;
1868
+ if (!rels || !(0, filters_js_1.isPlainRecord)(data))
1770
1869
  return false;
1771
1870
  return Object.keys(data).some((k) => {
1772
1871
  if (!Object.hasOwn(rels, k))
@@ -1827,7 +1926,7 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1827
1926
  // itself, for the same reason (see the comment above): a compat promise that
1828
1927
  // is built and never awaited must produce no output at all.
1829
1928
  const translate = () => {
1830
- warnUnknownQueryOptions(modelName(ctx, mm), op, rawArgs);
1929
+ warnUnknownQueryOptions(ctx.log, modelName(ctx, mm), op, rawArgs);
1831
1930
  return translateRaw();
1832
1931
  };
1833
1932
  const batchable = batch
@@ -1894,10 +1993,15 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1894
1993
  });
1895
1994
  },
1896
1995
  createMany: (args) => defer('createMany', args, () => {
1996
+ // Prisma takes one row object or an array of them. Anything else used
1997
+ // to become zero rows, so the call "succeeded" with `count: 0`.
1897
1998
  const data = args.data;
1898
- const rows = Array.isArray(data)
1899
- ? data.map((d) => translateWriteData(ctx, mm, applyCreateDefaults(mm, d)))
1900
- : [];
1999
+ const list = Array.isArray(data) ? data : (0, filters_js_1.isPlainRecord)(data) ? [data] : undefined;
2000
+ if (!list) {
2001
+ throw new errors_js_1.ValidationError(`prisma-compat: createMany on model "${modelName(ctx, mm)}" requires \`data\`, a row object or an ` +
2002
+ `array of them, received ${describeValue(data)}.`);
2003
+ }
2004
+ const rows = list.map((d) => translateWriteData(ctx, mm, applyCreateDefaults(mm, d)));
1901
2005
  const t = { data: rows };
1902
2006
  (0, index_js_1.applyNativeOptions)(index_js_1.CREATE_MANY_OPTIONS, args, t);
1903
2007
  if (args.skipDuplicates)
@@ -1935,7 +2039,7 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1935
2039
  // `optimisticLock.field` is a FIELD NAME, so it is renamed rather than
1936
2040
  // copied: a blind passthrough would send the Prisma spelling into core
1937
2041
  // and break on any model whose column is `@map`ped.
1938
- if (isPlainObject(a.optimisticLock)) {
2042
+ if ((0, filters_js_1.isPlainRecord)(a.optimisticLock)) {
1939
2043
  t.optimisticLock = {
1940
2044
  ...a.optimisticLock,
1941
2045
  field: renameField(mm, String(a.optimisticLock.field)),
@@ -2011,13 +2115,31 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
2011
2115
  execInTx: async (table, t) => shape(await table(mm.table).upsert(t)),
2012
2116
  });
2013
2117
  },
2014
- count: (args = {}) => defer('count', args, () => {
2015
- const t = {};
2016
- (0, index_js_1.applyNativeOptions)(index_js_1.COUNT_OPTIONS, args, t);
2017
- if (args.where !== undefined)
2018
- t.where = translateWhere(ctx, mm, args.where);
2019
- return t;
2020
- }, (qi, t) => qi.count(t), { build: (qi, t) => qi.buildCount(t), reshape: (raw) => raw }),
2118
+ count: (args = {}) => {
2119
+ const a = args;
2120
+ // `count({ select })` answers per field (`{ _all, title }`, a non-null
2121
+ // count per field), which is core's aggregate `_count` record form.
2122
+ if ((0, filters_js_1.isPlainRecord)(a.select)) {
2123
+ const countOf = (raw) => reshapeAggregate(ctx, mm, raw)._count;
2124
+ return defer('count', args, () => {
2125
+ refusePagedAggregate('count', a);
2126
+ const t = {};
2127
+ (0, index_js_1.applyNativeOptions)(index_js_1.AGGREGATE_OPTIONS, a, t);
2128
+ if (a.where !== undefined)
2129
+ t.where = translateWhere(ctx, mm, a.where);
2130
+ t._count = renameAggBlock(mm, a.select, true);
2131
+ return t;
2132
+ }, (qi, t) => qi.aggregate(t).then(countOf), { build: (qi, t) => qi.buildAggregate(t), reshape: countOf });
2133
+ }
2134
+ return defer('count', args, () => {
2135
+ refusePagedAggregate('count', a);
2136
+ const t = {};
2137
+ (0, index_js_1.applyNativeOptions)(index_js_1.COUNT_OPTIONS, a, t);
2138
+ if (a.where !== undefined)
2139
+ t.where = translateWhere(ctx, mm, a.where);
2140
+ return t;
2141
+ }, (qi, t) => qi.count(t), { build: (qi, t) => qi.buildCount(t), reshape: (raw) => raw });
2142
+ },
2021
2143
  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) }),
2022
2144
  groupBy: (args) => defer('groupBy', args, () => translateAggregateArgs(ctx, mm, args, true), (qi, t) => qi.groupBy(t).then((rows) => rows.map((r) => reshapeGroupRow(ctx, mm, r))), {
2023
2145
  build: (qi, t) => qi.buildGroupBy(t),
@@ -2132,8 +2254,9 @@ const JUNCTION_WARN_NS = 'prismaCompatJunction';
2132
2254
  * `$transaction` must not turn the transaction surface into a delegate).
2133
2255
  *
2134
2256
  * Every skip except the mapped-table one loses a capability the caller might
2135
- * expect, so those warn once per junction name in dev; a table that a model
2136
- * already maps to is not a loss (the model IS that table's accessor).
2257
+ * expect, so those warn once per junction name (a construction warning, so
2258
+ * always on); a table that a model already maps to is not a loss (the model IS
2259
+ * that table's accessor).
2137
2260
  */
2138
2261
  function junctionModels(ctx, map, tableToModel) {
2139
2262
  const out = [];
@@ -2154,16 +2277,16 @@ function junctionModels(ctx, map, tableToModel) {
2154
2277
  if (rel.type !== 'manyToMany')
2155
2278
  continue;
2156
2279
  const name = rel.through?.table;
2157
- if (!name || !ctx.schema.tables[name] || tableToModel.has(name))
2280
+ if (!name || !(0, utils_js_1.ownLookup)(ctx.schema.tables, name) || tableToModel.has(name))
2158
2281
  continue;
2159
2282
  if (registered.has(name))
2160
2283
  continue;
2161
2284
  if (seen.has(name)) {
2162
- if (process.env.NODE_ENV !== 'production' && (0, warn_registry_js_1.shouldWarnOnce)(JUNCTION_WARN_NS, name)) {
2163
- console.warn(`[turbine] prisma-compat: the many-to-many junction table "${name}" collides with an ` +
2285
+ if ((0, warn_registry_js_1.shouldWarnOnce)(JUNCTION_WARN_NS, name)) {
2286
+ (0, logger_js_1.logTo)(ctx.log.logger, 'warn', `prisma-compat: the many-to-many junction table "${name}" collides with an ` +
2164
2287
  'existing client member of the same name, so no junction accessor was created for it. ' +
2165
2288
  'Reach its rows through the owning model relation (a nested connect / disconnect / set on the ' +
2166
- 'related model), or through $queryRaw / $executeRaw.');
2289
+ 'related model), or through $queryRaw / $executeRaw.', { code: 'prisma-compat-junction-collision', table: name });
2167
2290
  }
2168
2291
  continue;
2169
2292
  }
@@ -2298,15 +2421,19 @@ function createPrismaCompatClient(client, map, options = {}) {
2298
2421
  // the real client satisfies it structurally (the cast just relaxes the strict
2299
2422
  // callback-param variance on `$transaction`).
2300
2423
  const db = client;
2301
- // Fire-and-forget, dev-only, once per process: is this map still describing
2302
- // the schema on disk? Nothing else in the system ever asks.
2303
- warnStalePrismaMap(map);
2424
+ // Log through the wrapped client's logger and diagnostics. A stand-in client
2425
+ // without them (a test stub) gets the console and the environment's mode.
2426
+ const log = client[logger_js_1.CLIENT_LOG_CONTEXT] ?? { logger: logger_js_1.consoleLogger, diagnostics: (0, diagnostics_js_1.resolveDiagnostics)() };
2427
+ // Fire-and-forget, once per process: is this map still describing the schema
2428
+ // on disk? Nothing else in the system ever asks.
2429
+ warnStalePrismaMap(map, log);
2304
2430
  const tableToModel = new Map();
2305
2431
  for (const [prismaModel, mm] of Object.entries(map.models))
2306
2432
  tableToModel.set(mm.table, prismaModel);
2307
2433
  const ctx = {
2308
2434
  map,
2309
2435
  schema: db.schema,
2436
+ log,
2310
2437
  tableToModel,
2311
2438
  lookups: new Map(),
2312
2439
  options: {
@@ -2398,7 +2525,15 @@ function createPrismaCompatClient(client, map, options = {}) {
2398
2525
  }
2399
2526
  const base = {
2400
2527
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
2401
- $transaction: ((arg, txOptions) => {
2528
+ $transaction: ((arg, rawTxOptions) => {
2529
+ // Rejected, never thrown: Prisma's $transaction is always thenable.
2530
+ let txOptions;
2531
+ try {
2532
+ txOptions = resolveTxOptions(ctx.log, rawTxOptions);
2533
+ }
2534
+ catch (err) {
2535
+ return Promise.reject(decorate(err, ctx.options.prismaErrorCodes));
2536
+ }
2402
2537
  // Array (lazy batch) form. Wrapped so validation/build errors REJECT the
2403
2538
  // returned promise (Prisma's $transaction is always thenable) rather than
2404
2539
  // throwing synchronously.
@@ -2425,7 +2560,7 @@ function createPrismaCompatClient(client, map, options = {}) {
2425
2560
  }, txOptions);
2426
2561
  }
2427
2562
  const deferreds = batchables.map((b) => b.build());
2428
- const results = (await db.$transaction(deferreds));
2563
+ const results = (await db.$transaction(deferreds, txOptions));
2429
2564
  return results.map((raw, i) => batchables[i].reshape(raw));
2430
2565
  }
2431
2566
  catch (err) {
@@ -2438,10 +2573,10 @@ function createPrismaCompatClient(client, map, options = {}) {
2438
2573
  return db.$transaction((tx) => {
2439
2574
  const txDelegates = {};
2440
2575
  for (const [prismaModel, mm] of delegateModels) {
2441
- txDelegates[prismaModel] = extendDelegate(makeDelegate(ctx, mm, () => tx.table(mm.table), (fn) => fn((n) => tx.table(n))), prismaModel, exts);
2576
+ (0, filters_js_1.setOwn)(txDelegates, prismaModel, extendDelegate(makeDelegate(ctx, mm, () => tx.table(mm.table), (fn) => fn((n) => tx.table(n))), prismaModel, exts));
2442
2577
  const alias = prismaPropertyAlias(prismaModel);
2443
- if (alias && !(alias in map.models) && !(alias in txDelegates)) {
2444
- txDelegates[alias] = txDelegates[prismaModel];
2578
+ if (alias && !Object.hasOwn(map.models, alias) && !Object.hasOwn(txDelegates, alias)) {
2579
+ (0, filters_js_1.setOwn)(txDelegates, alias, (0, utils_js_1.ownLookup)(txDelegates, prismaModel));
2445
2580
  }
2446
2581
  }
2447
2582
  // Raw SQL on the transaction's own connection. Prisma's tx client
@@ -2468,13 +2603,13 @@ function createPrismaCompatClient(client, map, options = {}) {
2468
2603
  // key from the map, so no dynamic-access proxy is needed.
2469
2604
  const result = { ...base };
2470
2605
  for (const [prismaModel, delegate] of delegates)
2471
- result[prismaModel] = delegate;
2606
+ (0, filters_js_1.setOwn)(result, prismaModel, delegate);
2472
2607
  // Prisma-spelling aliases (`prisma.user` for `model User`). Skipped when the
2473
2608
  // lowercased name is itself a model or already taken (never shadow a real key).
2474
2609
  for (const [prismaModel, delegate] of delegates) {
2475
2610
  const alias = prismaPropertyAlias(prismaModel);
2476
- if (alias && !(alias in result))
2477
- result[alias] = delegate;
2611
+ if (alias && !Object.hasOwn(result, alias))
2612
+ (0, filters_js_1.setOwn)(result, alias, delegate);
2478
2613
  }
2479
2614
  // Extension `client` members last: every name was checked against the real
2480
2615
  // client keys in applyExtension, so this can never overwrite a delegate.