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
@@ -6,6 +6,7 @@
6
6
  * and execution rather than filter-shape bookkeeping.
7
7
  */
8
8
  import { ValidationError } from '../errors.js';
9
+ import { isColumnRef, isPlainColumnRefShape, plainColumnRefMessage } from './column-ref.js';
9
10
  import { assertDirectionToken, assertOrderDirection } from './types.js';
10
11
  import { OPERATOR_KEYS, resolveColumnName } from './utils.js';
11
12
  // ---------------------------------------------------------------------------
@@ -24,19 +25,40 @@ export function isWhereOperator(value) {
24
25
  return keys.length > 0 && keys.every((k) => OPERATOR_KEYS.has(k));
25
26
  }
26
27
  /**
27
- * True for a *plain object literal* that reached an equality fallthrough
28
- * without matching any known filter shape, the misspelled-operator case.
29
- * Class instances (Buffer for bytea, Decimal wrappers, ...) are legitimate
30
- * bind values and return false, as do arrays and Dates.
28
+ * THE plain-object test for argument trees: an object literal (or a
29
+ * prototype-less object), never an array, Date, Buffer, typed array or other
30
+ * class instance. Class instances are values a driver binds (a Decimal, an
31
+ * interval wrapper), so no walker over caller arguments may descend into one.
31
32
  */
32
- export function isUnmatchedPlainObject(value) {
33
- if (typeof value !== 'object' || value === null || Array.isArray(value) || value instanceof Date)
34
- return false;
35
- if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value))
33
+ export function isPlainRecord(value) {
34
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
36
35
  return false;
37
36
  const proto = Object.getPrototypeOf(value);
38
37
  return proto === Object.prototype || proto === null;
39
38
  }
39
+ /**
40
+ * `target[key] = value`, always as an OWN property. A plain assignment of the
41
+ * key `__proto__` replaces the object's prototype instead, so a caller key
42
+ * spelled that way (`JSON.parse` produces it as an ordinary own key) would
43
+ * vanish from a rebuilt argument object rather than reach the check that
44
+ * refuses it, and a filter would silently stop filtering.
45
+ */
46
+ export function setOwn(target, key, value) {
47
+ if (key === '__proto__') {
48
+ Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
49
+ }
50
+ else {
51
+ target[key] = value;
52
+ }
53
+ }
54
+ /**
55
+ * {@link isPlainRecord} read at an equality fallthrough: a plain object that
56
+ * matched no known filter shape is a misspelled operator, while a class
57
+ * instance (Buffer for bytea, a Decimal wrapper) is a legitimate bind value.
58
+ */
59
+ export function isUnmatchedPlainObject(value) {
60
+ return isPlainRecord(value);
61
+ }
40
62
  /**
41
63
  * Operator keys that accept a {@link ColumnRef} (`{ col: 'otherField' }`)
42
64
  * value for column-to-column comparison. `in`/`notIn` and the LIKE operators
@@ -47,54 +69,409 @@ export const COLUMN_REF_OPERATORS = new Set(['equals', 'not', 'gt', 'gte', 'lt',
47
69
  * THE relation-filter wrappers: the keys whose body is a clause against the
48
70
  * relation's TARGET table rather than against this one.
49
71
  *
50
- * One named list, here, because everything that walks a where clause has to
51
- * agree about them and the copies had already spread. It lived inlined in
52
- * `where-compile.ts` (`'some' in x || 'every' in x || …`, the SQL compiler's
53
- * own answer), again in `normalizeRelationFilter` (where.ts) as the negated
54
- * conjunction of the same five, again in `cli/pii-predicate-guard.ts`, and a
55
- * fourth time in `prisma-compat.ts` SPLIT across a
56
- * `RELATION_QUANTIFIERS` set (`some`/`every`/`none`) plus two inline
57
- * `k === 'is' || k === 'isNot'` tests, which is the copy most likely to drift
58
- * because half of it does not read as a list and a grep for the list does not
59
- * find it.
60
- *
61
- * The failure mode is not cosmetic: a wrapper the SQL compiler treats as a
62
- * relation filter but a WALKER does not is a wrapper whose body reaches the
63
- * builder unwalked. That is precisely the operand-position channel this
64
- * release closed elsewhere.
72
+ * Everything that walks a where clause has to agree about them: a wrapper the
73
+ * SQL compiler treats as a relation filter but a walker does not is a wrapper
74
+ * whose body reaches the builder unwalked. The SQL walk (`walkWhere` in
75
+ * where-compile.ts), the bare to-one normalization (`normalizeRelationFilter`
76
+ * in where.ts) and the PII predicate guard (`cli/pii-predicate-guard.ts`) all
77
+ * read this list. `prisma-compat.ts` still carries its own copy
78
+ * (`RELATION_QUANTIFIERS` plus inline `is` / `isNot` tests).
65
79
  *
66
80
  * `query/` is the right home rather than `cli/` because the direction of the
67
81
  * dependency is fixed: `cli/` and the prisma-compat shim may import from the
68
82
  * query path, and the query path may never import from `cli/`
69
83
  * (`scripts/check-import-cycles.mjs`).
70
- *
71
- * ## STATUS: a landing spot, not yet a deduplication. Read this before trusting it.
72
- *
73
- * NOTHING IMPORTS THIS YET. The three copies listed above are all still in
74
- * place and still hand-synced; declaring the canonical home did not by itself
75
- * move any of them onto it. It is kept, rather than deleted as unused, because
76
- * `cli/pii-predicate-guard.ts` names this module as where its own copy wants to
77
- * go, and deleting the destination is the one change that makes converging
78
- * harder. Wiring the copies up is a separate change: each one sits on a
79
- * different walk, so each has to be re-tested on its own.
80
- *
81
- * So do not read this constant as evidence that the walkers agree. The comment
82
- * on each copy is still the only thing holding them in step.
83
84
  */
84
85
  export const RELATION_FILTER_WRAPPERS = ['some', 'none', 'every', 'is', 'isNot'];
85
86
  /** {@link RELATION_FILTER_WRAPPERS} as a membership set, for the walkers. */
86
87
  export const RELATION_FILTER_WRAPPER_SET = new Set(RELATION_FILTER_WRAPPERS);
88
+ // The column-reference check lives with its constructor; re-exported so the
89
+ // where modules keep one import site for every operand-shape guard.
90
+ export { isColumnRef } from './column-ref.js';
91
+ // ---------------------------------------------------------------------------
92
+ // `undefined` means absent
93
+ // ---------------------------------------------------------------------------
94
+ /**
95
+ * Recursion bound for nested `not` operator objects. Past it the operand walk
96
+ * refuses the filter by name instead of recursing further.
97
+ */
98
+ const OPERATOR_MAX_DEPTH = 64;
87
99
  /**
88
- * Check if an operator value is a column reference: a plain object whose ONLY
89
- * key is `col` with a string value. Anything else (extra keys, non-string
90
- * `col`) is treated as a plain value so JSON payloads that merely contain a
91
- * `col` property keep their equality meaning.
100
+ * `value` without its `undefined`-valued keys, one level deep. Returns the
101
+ * input BY REFERENCE when there are none, so the common path allocates
102
+ * nothing. Symbol-keyed properties (internal brands) survive a copy.
103
+ *
104
+ * This is the where walkers' backstop only. The args seam
105
+ * (`pruneUndefinedArgs` in normalize-args.ts) is the authority on `undefined`:
106
+ * it removes undefined keys at every where position and objects that became
107
+ * empty because of them. The walkers see what the seam missed (internal
108
+ * callers below it), skip a key whose value is `undefined`, and never look
109
+ * inside a value, so a JSON operand is data however it is shaped.
110
+ */
111
+ export function dropUndefinedKeys(value) {
112
+ const keys = Object.keys(value);
113
+ for (const key of keys) {
114
+ if (value[key] !== undefined)
115
+ continue;
116
+ const out = copySymbols(value);
117
+ for (const k of keys)
118
+ if (value[k] !== undefined)
119
+ setOwn(out, k, value[k]);
120
+ return out;
121
+ }
122
+ return value;
123
+ }
124
+ /** A fresh object carrying `source`'s own symbol-keyed properties (brands). */
125
+ function copySymbols(source) {
126
+ const out = Object.getPrototypeOf(source) === null ? Object.create(null) : {};
127
+ for (const sym of Object.getOwnPropertySymbols(source)) {
128
+ const descriptor = Object.getOwnPropertyDescriptor(source, sym);
129
+ if (descriptor)
130
+ Object.defineProperty(out, sym, descriptor);
131
+ }
132
+ return out;
133
+ }
134
+ /** {@link OperandColumnKind} for a column type token (case-insensitive). */
135
+ export function operandColumnKind(columnType) {
136
+ if (columnType === undefined)
137
+ return 'scalar';
138
+ const t = columnType.toLowerCase();
139
+ if (t === 'json' || t === 'jsonb')
140
+ return 'json';
141
+ if (t.startsWith('_'))
142
+ return 'array';
143
+ if (t === 'vector')
144
+ return 'vector';
145
+ return 'scalar';
146
+ }
147
+ /**
148
+ * Built-in containers whose instances serialize to `{}` (or nothing useful)
149
+ * when a driver JSON-encodes them, so binding one compares against `'{}'`.
92
150
  */
93
- export function isColumnRef(value) {
94
- if (typeof value !== 'object' || value === null || Array.isArray(value) || value instanceof Date)
151
+ const NON_SCALAR_BUILTINS = [
152
+ Map,
153
+ Set,
154
+ WeakMap,
155
+ WeakSet,
156
+ Promise,
157
+ RegExp,
158
+ Error,
159
+ ];
160
+ /**
161
+ * A class instance the driver can bind as one value: one that says how to
162
+ * render itself (`toPostgres`, `toJSON`, or its own `toString`, as a Decimal
163
+ * or an interval wrapper does), and is not a built-in container.
164
+ */
165
+ function isBindableInstance(value) {
166
+ if (NON_SCALAR_BUILTINS.some((ctor) => value instanceof ctor))
167
+ return false;
168
+ const v = value;
169
+ return (typeof v.toPostgres === 'function' ||
170
+ typeof v.toJSON === 'function' ||
171
+ (typeof v.toString === 'function' && v.toString !== Object.prototype.toString));
172
+ }
173
+ /**
174
+ * A value the driver binds as ONE scalar: a primitive, a Date, a Buffer or
175
+ * typed array, or a class instance that renders itself (see
176
+ * {@link isBindableInstance}). A plain object, an array, a Map or Set is not,
177
+ * and neither is a function, a symbol or `undefined`.
178
+ */
179
+ function isBindableScalar(value) {
180
+ switch (typeof value) {
181
+ case 'string':
182
+ case 'number':
183
+ case 'bigint':
184
+ case 'boolean':
185
+ return true;
186
+ case 'object':
187
+ break;
188
+ default:
189
+ return false;
190
+ }
191
+ if (value === null || Array.isArray(value))
192
+ return false;
193
+ // An Invalid Date has no SQL value: drivers send it as `NaN` text, a server
194
+ // error, a thrown RangeError or, inside a JSON-encoded list, `null`, which
195
+ // matched nothing and so negated to every row.
196
+ if (value instanceof Date)
197
+ return !Number.isNaN(value.getTime());
198
+ if (ArrayBuffer.isView(value))
199
+ return true;
200
+ if (isColumnRef(value) || isPlainRecord(value))
201
+ return false;
202
+ return isBindableInstance(value);
203
+ }
204
+ /** A non-null value operand that fits a column of `kind`. */
205
+ function fitsColumn(value, kind) {
206
+ if (isBindableScalar(value))
207
+ return true;
208
+ if (kind === 'json') {
209
+ if (value === undefined || typeof value === 'function' || typeof value === 'symbol')
210
+ return false;
211
+ if (value instanceof Date)
212
+ return false;
213
+ return !(typeof value === 'object' && value !== null && NON_SCALAR_BUILTINS.some((c) => value instanceof c));
214
+ }
215
+ if (kind === 'array' || kind === 'vector') {
216
+ return Array.isArray(value) && value.every((el) => el === null || isBindableScalar(el) || Array.isArray(el));
217
+ }
218
+ return false;
219
+ }
220
+ /** Short, value-free description of an operand for an error message. */
221
+ export function describeOperand(value) {
222
+ if (value === null)
223
+ return 'null';
224
+ if (value === undefined)
225
+ return 'undefined';
226
+ if (Array.isArray(value))
227
+ return 'an array';
228
+ if (isColumnRef(value))
229
+ return 'a column reference';
230
+ if (value instanceof Date && Number.isNaN(value.getTime()))
231
+ return 'an Invalid Date';
232
+ if (isPlainRecord(value)) {
233
+ const keys = Object.keys(value);
234
+ return keys.length === 0 ? 'an empty object' : `an object with keys ${keys.map((k) => `"${k}"`).join(', ')}`;
235
+ }
236
+ if (typeof value === 'object') {
237
+ const name = value.constructor?.name;
238
+ return typeof name === 'string' && name.length > 0 ? `a ${name} instance` : 'an object';
239
+ }
240
+ return `a ${typeof value}`;
241
+ }
242
+ const SCALAR_WORDS = 'a string, number, bigint, boolean, Date or Buffer';
243
+ function acceptedFor(kind) {
244
+ if (kind === 'array')
245
+ return `an array (or ${SCALAR_WORDS})`;
246
+ if (kind === 'vector')
247
+ return 'an array of numbers';
248
+ if (kind === 'json')
249
+ return 'a JSON value';
250
+ return SCALAR_WORDS;
251
+ }
252
+ /** `on "<column>" for table "<table>"`, for refusal messages. Built only on the throw path. */
253
+ function operandSite(ctx) {
254
+ return `on "${ctx.column}" for table "${ctx.table}"`;
255
+ }
256
+ function refuseOperand(key, value, ctx, accepted) {
257
+ throw new ValidationError(`Invalid operand for '${key}' ${operandSite(ctx)}: received ${describeOperand(value)}. '${key}' takes ${accepted}.`);
258
+ }
259
+ /**
260
+ * Validate every operand of an operator object against its column, and throw
261
+ * {@link ValidationError} (E003) naming the column, the operator, what was
262
+ * received and what is accepted.
263
+ *
264
+ * An operator object must state at least one comparison. One made only of
265
+ * `mode` (or of operators whose value is `undefined`) compares nothing, so it
266
+ * is refused rather than compiled to no predicate: inside a relation filter
267
+ * that silence turned `deleteMany` into a delete of every row. The args seam
268
+ * removes the object when `undefined` emptied it, so Prisma's optional-search
269
+ * idiom (`{ contains: q, mode: 'insensitive' }` with `q` undefined) still
270
+ * reads as "no filter".
271
+ *
272
+ * Called by the SQL build AND the cache-hit param collect with the same
273
+ * arguments, so a warmed template can never bind an operand the cold build
274
+ * refuses. Recurses into a `not` operator object, bounded by `depth`. Runs on
275
+ * every operator object of every query, so it reads only the keys present and
276
+ * builds a message only when it throws.
277
+ */
278
+ export function assertOperatorOperands(op, ctx, depth = 0) {
279
+ if (depth > OPERATOR_MAX_DEPTH) {
280
+ throw new ValidationError(`\`not\` operators on "${ctx.column}" nest too deeply; flatten the filter.`);
281
+ }
282
+ const mode = op.mode;
283
+ if (mode !== undefined && mode !== 'default' && mode !== 'insensitive') {
284
+ throw new ValidationError(`Invalid mode ${typeof mode === 'string' ? JSON.stringify(mode) : describeOperand(mode)} ${operandSite(ctx)}. ` +
285
+ "Use 'insensitive' or 'default'.");
286
+ }
287
+ // The classifier sends only operator keys here; `mode` was checked above.
288
+ let comparisons = 0;
289
+ for (const key of Object.keys(op)) {
290
+ const value = op[key];
291
+ if (value === undefined)
292
+ continue;
293
+ switch (key) {
294
+ case 'equals':
295
+ case 'not':
296
+ comparisons++;
297
+ if (value !== null && typeof value === 'object')
298
+ assertObjectEqualityOperand(key, value, ctx, depth);
299
+ else if (value !== null && !fitsColumn(value, ctx.kind)) {
300
+ refuseOperand(key, value, ctx, `${acceptedFor(ctx.kind)}, null, or col('field')`);
301
+ }
302
+ break;
303
+ case 'gt':
304
+ case 'gte':
305
+ case 'lt':
306
+ case 'lte':
307
+ comparisons++;
308
+ if (value === null) {
309
+ refuseOperand(key, value, ctx, `${acceptedFor(ctx.kind)} or col('field'); a comparison with null matches no row, use equals: null or not: null`);
310
+ }
311
+ if (isColumnRef(value))
312
+ break;
313
+ if (ctx.kind !== 'json' && isPlainColumnRefShape(value)) {
314
+ throw new ValidationError(plainColumnRefMessage(value, key, ctx.column, ctx.table));
315
+ }
316
+ if (!fitsColumn(value, ctx.kind))
317
+ refuseOperand(key, value, ctx, `${acceptedFor(ctx.kind)} or col('field')`);
318
+ break;
319
+ case 'in':
320
+ case 'notIn':
321
+ comparisons++;
322
+ assertListOperand(key, value, ctx);
323
+ break;
324
+ case 'contains':
325
+ case 'startsWith':
326
+ case 'endsWith':
327
+ comparisons++;
328
+ if (typeof value !== 'string')
329
+ refuseOperand(key, value, ctx, 'a string');
330
+ break;
331
+ }
332
+ }
333
+ if (comparisons === 0) {
334
+ throw new ValidationError(`The ${depth > 0 ? '`not` operator object' : 'filter'} ${operandSite(ctx)} names no operator: received ` +
335
+ `${describeOperand(op)}. Add one (for example { contains: 'x', mode: 'insensitive' }), or leave ` +
336
+ `"${ctx.column}" out of the filter to apply no condition to it.`);
337
+ }
338
+ }
339
+ /** An object at `equals` / `not`: a column reference, or a `not` operator object to negate. */
340
+ function assertObjectEqualityOperand(key, value, ctx, depth) {
341
+ if (isColumnRef(value)) {
342
+ if (ctx.kind === 'json') {
343
+ refuseOperand(key, value, ctx, `${acceptedFor('json')}; column references are not supported on JSON columns`);
344
+ }
345
+ return;
346
+ }
347
+ if (ctx.kind === 'json') {
348
+ if (key === 'not')
349
+ assertJsonNotOperand(value, ctx);
350
+ if (!fitsColumn(value, 'json'))
351
+ refuseOperand(key, value, ctx, `${acceptedFor('json')} or null`);
352
+ return;
353
+ }
354
+ if (isPlainColumnRefShape(value)) {
355
+ throw new ValidationError(plainColumnRefMessage(value, key, ctx.column, ctx.table));
356
+ }
357
+ if (key === 'not' && isPlainRecord(value)) {
358
+ if (Object.keys(value).length === 0) {
359
+ refuseOperand(key, value, ctx, `a value, null, a column reference or an operator object such as { contains: 'x' }`);
360
+ }
361
+ if (!isWhereOperator(value)) {
362
+ refuseOperand(key, value, ctx, `${acceptedFor(ctx.kind)}, null, col('field'), or an operator object`);
363
+ }
364
+ assertOperatorOperands(value, ctx, depth + 1);
365
+ return;
366
+ }
367
+ if (!fitsColumn(value, ctx.kind)) {
368
+ refuseOperand(key, value, ctx, `${acceptedFor(ctx.kind)}, null, or col('field')` +
369
+ (isPlainRecord(value) ? ' (an object is a JSON value, accepted only on json/jsonb columns)' : ''));
370
+ }
371
+ }
372
+ /**
373
+ * On a JSON column `not` takes a JSON VALUE (`not: { a: 1 }` compares
374
+ * documents), so `not: { contains: {...} }` was compared as the literal document
375
+ * `{"contains": ...}` and matched nearly every row, while the same spelling on a
376
+ * text column negates the filter. An object made only of filter-operator keys
377
+ * is therefore refused, except `not: { equals: X }`, which negates the
378
+ * whole-document equality. A literal document with such keys stays expressible.
379
+ * Every engine runs it (PowDB too), so the spelling means one thing everywhere.
380
+ */
381
+ export function assertJsonNotOperand(value, ctx) {
382
+ if (!isPlainRecord(value))
383
+ return;
384
+ const keys = Object.keys(value).filter((k) => value[k] !== undefined);
385
+ if (keys.length === 0 || (keys.length === 1 && keys[0] === 'equals'))
386
+ return;
387
+ if (!keys.every((k) => JSON_FILTER_KEYS.has(k) || OPERATOR_KEYS.has(k)))
388
+ return;
389
+ const named = keys.map((k) => `"${k}"`).join(', ');
390
+ throw new ValidationError(`Invalid \`not\` ${operandSite(ctx)}, a JSON column: an object whose keys are all filter operators (${named}) ` +
391
+ 'would be compared as a literal JSON document and match nearly every row. To negate the filter, write ' +
392
+ `\`NOT: { ${ctx.column}: { ${keys[0]}: ... } }\`. To compare against a document that has these keys, ` +
393
+ 'pass it as `not: jsonValue({ ... })` or `not: { equals: { ... } }`.');
394
+ }
395
+ /** `in` / `notIn`: an array whose elements fit the column (or are null). */
396
+ function assertListOperand(key, value, ctx) {
397
+ if (!Array.isArray(value))
398
+ refuseOperand(key, value, ctx, 'an array of values');
399
+ const elementKind = ctx.kind === 'array' ? 'array' : ctx.kind;
400
+ for (const element of value) {
401
+ if (element === null || fitsColumn(element, elementKind))
402
+ continue;
403
+ throw new ValidationError(`Invalid element in '${key}' ${operandSite(ctx)}: received ${describeOperand(element)}. ` +
404
+ `Every element must be ${acceptedFor(elementKind)} (or null).`);
405
+ }
406
+ }
407
+ /**
408
+ * Validate a BARE value (implicit equality, `{ id: 5 }`) against its column.
409
+ * Shared by the build and collect paths of every where scope.
410
+ */
411
+ export function assertEqualityOperand(value, ctx) {
412
+ if (value === null)
413
+ return;
414
+ const where = `on "${ctx.column}" for table "${ctx.table}"`;
415
+ if (isColumnRef(value)) {
416
+ throw new ValidationError(`A column reference ${where} needs an operator: write { equals: col(${JSON.stringify(value.col)}) }` +
417
+ (ctx.kind === 'json' ? ', on a non-JSON column (references are not supported on JSON columns).' : '.'));
418
+ }
419
+ if (ctx.kind === 'json') {
420
+ if (typeof value === 'function' || typeof value === 'symbol') {
421
+ throw new ValidationError(`Invalid value ${where}: received ${describeOperand(value)}. Pass a JSON value.`);
422
+ }
423
+ return;
424
+ }
425
+ if (isPlainColumnRefShape(value)) {
426
+ throw new ValidationError(plainColumnRefMessage(value, 'equals', ctx.column, ctx.table));
427
+ }
428
+ if (isPlainRecord(value)) {
429
+ const keys = Object.keys(value);
430
+ throw new ValidationError(keys.length === 0
431
+ ? `Empty filter object ${where}. Provide a value or an operator like { gt: 1 }.`
432
+ : `Unknown operator${keys.length > 1 ? 's' : ''} ${keys.map((k) => `"${k}"`).join(', ')} ${where}. ` +
433
+ `Supported operators: ${[...OPERATOR_KEYS].join(', ')}.`);
434
+ }
435
+ if (fitsColumn(value, ctx.kind))
436
+ return;
437
+ throw new ValidationError(`Invalid value ${where}: received ${describeOperand(value)}. A bare value takes ${acceptedFor(ctx.kind)}` +
438
+ (Array.isArray(value) ? '; to match any of several values use { in: [...] }.' : '.'));
439
+ }
440
+ /** `{ equals: v }` written as an object literal with that one key. */
441
+ function isEqualsWrapper(value) {
442
+ if (!isPlainRecord(value))
95
443
  return false;
96
444
  const keys = Object.keys(value);
97
- return keys.length === 1 && keys[0] === 'col' && typeof value.col === 'string';
445
+ return keys.length === 1 && keys[0] === 'equals';
446
+ }
447
+ /**
448
+ * One `cursor` field's value, checked and unwrapped. A cursor value is the
449
+ * value the last row holds in that column, so it takes the bare-equality rule
450
+ * (`{ equals: v }` is the same as `v`); an operator object, a list on a scalar
451
+ * column or a column reference is refused (E003) in terms of cursors, not
452
+ * filters. NULL is a row's value only in a column that can hold it: in a NOT
453
+ * NULL column (the primary key included) no row has it, so it is refused
454
+ * rather than placing the page nowhere. Shared by every engine's cursor path.
455
+ */
456
+ export function cursorFieldValue(field, written, ctx) {
457
+ const value = isEqualsWrapper(written) ? written.equals : written;
458
+ const site = `\`cursor\` field "${field}" on table "${ctx.table}"`;
459
+ if (value === null) {
460
+ if (ctx.nullable)
461
+ return null;
462
+ throw new ValidationError(`Invalid ${site}: null. "${ctx.column}" is NOT NULL, so no row holds null there and the cursor names no ` +
463
+ "position. Pass the last row's value, or leave `cursor` out for the first page.");
464
+ }
465
+ const columnRef = isColumnRef(value) || isPlainColumnRefShape(value);
466
+ const operatorShaped = isPlainRecord(value) && ctx.kind !== 'json';
467
+ const list = Array.isArray(value) && ctx.kind === 'scalar';
468
+ if (columnRef || operatorShaped || list) {
469
+ throw new ValidationError(`Invalid ${site}: received ${describeOperand(value)}. A cursor value is the value the last row holds in ` +
470
+ `"${field}" (for example \`cursor: { ${field}: 42 }\`), written bare or as \`{ equals: 42 }\`. ` +
471
+ 'To filter rows, use `where`.');
472
+ }
473
+ assertEqualityOperand(value, ctx);
474
+ return value;
98
475
  }
99
476
  /**
100
477
  * Fingerprint the SHAPE of a where-operator object. Null-valued `equals` /
@@ -103,43 +480,35 @@ export function isColumnRef(value) {
103
480
  * warmed by `{ not: 5 }` would serve `{ not: null }` with a desynced param list.
104
481
  *
105
482
  * Column references ({@link ColumnRef}) compile the referenced column into the
106
- * SQL TEXT (no param bound), so the referenced field name is part of the shape
107
- *: `{ equals: { col: 'a' } }` and `{ equals: { col: 'b' } }` must never share
108
- * a cache entry. The name is JSON-encoded so exotic field names cannot collide
483
+ * SQL TEXT (no param bound), so the referenced field name is part of the
484
+ * shape: `{ equals: col('a') }` and `{ equals: col('b') }` must never share a
485
+ * cache entry. The name is JSON-encoded so exotic field names cannot collide
109
486
  * with other fingerprint tokens.
487
+ *
488
+ * A `not` operator object compiles to `NOT (<inner>)`, so its whole inner shape
489
+ * is part of the token, with the inner object's EFFECTIVE mode (its own, else
490
+ * the inherited one). Keys holding `undefined` are skipped, as the builders skip them.
110
491
  */
111
- export function fingerprintOperatorShape(value) {
492
+ export function fingerprintOperatorShape(value, inheritedMode, depth = 0) {
112
493
  const obj = value;
494
+ const effectiveMode = obj.mode !== undefined ? obj.mode : inheritedMode;
113
495
  const opKeys = Object.keys(obj)
114
- .filter((k) => k !== 'mode')
496
+ .filter((k) => k !== 'mode' && obj[k] !== undefined)
115
497
  .map((k) => {
116
498
  const v = obj[k];
117
499
  if ((k === 'equals' || k === 'not') && v === null)
118
500
  return `${k}:null`;
119
501
  if (COLUMN_REF_OPERATORS.has(k) && isColumnRef(v))
120
502
  return `${k}:col(${JSON.stringify(v.col)})`;
503
+ if (k === 'not' && depth < OPERATOR_MAX_DEPTH && isWhereOperator(v)) {
504
+ return `not:${fingerprintOperatorShape(v, effectiveMode, depth + 1)}`;
505
+ }
121
506
  return k;
122
507
  })
123
508
  .sort();
124
- const modeStr = value.mode === 'insensitive' ? ':i' : '';
509
+ const modeStr = effectiveMode === 'insensitive' ? ':i' : '';
125
510
  return `op(${opKeys.join(',')}${modeStr})`;
126
511
  }
127
- /**
128
- * Guard for the value of an `equals` operator reaching the plain-equality
129
- * operator path. A plain object literal can only legitimately be an equality
130
- * value on a json/jsonb column, and those route to the JSONB filter branch
131
- * BEFORE the operator branch, so any plain object that reaches here is a
132
- * mistake (e.g. `{ equals: { foo: 1 } }` on a text column). Shared by the
133
- * SQL-build path and the cache-hit param-collect path so a warmed cache can
134
- * never skip the check.
135
- */
136
- export function assertBindableEqualsOperand(value, column) {
137
- if (!isUnmatchedPlainObject(value))
138
- return;
139
- throw new ValidationError(`Plain-object value for operator 'equals' on ${column}: ` +
140
- `objects are only valid 'equals' values on JSON (json/jsonb) columns, ` +
141
- `where 'equals' is the JSONB containment filter.`);
142
- }
143
512
  /**
144
513
  * Object keys in sorted order, mirroring the canonical order used by every
145
514
  * cache fingerprint. The SQL-build and cache-hit param-collect paths MUST
@@ -296,7 +665,9 @@ export const ARRAY_UNIQUE_KEYS = new Set(['has', 'hasEvery', 'hasSome', 'isEmpty
296
665
  * concrete value is part of the shape; the other operators are value-invariant.
297
666
  */
298
667
  export function fingerprintArrayFilterShape(filter) {
299
- const keys = Object.keys(filter).sort();
668
+ const keys = Object.keys(filter)
669
+ .filter((k) => filter[k] !== undefined)
670
+ .sort();
300
671
  const suffix = filter.isEmpty === undefined ? '' : `:empty=${filter.isEmpty ? 'true' : 'false'}`;
301
672
  return `${keys.join(',')}${suffix}`;
302
673
  }
@@ -640,24 +1011,34 @@ function assertDroppedDirection(key, value, table) {
640
1011
  * has a meaning that degrades as the list grows (every added column makes the
641
1012
  * result strictly less distinct, converging on the plain query), so a long list
642
1013
  * is self-limiting in a way a long sort is not.
1014
+ *
1015
+ * Every element must be a field name: anything else (an `undefined` hole, a
1016
+ * number) is E003 here, the first place either caller reads the list.
643
1017
  */
644
- export function dedupeColumnList(meta, columns) {
1018
+ export function dedupeColumnList(meta, columns, label = 'distinct') {
1019
+ for (let i = 0; i < columns.length; i++) {
1020
+ const key = columns[i];
1021
+ if (typeof key === 'string' && key.length > 0)
1022
+ continue;
1023
+ throw new ValidationError(`\`${label}\`${meta?.name ? ` on table "${meta.name}"` : ''} takes field names, but element ${i} is ` +
1024
+ `${key === undefined ? 'undefined' : describeOperand(key)}. Pass each field as a string, or leave the ` +
1025
+ `list out to apply no ${label}.`);
1026
+ }
645
1027
  if (columns.length < 2)
646
1028
  return null;
647
1029
  const seen = new Map();
648
1030
  let dropped;
649
1031
  const kept = [];
650
1032
  for (const key of columns) {
651
- const column = typeof key === 'string' ? resolvedOrderColumn(meta, key) : null;
652
- const first = column === null ? undefined : seen.get(column);
653
- if (first !== undefined && column !== null) {
1033
+ const column = resolvedOrderColumn(meta, key);
1034
+ const first = seen.get(column);
1035
+ if (first !== undefined) {
654
1036
  if (dropped === undefined)
655
1037
  dropped = [];
656
1038
  dropped.push({ key, first, resolved: column });
657
1039
  continue;
658
1040
  }
659
- if (column !== null)
660
- seen.set(column, key);
1041
+ seen.set(column, key);
661
1042
  kept.push(key);
662
1043
  }
663
1044
  return dropped ? { columns: kept, dropped } : null;
@@ -688,3 +1069,147 @@ export function isEmptyOrderBy(orderBy) {
688
1069
  }
689
1070
  return orderBy === undefined || orderBy === null;
690
1071
  }
1072
+ // ---------------------------------------------------------------------------
1073
+ // JSON and array filter keys, and `undefined` inside a filter
1074
+ // ---------------------------------------------------------------------------
1075
+ /**
1076
+ * Refuse a {@link JsonFilter} carrying a key that is not a JSON operator, and
1077
+ * refuse a filter that selects a `path` but never compares it.
1078
+ *
1079
+ * Both shapes used to compile to NOTHING. `buildJsonFilterClauses` only ever
1080
+ * emitted a clause for a key it recognized, so `{ path: ['title'],
1081
+ * string_contains: 'x' }`, the Prisma spelling, and an easy typo besides -
1082
+ * produced an empty clause list, the predicate vanished, and the query
1083
+ * returned EVERY row. Inside an `AND` it silently dropped that conjunct, so a
1084
+ * tenant scope written this way widened to the whole table. The equivalent
1085
+ * typo on a scalar column has always thrown; this closes the inconsistency.
1086
+ *
1087
+ * Called from both the SQL-build path and the cache-hit param-collect path, so
1088
+ * a warmed SQL cache cannot skip the check.
1089
+ */
1090
+ export function assertJsonFilterKeys(filter, column) {
1091
+ const obj = filter;
1092
+ const present = Object.keys(obj).filter((k) => obj[k] !== undefined);
1093
+ for (const key of present) {
1094
+ if (isColumnRef(obj[key])) {
1095
+ throw new ValidationError(`A column reference cannot be the '${key}' operand of a JSON filter on ${column}: on a json/jsonb ` +
1096
+ 'column the operand is a JSON value. Compare the column with col() only on non-JSON columns.');
1097
+ }
1098
+ }
1099
+ for (const key of present) {
1100
+ if (JSON_FILTER_KEYS.has(key))
1101
+ continue;
1102
+ const suggestion = JSON_PRISMA_SPELLINGS[key];
1103
+ throw new ValidationError(`Unknown JSON filter operator "${key}" on ${column}.` +
1104
+ (suggestion ? ` Did you mean \`${suggestion}\`?` : '') +
1105
+ ` Supported operators: ${[...JSON_FILTER_KEYS].sort().join(', ')}.`);
1106
+ }
1107
+ // `mode` and `path` are modifiers, not comparisons: a filter made only of
1108
+ // those (or of keys holding `undefined`) compares nothing, which is exactly
1109
+ // the silent no-op shape above.
1110
+ if (present.length === 0) {
1111
+ throw new ValidationError(`JSON filter on ${column} names no comparison. Add one of: ${[...JSON_FILTER_KEYS]
1112
+ .filter((k) => k !== 'path' && k !== 'mode')
1113
+ .sort()
1114
+ .join(', ')}.`);
1115
+ }
1116
+ if (present.every((k) => k === 'path' || k === 'mode')) {
1117
+ throw new ValidationError(`JSON filter on ${column} selects a \`path\` but has no comparison. ` +
1118
+ `Add one of: ${[...JSON_FILTER_KEYS]
1119
+ .filter((k) => k !== 'path' && k !== 'mode')
1120
+ .sort()
1121
+ .join(', ')}.`);
1122
+ }
1123
+ }
1124
+ /**
1125
+ * The Prisma spelling of each JSON operator, so a migrator who writes the
1126
+ * name they already know gets told the turbine one instead of a bare list.
1127
+ */
1128
+ const JSON_PRISMA_SPELLINGS = {
1129
+ string_contains: 'stringContains',
1130
+ string_starts_with: 'stringStartsWith',
1131
+ string_ends_with: 'stringEndsWith',
1132
+ array_contains: 'contains',
1133
+ startsWith: 'stringStartsWith',
1134
+ endsWith: 'stringEndsWith',
1135
+ };
1136
+ /**
1137
+ * Whether {@link findUndefinedPath} would find anything, without building the
1138
+ * path strings. Same walk, same leaves, same plain-object rule, same depth bound.
1139
+ */
1140
+ export function holdsUndefinedValue(value, depth) {
1141
+ if (value === undefined)
1142
+ return true;
1143
+ if (typeof value !== 'object' || value === null || depth > 64)
1144
+ return false;
1145
+ if (Array.isArray(value)) {
1146
+ for (let i = 0; i < value.length; i++)
1147
+ if (holdsUndefinedValue(value[i], depth + 1))
1148
+ return true;
1149
+ return false;
1150
+ }
1151
+ const proto = Object.getPrototypeOf(value);
1152
+ if (proto !== Object.prototype && proto !== null)
1153
+ return false;
1154
+ for (const key of Object.keys(value)) {
1155
+ if (holdsUndefinedValue(value[key], depth + 1))
1156
+ return true;
1157
+ }
1158
+ return false;
1159
+ }
1160
+ /**
1161
+ * The key path of the first `undefined` inside a where object (a global filter),
1162
+ * or `null`.
1163
+ * Walks plain objects and arrays; values of any other kind (Dates, Buffers,
1164
+ * column references) are leaves. Key NAMES only, never values, so the path is
1165
+ * safe in an error message.
1166
+ */
1167
+ export function findUndefinedPath(value, path, depth) {
1168
+ if (value === undefined)
1169
+ return path || '(root)';
1170
+ if (typeof value !== 'object' || value === null || depth > 64)
1171
+ return null;
1172
+ if (Array.isArray(value)) {
1173
+ for (let i = 0; i < value.length; i++) {
1174
+ const hit = findUndefinedPath(value[i], `${path}[${i}]`, depth + 1);
1175
+ if (hit !== null)
1176
+ return hit;
1177
+ }
1178
+ return null;
1179
+ }
1180
+ const proto = Object.getPrototypeOf(value);
1181
+ if (proto !== Object.prototype && proto !== null)
1182
+ return null;
1183
+ for (const key of Object.keys(value)) {
1184
+ const hit = findUndefinedPath(value[key], path ? `${path}.${key}` : key, depth + 1);
1185
+ if (hit !== null)
1186
+ return hit;
1187
+ }
1188
+ return null;
1189
+ }
1190
+ /**
1191
+ * Refuse an {@link ArrayFilter} that would compile to less than it says: a key
1192
+ * that is not an array operator (it would be dropped, so `{ has: 'a', hsa: 'b' }`
1193
+ * filtered on `has` alone), an `isEmpty` that is not a boolean (it compiled to
1194
+ * nothing), or no comparison at all. Called by the SQL build and the cache-hit
1195
+ * param collect alike.
1196
+ */
1197
+ export function assertArrayFilter(filter, column) {
1198
+ const obj = filter;
1199
+ let comparisons = 0;
1200
+ for (const key of Object.keys(obj)) {
1201
+ const value = obj[key];
1202
+ if (value === undefined)
1203
+ continue;
1204
+ if (!ARRAY_OPERATOR_KEYS.has(key)) {
1205
+ throw new ValidationError(`Unknown array filter operator "${key}" on ${column}. Supported operators: ${[...ARRAY_OPERATOR_KEYS].join(', ')}.`);
1206
+ }
1207
+ if (key === 'isEmpty' && typeof value !== 'boolean') {
1208
+ throw new ValidationError(`\`isEmpty\` on ${column} takes true or false, received ${describeOperand(value)}.`);
1209
+ }
1210
+ comparisons++;
1211
+ }
1212
+ if (comparisons === 0) {
1213
+ throw new ValidationError(`Array filter on ${column} names no comparison. Add one of: ${[...ARRAY_OPERATOR_KEYS].join(', ')}.`);
1214
+ }
1215
+ }