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
@@ -12,6 +12,8 @@
12
12
  * primitives this module needs. See builder.ts for the thin delegating methods.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.TEXT_COMPARED_TYPES = exports.assertJsonFilterKeys = void 0;
16
+ exports.adviceLogger = adviceLogger;
15
17
  exports.fingerprintWhere = fingerprintWhere;
16
18
  exports.fingerprintRelationParts = fingerprintRelationParts;
17
19
  exports.fingerprintRelFilter = fingerprintRelFilter;
@@ -27,6 +29,7 @@ exports.buildWhere = buildWhere;
27
29
  exports.resolveGlobalFilterFrom = resolveGlobalFilterFrom;
28
30
  exports.resolveGlobalFilter = resolveGlobalFilter;
29
31
  exports.mergeGlobalFilter = mergeGlobalFilter;
32
+ exports.withTargetGlobalFilter = withTargetGlobalFilter;
30
33
  exports.targetGlobalFilterAlias = targetGlobalFilterAlias;
31
34
  exports.collectTargetGlobalFilterAlias = collectTargetGlobalFilterAlias;
32
35
  exports.targetGlobalFilterExists = targetGlobalFilterExists;
@@ -34,6 +37,7 @@ exports.collectTargetGlobalFilterExists = collectTargetGlobalFilterExists;
34
37
  exports.globalFilterCacheSegment = globalFilterCacheSegment;
35
38
  exports.userPredicateIsEmpty = userPredicateIsEmpty;
36
39
  exports.assertMutationHasPredicate = assertMutationHasPredicate;
40
+ exports.emptyMutationWhereMessage = emptyMutationWhereMessage;
37
41
  exports.buildWhereClause = buildWhereClause;
38
42
  exports.buildScalarClause = buildScalarClause;
39
43
  exports.emptyRelationsHost = emptyRelationsHost;
@@ -51,6 +55,8 @@ exports.buildRelationFilter = buildRelationFilter;
51
55
  exports.buildSubWhereForRelation = buildSubWhereForRelation;
52
56
  exports.pgTypeForColumn = pgTypeForColumn;
53
57
  exports.coerceWhereOperand = coerceWhereOperand;
58
+ exports.textComparedOperand = textComparedOperand;
59
+ exports.utcDateTimeRewrite = utcDateTimeRewrite;
54
60
  exports.enumTypeForColumn = enumTypeForColumn;
55
61
  exports.enumCastSuffix = enumCastSuffix;
56
62
  exports.assertBindableEqualityValue = assertBindableEqualityValue;
@@ -69,9 +75,9 @@ exports.normalizeRelationFilter = normalizeRelationFilter;
69
75
  exports.isJsonColumnType = isJsonColumnType;
70
76
  exports.getColumnPgType = getColumnPgType;
71
77
  exports.getArrayElementType = getArrayElementType;
72
- exports.assertJsonFilterKeys = assertJsonFilterKeys;
73
78
  exports.jsonStringEntries = jsonStringEntries;
74
79
  exports.jsonRangeEntries = jsonRangeEntries;
80
+ exports.jsonPathEqualsOperand = jsonPathEqualsOperand;
75
81
  exports.buildJsonFilterClauses = buildJsonFilterClauses;
76
82
  exports.jsonPathParam = jsonPathParam;
77
83
  exports.castJsonNumeric = castJsonNumeric;
@@ -80,11 +86,30 @@ exports.buildVectorFilterClauses = buildVectorFilterClauses;
80
86
  exports.vectorThresholdEntries = vectorThresholdEntries;
81
87
  exports.buildTextSearchClause = buildTextSearchClause;
82
88
  exports.getColumnArrayType = getColumnArrayType;
89
+ const diagnostics_js_1 = require("../diagnostics.js");
83
90
  const errors_js_1 = require("../errors.js");
91
+ const global_filters_config_js_1 = require("../global-filters-config.js");
92
+ const logger_js_1 = require("../logger.js");
84
93
  const schema_js_1 = require("../schema.js");
85
94
  const filters_js_1 = require("./filters.js");
86
95
  const utils_js_1 = require("./utils.js");
87
96
  const where_compile_js_1 = require("./where-compile.js");
97
+ // The JSON filter key check lives with the other operand checks in filters.ts;
98
+ // re-exported so the engines keep one import site for it.
99
+ var filters_js_2 = require("./filters.js");
100
+ Object.defineProperty(exports, "assertJsonFilterKeys", { enumerable: true, get: function () { return filters_js_2.assertJsonFilterKeys; } });
101
+ /**
102
+ * Where per-query advice goes for this context: the client's logger when its
103
+ * `diagnostics.advice` is on, else `undefined`. A site calls this first and
104
+ * builds its message only on a logger.
105
+ *
106
+ * A context without the client's diagnostics (one built by hand) resolves
107
+ * them here; every context a QueryInterface builds carries them, so on a query
108
+ * path this never reads the environment.
109
+ */
110
+ function adviceLogger(qi) {
111
+ return (qi.diagnostics ?? (0, diagnostics_js_1.resolveDiagnostics)()).advice ? (qi.logger ?? logger_js_1.consoleLogger) : undefined;
112
+ }
88
113
  /**
89
114
  * LIKE-escape one bound operand through the ACTIVE dialect, falling back to the
90
115
  * shared {@link escapeLike} (`\`, `%`, `_`) when the dialect declares no
@@ -120,6 +145,21 @@ function likeOperand(qi, value) {
120
145
  function valueForMessage(value) {
121
146
  return (0, errors_js_1.getErrorMessageMode)() === 'verbose' ? JSON.stringify(value) : '<value>';
122
147
  }
148
+ /** The operand-validation context for an operator object on `refCtx`'s column. */
149
+ function operandContext(qi, column, refCtx) {
150
+ if (!refCtx)
151
+ return { column, table: qi.table, kind: 'scalar' };
152
+ return { column: refCtx.rawColumn, table: refCtx.table, kind: (0, filters_js_1.operandColumnKind)(refCtx.pgType) };
153
+ }
154
+ /**
155
+ * The inner operator object of `not: { ... }`, with this object's `mode`
156
+ * inherited when the inner one sets none. The fingerprint reads the same
157
+ * effective mode (`fingerprintOperatorShape`).
158
+ */
159
+ function notOperand(op) {
160
+ const inner = op.not;
161
+ return inner.mode === undefined && op.mode !== undefined ? { ...inner, mode: op.mode } : inner;
162
+ }
123
163
  /**
124
164
  * Produce a value-invariant fingerprint of a where clause.
125
165
  * Same keys + same operator shapes + same combinator structure => same string.
@@ -156,25 +196,20 @@ function fingerprintWhere(qi, where, depth = 0) {
156
196
  }
157
197
  /**
158
198
  * Fingerprint the present branches of a normalized relation filter, in the
159
- * fixed order some→every→none→is→isNot. A `null` branch tokenizes as
160
- * `<branch>(null)`; a present branch recurses through
199
+ * fixed order some→every→none→is→isNot. A present branch recurses through
161
200
  * {@link fingerprintRelFilter} so the FULL inner shape is captured (two
162
- * different sub-wheres must never collide on one cached SQL text).
201
+ * different sub-wheres must never collide on one cached SQL text). `is: null`
202
+ * and `isNot: null` tokenize as `<branch>(null)`; the where walk refuses any
203
+ * other non-object value before this runs.
163
204
  */
164
205
  function fingerprintRelationParts(qi, relDef, filterObj, depth = 0) {
165
206
  const relParts = [];
166
207
  if (filterObj.some !== undefined)
167
- relParts.push(filterObj.some === null
168
- ? 'some(null)'
169
- : `some(${fingerprintRelFilter(qi, relDef.to, filterObj.some, depth + 1)})`);
208
+ relParts.push(`some(${fingerprintRelFilter(qi, relDef.to, filterObj.some, depth + 1)})`);
170
209
  if (filterObj.every !== undefined)
171
- relParts.push(filterObj.every === null
172
- ? 'every(null)'
173
- : `every(${fingerprintRelFilter(qi, relDef.to, filterObj.every, depth + 1)})`);
210
+ relParts.push(`every(${fingerprintRelFilter(qi, relDef.to, filterObj.every, depth + 1)})`);
174
211
  if (filterObj.none !== undefined)
175
- relParts.push(filterObj.none === null
176
- ? 'none(null)'
177
- : `none(${fingerprintRelFilter(qi, relDef.to, filterObj.none, depth + 1)})`);
212
+ relParts.push(`none(${fingerprintRelFilter(qi, relDef.to, filterObj.none, depth + 1)})`);
178
213
  if (filterObj.is !== undefined)
179
214
  relParts.push(filterObj.is === null
180
215
  ? 'is(null)'
@@ -199,32 +234,43 @@ function fingerprintRelFilter(qi, targetTable, subWhere, depth = 0) {
199
234
  /**
200
235
  * Walk a where clause and push ONLY values into `params`, in the EXACT same
201
236
  * order that `buildWhereClause` pushes them. Used on cache hit to fill params
202
- * without rebuilding SQL.
237
+ * without rebuilding SQL. Returns whether the clause compiles to a condition,
238
+ * i.e. whether `buildWhereClause` returns non-null for it.
203
239
  *
204
240
  * @internal Exposed as package-private for testing.
205
241
  */
206
242
  function collectWhereParams(qi, where, params, depth = 0) {
207
243
  (0, where_compile_js_1.assertWhereDepth)(depth);
244
+ let emitted = false;
208
245
  // ONE canonical walk (shared with fingerprintWhere + buildWhereClause), so
209
246
  // the key order + combinator structure cannot drift out of lockstep.
210
247
  for (const event of (0, where_compile_js_1.walkWhere)(qi.whereHost, where)) {
211
248
  switch (event.kind) {
212
249
  case 'or':
250
+ collectOrParams(event.conditions, params, (cond) => collectWhereParams(qi, cond, params, depth + 1));
251
+ emitted = true;
252
+ break;
213
253
  case 'and':
214
- for (const cond of event.conditions)
215
- collectWhereParams(qi, cond, params, depth + 1);
254
+ for (const cond of event.conditions) {
255
+ if (collectWhereParams(qi, cond, params, depth + 1))
256
+ emitted = true;
257
+ }
216
258
  break;
217
259
  case 'not':
218
- collectWhereParams(qi, event.condition, params, depth + 1);
260
+ if (collectWhereParams(qi, event.condition, params, depth + 1))
261
+ emitted = true;
219
262
  break;
220
263
  case 'relation':
221
- collectRelationFilterParams(qi, event.relDef, event.filterObj, params, depth + 1);
264
+ if (collectRelationFilterParams(qi, event.relDef, event.filterObj, params, depth + 1))
265
+ emitted = true;
222
266
  break;
223
267
  case 'scalar':
224
268
  collectScalarParams(qi, event.key, event.value, params);
269
+ emitted = true;
225
270
  break;
226
271
  }
227
272
  }
273
+ return emitted;
228
274
  }
229
275
  /**
230
276
  * Push a scalar WHERE value's params, mirroring {@link buildScalarClause}'s
@@ -252,7 +298,7 @@ function collectScalarParams(qi, key, value, params) {
252
298
  collectJsonFilterParams(qi, value, params, qi.q(rawColumn));
253
299
  return;
254
300
  case 'array':
255
- collectArrayFilterParams(qi, value, params);
301
+ collectArrayFilterParams(qi, value, params, qi.q(rawColumn));
256
302
  return;
257
303
  case 'textsearch':
258
304
  // Same gate the build path applies, so the collect path never diverges
@@ -266,15 +312,22 @@ function collectScalarParams(qi, key, value, params) {
266
312
  table: qi.table,
267
313
  prefix: '',
268
314
  rawColumn,
315
+ pgType: getColumnPgType(qi, rawColumn),
269
316
  });
270
317
  return;
271
- default:
318
+ default: {
272
319
  // 'equality' | 'jsonThrow' | 'arrayThrow': same strict validation as
273
320
  // the build path, so a cache hit can never silently bind a
274
321
  // misspelled-operator object.
275
- assertBindableEqualityValue(qi, rawColumn, value, getColumnPgType(qi, rawColumn), qi.table);
322
+ const pgType = getColumnPgType(qi, rawColumn);
323
+ assertBindableEqualityValue(qi, rawColumn, value, pgType, qi.table);
324
+ if (isBareJsonDocument(qi, pgType, value)) {
325
+ jsonDocumentEquals(qi, '', rawColumn, value, collectOnly(params));
326
+ return;
327
+ }
276
328
  params.push(coerceWhereOperand(qi, qi.tableMeta, rawColumn, value));
277
329
  return;
330
+ }
278
331
  }
279
332
  }
280
333
  /**
@@ -286,52 +339,69 @@ function collectScalarParams(qi, key, value, params) {
286
339
  * the gf calls are no-ops, so this stays byte-identical to the pre-0.28 path.
287
340
  * Shared by every collect site that mirrors buildRelationFilter
288
341
  * (collectWhereParams, collectRelFilterParams, collectAliasWhereParams).
342
+ * Returns whether {@link buildRelationFilter} emits a clause for it.
289
343
  */
290
344
  function collectRelationFilterParams(qi, relDef, filterObj, params, depth = 0) {
291
345
  const target = relDef.to;
292
- if (filterObj.some !== undefined && filterObj.some !== null) {
346
+ if (!qi.schema.tables[target])
347
+ return false;
348
+ let emitted = false;
349
+ if (filterObj.some !== undefined) {
293
350
  collectRelFilterParams(qi, target, filterObj.some, params, depth + 1);
294
351
  collectTargetGlobalFilterExists(qi, target, params);
352
+ emitted = true;
295
353
  }
296
- if (filterObj.none !== undefined && filterObj.none !== null) {
354
+ if (filterObj.none !== undefined) {
297
355
  collectRelFilterParams(qi, target, filterObj.none, params, depth + 1);
298
356
  collectTargetGlobalFilterExists(qi, target, params);
357
+ emitted = true;
299
358
  }
300
- if (filterObj.every !== undefined && filterObj.every !== null) {
301
- // gf is only emitted (build) when the `every` sub-where compiles to a
302
- // filter, otherwise `every` is trivially true and no subquery is built.
303
- if (buildSubWhereForRelation(qi, target, qi.q(target), filterObj.every, [], depth + 1) !==
304
- null) {
305
- collectRelFilterParams(qi, target, filterObj.every, params, depth + 1);
306
- collectTargetGlobalFilterExists(qi, target, params);
307
- }
359
+ // gf is only emitted (build) when the `every` sub-where compiles to a
360
+ // filter, otherwise `every` is trivially true and no subquery is built. A
361
+ // sub-where that compiles to nothing pushes nothing.
362
+ if (filterObj.every !== undefined &&
363
+ collectRelFilterParams(qi, target, filterObj.every, params, depth + 1)) {
364
+ collectTargetGlobalFilterExists(qi, target, params);
365
+ emitted = true;
308
366
  }
309
367
  if (filterObj.is !== undefined) {
310
368
  if (filterObj.is !== null)
311
369
  collectRelFilterParams(qi, target, filterObj.is, params, depth + 1);
312
370
  collectTargetGlobalFilterExists(qi, target, params);
371
+ emitted = true;
313
372
  }
314
373
  if (filterObj.isNot !== undefined) {
315
374
  if (filterObj.isNot !== null)
316
375
  collectRelFilterParams(qi, target, filterObj.isNot, params, depth + 1);
317
376
  collectTargetGlobalFilterExists(qi, target, params);
377
+ emitted = true;
318
378
  }
379
+ return emitted;
319
380
  }
381
+ /** Collect a relation sub-where's params; returns whether it compiles to a condition. */
320
382
  function collectRelFilterParams(qi, targetTable, subWhere, params, depth = 0) {
321
383
  const meta = qi.schema.tables[targetTable];
322
384
  if (!meta)
323
- return;
385
+ return false;
324
386
  // The FROM-item reference only shapes SQL text, never params, so the bare
325
387
  // name serves the collect mirror whether or not the build path aliased it.
326
- collectScopedWhereParams(qi, relationWhereScope(qi, targetTable, meta, qi.q(targetTable)), subWhere, params, depth);
388
+ return collectScopedWhereParams(qi, relationWhereScope(qi, targetTable, meta, qi.q(targetTable)), subWhere, params, depth);
327
389
  }
328
390
  /**
329
391
  * Collect params from operator clauses. Mirrors buildOperatorClauses:
330
392
  * {@link ColumnRef} values compile into the SQL text, so they push NOTHING -
331
393
  * but they re-run the same validation (unknown ref / insensitive mode) so a
332
- * warmed cache can never skip a check the build path enforces.
394
+ * warmed cache can never skip a check the build path enforces. The operand
395
+ * validation ({@link assertOperatorOperands}) runs here too, for the same
396
+ * reason.
333
397
  */
334
398
  function collectOperatorParams(qi, column, op, params, refCtx) {
399
+ const operand = operandContext(qi, column, refCtx);
400
+ (0, filters_js_1.assertOperatorOperands)(op, operand);
401
+ collectOperatorParamsChecked(qi, column, op, params, refCtx, operand);
402
+ }
403
+ /** {@link collectOperatorParams} after validation; recursive for a `not` operator object. */
404
+ function collectOperatorParamsChecked(qi, column, op, params, refCtx, operand) {
335
405
  const skipRef = (v) => {
336
406
  if (!(0, filters_js_1.isColumnRef)(v))
337
407
  return false;
@@ -343,7 +413,6 @@ function collectOperatorParams(qi, column, op, params, refCtx) {
343
413
  const cv = (v) => (refCtx ? coerceWhereOperand(qi, refCtx.meta, refCtx.rawColumn, v) : v);
344
414
  const insensitive = op.mode === 'insensitive';
345
415
  if (op.equals !== undefined && op.equals !== null && !skipRef(op.equals)) {
346
- (0, filters_js_1.assertBindableEqualsOperand)(op.equals, `"${column}"`);
347
416
  if (insensitive)
348
417
  assertInsensitiveOperand(op.equals, 'equals', column);
349
418
  params.push(cv(op.equals));
@@ -357,9 +426,18 @@ function collectOperatorParams(qi, column, op, params, refCtx) {
357
426
  if (op.lte !== undefined && !skipRef(op.lte))
358
427
  params.push(cv(op.lte));
359
428
  if (op.not !== undefined && op.not !== null && !skipRef(op.not)) {
360
- if (insensitive)
361
- assertInsensitiveOperand(op.not, 'not', column);
362
- params.push(cv(op.not));
429
+ const negatedDocument = jsonNegatedDocument(qi, op.not, operand);
430
+ if (negatedDocument !== NO_JSON_NEGATION) {
431
+ jsonDocumentEquals(qi, '', column, negatedDocument, collectOnly(params));
432
+ }
433
+ else if (isNegatedOperatorObject(op.not, operand)) {
434
+ collectOperatorParamsChecked(qi, column, notOperand(op), params, refCtx, operand);
435
+ }
436
+ else {
437
+ if (insensitive)
438
+ assertInsensitiveOperand(op.not, 'not', column);
439
+ params.push(cv(op.not));
440
+ }
363
441
  }
364
442
  if (op.in !== undefined) {
365
443
  if (insensitive) {
@@ -382,6 +460,14 @@ function collectOperatorParams(qi, column, op, params, refCtx) {
382
460
  if (op.endsWith !== undefined)
383
461
  params.push(`%${likeOperand(qi, op.endsWith)}`);
384
462
  }
463
+ /**
464
+ * Is this `not` operand an operator object to negate as a whole, rather than a
465
+ * value? On a JSON column an object is a JSON VALUE (`not: { a: 1 }` compares
466
+ * documents), so only non-JSON columns read it as an operator object.
467
+ */
468
+ function isNegatedOperatorObject(value, operand) {
469
+ return operand.kind !== 'json' && (0, filters_js_1.isWhereOperator)(value);
470
+ }
385
471
  /**
386
472
  * Collect params from JSON filter. Mirrors buildJsonFilterClauses exactly:
387
473
  * the `path` is bound at most once (its placeholder is shared by every
@@ -389,7 +475,7 @@ function collectOperatorParams(qi, column, op, params, refCtx) {
389
475
  * comparison values in {@link JSON_RANGE_OPERATORS} order.
390
476
  */
391
477
  function collectJsonFilterParams(qi, filter, params, column) {
392
- assertJsonFilterKeys(filter, column);
478
+ (0, filters_js_1.assertJsonFilterKeys)(filter, column);
393
479
  let pathPushed = false;
394
480
  const pushPathOnce = () => {
395
481
  if (!pathPushed) {
@@ -400,14 +486,18 @@ function collectJsonFilterParams(qi, filter, params, column) {
400
486
  };
401
487
  if (filter.path !== undefined && filter.equals !== undefined) {
402
488
  pushPathOnce();
403
- params.push(String(filter.equals));
489
+ const hook = jsonPathEqualsHook(qi);
490
+ if (hook) {
491
+ hook.call(qi.dialect, jsonPathEqualsInput(qi, filter, column, '', '', collectOnly(params)));
492
+ }
493
+ else {
494
+ params.push(jsonPathEqualsOperand(qi, filter.equals, column));
495
+ }
404
496
  }
405
497
  else if (filter.equals !== undefined) {
406
- // Mirrors the build path's refusal, and thrown before any param is pushed,
407
- // like the text-search gate a few cases above. Not defensive: it is what
408
- // stops a warmed template serving a shape the cold build refuses.
409
- requireJsonContains(qi, 'equals');
410
- params.push(JSON.stringify(filter.equals));
498
+ // Mirrors the build path, refusals included: a warmed template must never
499
+ // serve a shape the cold build refuses.
500
+ jsonDocumentEquals(qi, '', column, filter.equals, collectOnly(params));
411
501
  }
412
502
  if (filter.contains !== undefined) {
413
503
  requireJsonContains(qi, 'contains');
@@ -426,8 +516,9 @@ function collectJsonFilterParams(qi, filter, params, column) {
426
516
  }
427
517
  }
428
518
  /** Collect params from array filter. Mirrors buildArrayFilterClauses. */
429
- function collectArrayFilterParams(qi, filter, params) {
519
+ function collectArrayFilterParams(qi, filter, params, column) {
430
520
  requireArrayColumns(qi);
521
+ (0, filters_js_1.assertArrayFilter)(filter, column);
431
522
  if (filter.has !== undefined)
432
523
  params.push(filter.has);
433
524
  if (filter.hasEvery !== undefined)
@@ -461,34 +552,57 @@ function buildWhere(qi, where) {
461
552
  * THE global-filter resolution rule, with no dependency on a `BuilderCtx`.
462
553
  *
463
554
  * Split out from {@link resolveGlobalFilter} so `PowqlInterface` can consume
464
- * the SAME rule rather than a transcription of it. It is a parallel
465
- * implementation of the public surface, and this repository's history is
466
- * unambiguous that a rule written twice is how two engines come to disagree
467
- * (the 0.64 projection resolver, the 0.73 findUnique guard). Every clause here
468
- * is load-bearing on both engines: a function filter is evaluated PER BUILD so
469
- * per-request tenancy works, an all-undefined filter is treated as absent so it
470
- * cannot emit a dangling clause, and `skip` is honoured in both its whole-query
471
- * and per-table forms.
472
- */
473
- function resolveGlobalFilterFrom(filters, table, skip) {
474
- if (!filters)
475
- return null;
555
+ * the SAME rule rather than a transcription of it: a rule written twice is how
556
+ * two engines come to disagree. A function filter is evaluated PER BUILD so
557
+ * per-request tenancy works, and `skip` is honoured in both its whole-query and
558
+ * per-table forms.
559
+ *
560
+ * FAIL CLOSED. A filter that resolves to `undefined`, or to an object holding
561
+ * an `undefined` anywhere inside it, throws {@link ValidationError} (E003). A
562
+ * missing tenant context is exactly what produces `{ tenantId: undefined }`,
563
+ * and treating that as "no filter" read and wrote every tenant's rows. The
564
+ * explicit values `null` and `{}` still mean "no filter for this call": neither
565
+ * is what a missing value produces.
566
+ *
567
+ * A `skipGlobalFilters` list names tables by table name or client accessor,
568
+ * the two spellings `globalFilters` keys take. With `tables` (the schema's
569
+ * table map), an entry that names no table is refused (E003): it would skip
570
+ * nothing, so the caller who meant to lift a filter would silently keep it.
571
+ */
572
+ function resolveGlobalFilterFrom(filters, table, skip, tables) {
476
573
  if (skip === true)
477
574
  return null;
478
- if (Array.isArray(skip) && skip.includes(table))
575
+ if (Array.isArray(skip)) {
576
+ if (tables !== undefined)
577
+ (0, global_filters_config_js_1.assertSkipListNames)(skip, tables);
578
+ if ((0, global_filters_config_js_1.skipListNamesTable)(skip, table, tables))
579
+ return null;
580
+ }
581
+ if (!filters)
479
582
  return null;
480
- const raw = filters[table];
481
- if (raw === undefined)
583
+ if (!Object.hasOwn(filters, table))
482
584
  return null;
585
+ const raw = filters[table];
483
586
  const resolved = typeof raw === 'function' ? raw() : raw;
484
- if (resolved === null || resolved === undefined)
485
- return null;
486
- const obj = resolved;
487
- // An all-undefined filter (e.g. `{ tenantId: undefined }`) contributes
488
- // nothing, treat it as absent so it never emits a dangling clause.
489
- if (Object.keys(obj).every((k) => obj[k] === undefined))
587
+ if (resolved === null)
490
588
  return null;
491
- return obj;
589
+ if (resolved === undefined)
590
+ throw unresolvedGlobalFilter(table, 'resolved to undefined');
591
+ if (typeof resolved !== 'object' || Array.isArray(resolved)) {
592
+ throw new errors_js_1.ValidationError(`Global filter for "${table}" must be a where object (or a function returning one), received ` +
593
+ `${Array.isArray(resolved) ? 'an array' : typeof resolved}.`);
594
+ }
595
+ // The boolean scan allocates nothing; the path string is built only on the throw path.
596
+ const hole = (0, filters_js_1.holdsUndefinedValue)(resolved, 0) ? (0, filters_js_1.findUndefinedPath)(resolved, '', 0) : null;
597
+ if (hole !== null)
598
+ throw unresolvedGlobalFilter(table, `has an undefined value at "${hole}"`);
599
+ return Object.keys(resolved).length === 0 ? null : resolved;
600
+ }
601
+ function unresolvedGlobalFilter(table, what) {
602
+ return new errors_js_1.ValidationError(`Global filter for "${table}" ${what}. A global filter is never dropped because its input is missing: ` +
603
+ "an undefined tenant id would otherwise read and write every tenant's rows. Supply the value it reads " +
604
+ '(for example the current tenant) before this query runs, or pass `skipGlobalFilters: UNSAFE` ' +
605
+ "(import { UNSAFE } from 'turbine-orm') on a query that must see every row.");
492
606
  }
493
607
  /**
494
608
  * Resolve the configured global filter for `table`, evaluating a function
@@ -496,7 +610,7 @@ function resolveGlobalFilterFrom(filters, table, skip) {
496
610
  * no filter applies, the query opted out, or the filter is empty.
497
611
  */
498
612
  function resolveGlobalFilter(qi, table, skip = qi.currentSkip) {
499
- return resolveGlobalFilterFrom(qi.globalFilters, table, skip);
613
+ return resolveGlobalFilterFrom(qi.globalFilters, table, skip, qi.schema.tables);
500
614
  }
501
615
  /**
502
616
  * AND-merge this table's resolved global filter into a user `where`. Either
@@ -514,6 +628,45 @@ function mergeGlobalFilter(qi, userWhere) {
514
628
  // {@link markInternalCombinator}.
515
629
  return (0, utils_js_1.markInternalCombinator)({ AND: [userWhere, gf] });
516
630
  }
631
+ /**
632
+ * The tables whose global filter is being compiled right now, per builder
633
+ * context. A table's filter applies to every relation target a query reaches,
634
+ * the targets inside another table's filter included, so two filters that
635
+ * reach each other through relations (or one that reaches its own table) would
636
+ * apply each other without end. While a table's filter is being compiled, a
637
+ * relation that reaches that table again reads its rows unfiltered, which ends
638
+ * the chain. A static filter that forms such a cycle is refused when the client
639
+ * is built (`normalizeGlobalFilters`); this covers function filters, whose
640
+ * relations are known only per query. Compilation is synchronous, so the stack
641
+ * is pushed and popped around each application, on the build and the collect
642
+ * pass alike.
643
+ */
644
+ const filtersBeingApplied = new WeakMap();
645
+ /**
646
+ * Apply `table`'s global filter as a relation target: `apply(gf)` with the
647
+ * table on the stack, or `none` when no filter applies or the table's filter
648
+ * is already being compiled further out (see {@link filtersBeingApplied}).
649
+ * Every target application goes through here, build and collect alike.
650
+ */
651
+ function withTargetGlobalFilter(qi, table, none, apply) {
652
+ let stack = filtersBeingApplied.get(qi);
653
+ if (stack?.includes(table))
654
+ return none;
655
+ const gf = resolveGlobalFilter(qi, table);
656
+ if (!gf)
657
+ return none;
658
+ if (!stack) {
659
+ stack = [];
660
+ filtersBeingApplied.set(qi, stack);
661
+ }
662
+ stack.push(table);
663
+ try {
664
+ return apply(gf);
665
+ }
666
+ finally {
667
+ stack.pop();
668
+ }
669
+ }
517
670
  /**
518
671
  * SQL clause for `targetTable`'s global filter rendered against `alias`
519
672
  * (relation subqueries, `_count`, relation `orderBy`). Pushes its params to
@@ -521,23 +674,17 @@ function mergeGlobalFilter(qi, userWhere) {
521
674
  * {@link collectTargetGlobalFilterAlias}.
522
675
  */
523
676
  function targetGlobalFilterAlias(qi, targetTable, alias, params) {
524
- const gf = resolveGlobalFilter(qi, targetTable);
525
- if (!gf)
526
- return '';
527
677
  const meta = qi.schema.tables[targetTable];
528
678
  if (!meta)
529
679
  return '';
530
- return buildAliasWhere(qi, targetTable, meta, alias, gf, params) ?? '';
680
+ return withTargetGlobalFilter(qi, targetTable, '', (gf) => buildAliasWhere(qi, targetTable, meta, alias, gf, params) ?? '');
531
681
  }
532
682
  /** Param-collect mirror of {@link targetGlobalFilterAlias}. */
533
683
  function collectTargetGlobalFilterAlias(qi, targetTable, params) {
534
- const gf = resolveGlobalFilter(qi, targetTable);
535
- if (!gf)
536
- return;
537
684
  const meta = qi.schema.tables[targetTable];
538
685
  if (!meta)
539
686
  return;
540
- collectAliasWhereParams(qi, targetTable, meta, gf, params);
687
+ withTargetGlobalFilter(qi, targetTable, false, (gf) => collectAliasWhereParams(qi, targetTable, meta, gf, params));
541
688
  }
542
689
  /**
543
690
  * SQL clause for `targetTable`'s global filter rendered against `targetRef`,
@@ -546,17 +693,11 @@ function collectTargetGlobalFilterAlias(qi, targetTable, params) {
546
693
  * params; `''` when none. Mirror: {@link collectTargetGlobalFilterExists}.
547
694
  */
548
695
  function targetGlobalFilterExists(qi, targetTable, targetRef, params) {
549
- const gf = resolveGlobalFilter(qi, targetTable);
550
- if (!gf)
551
- return '';
552
- return buildSubWhereForRelation(qi, targetTable, targetRef, gf, params) ?? '';
696
+ return withTargetGlobalFilter(qi, targetTable, '', (gf) => buildSubWhereForRelation(qi, targetTable, targetRef, gf, params) ?? '');
553
697
  }
554
698
  /** Param-collect mirror of {@link targetGlobalFilterExists}. */
555
699
  function collectTargetGlobalFilterExists(qi, targetTable, params) {
556
- const gf = resolveGlobalFilter(qi, targetTable);
557
- if (!gf)
558
- return;
559
- collectRelFilterParams(qi, targetTable, gf, params);
700
+ withTargetGlobalFilter(qi, targetTable, false, (gf) => collectRelFilterParams(qi, targetTable, gf, params));
560
701
  }
561
702
  /**
562
703
  * Value-invariant SQL-cache-key segment for the active global-filter
@@ -601,16 +742,41 @@ function globalFilterCacheSegment(qi) {
601
742
  return parts.length ? `|gf=${parts.join(';')}` : '';
602
743
  }
603
744
  /**
604
- * True when the USER-supplied `where` compiles to no predicate (`{}`,
605
- * `{ id: undefined }`, `{ OR: [{ a: undefined }] }`, …). This is the exact
606
- * signal the empty-`where` guard needs, the compiled emptiness, NOT the
607
- * fingerprint (which is non-empty for an all-undefined `OR`/`AND`). It ignores
608
- * any configured global filter, so a global filter never lets an unguarded
609
- * mass mutation through.
745
+ * True when the USER-supplied `where` narrows no row: it compiles to no
746
+ * predicate (`{}`, `{ id: undefined }`, `{ OR: [{ a: undefined }] }`, …), or
747
+ * to one that is TRUE for every row (`{ NOT: { OR: [] } }`,
748
+ * `{ id: { notIn: [] } }`, and on a NOT NULL column `{ email: { contains: '' } }`
749
+ * or `{ id: { not: null } }`, see {@link foldWhere}). Either way a mutation
750
+ * guarded by it would write every row. It reads the compiled emptiness, NOT
751
+ * the fingerprint (which is non-empty for an all-undefined `OR`/`AND`), and it
752
+ * ignores any configured global filter, so a global filter never lets an
753
+ * unguarded mass mutation through.
610
754
  */
611
755
  function userPredicateIsEmpty(qi, userWhere) {
612
756
  const throwaway = [];
613
- return buildWhereClause(qi, userWhere, throwaway) === null;
757
+ if (buildWhereClause(qi, userWhere, throwaway) === null)
758
+ return true;
759
+ return (0, where_compile_js_1.foldWhere)(qi.whereHost, userWhere, (table) => tableWhereHost(qi, table), 0, guardFoldOptions(qi)) === 'true';
760
+ }
761
+ /**
762
+ * The guard's fold reads NOT NULL columns, and trusts a declared foreign key
763
+ * except on SQLite, which enforces one only when the connection turned
764
+ * `foreign_keys` on, so a row there can point at a row that does not exist.
765
+ */
766
+ function guardFoldOptions(qi) {
767
+ return {
768
+ notNull: {
769
+ foreignKeys: qi.dialect.capabilities.enforcesForeignKeys,
770
+ hasGlobalFilter: (table) => qi.globalFilters !== undefined && Object.hasOwn(qi.globalFilters, table),
771
+ },
772
+ };
773
+ }
774
+ /** The where host of `table` in this client's schema, or `undefined` when the schema has no such table. */
775
+ function tableWhereHost(qi, table) {
776
+ const meta = (0, utils_js_1.ownLookup)(qi.schema.tables, table);
777
+ if (!meta)
778
+ return undefined;
779
+ return meta.name === qi.tableMeta.name ? qi.whereHost : scopedWhereHost(qi, meta);
614
780
  }
615
781
  function assertMutationHasPredicate(qi, operation, whereSql,
616
782
  // Already RESOLVED by the caller (writes.ts) through `resolveUnsafeFlag`, so
@@ -622,9 +788,18 @@ allowFullTableScan) {
622
788
  return;
623
789
  if (allowFullTableScan === true)
624
790
  return;
625
- throw new errors_js_1.ValidationError(`${operation} on "${qi.table}" refused: the \`where\` clause is empty. ` +
791
+ throw new errors_js_1.ValidationError(emptyMutationWhereMessage(operation, qi.table));
792
+ }
793
+ /**
794
+ * The mass-mutation refusal, one text for every engine. "Empty" covers a where
795
+ * that is always true (see {@link userPredicateIsEmpty}), which writes every
796
+ * row just the same.
797
+ */
798
+ function emptyMutationWhereMessage(operation, table) {
799
+ return (`${operation} on "${table}" refused: the \`where\` clause is empty or always true ` +
800
+ '(for example `NOT: { OR: [] }` or `{ id: { notIn: [] } }`), so it would apply to every row. ' +
626
801
  "Pass `allowFullTableScan: UNSAFE` to opt in (import { UNSAFE } from 'turbine-orm'), " +
627
- 'or check that your filter values are defined.');
802
+ 'or check that your filter values are defined and your lists are not empty.');
628
803
  }
629
804
  /**
630
805
  * Build the inner WHERE expression (without the WHERE keyword).
@@ -648,18 +823,12 @@ function buildWhereClause(qi, where, params, depth = 0) {
648
823
  case 'or': {
649
824
  if (!internal)
650
825
  qi.markVariableArity();
651
- const orClauses = [];
652
- for (const orCond of event.conditions) {
653
- const sub = buildWhereClause(qi, orCond, params, depth + 1);
654
- if (sub)
655
- orClauses.push(sub);
656
- }
657
- if (orClauses.length > 0)
658
- andClauses.push(`(${orClauses.join(' OR ')})`);
826
+ andClauses.push(buildOrClause(event.conditions, params, (cond) => buildWhereClause(qi, cond, params, depth + 1)));
659
827
  break;
660
828
  }
661
829
  case 'and':
662
- if (!internal)
830
+ // `AND: { ... }` is one branch whatever the caller sends (fixedArity).
831
+ if (!internal && !event.fixedArity)
663
832
  qi.markVariableArity();
664
833
  for (const andCond of event.conditions) {
665
834
  const sub = buildWhereClause(qi, andCond, params, depth + 1);
@@ -689,6 +858,46 @@ function buildWhereClause(qi, where, params, depth = 0) {
689
858
  return null;
690
859
  return andClauses.join(' AND ');
691
860
  }
861
+ /**
862
+ * The SQL an `OR` compiles to when one of its branches compiles to nothing.
863
+ * Written out rather than left empty, because `NOT` of an empty clause applies
864
+ * no condition while `NOT` of this matches no row.
865
+ */
866
+ const ALWAYS_TRUE_SQL = '(1 = 1)';
867
+ /**
868
+ * Compile an `OR` from its branches. A branch with no condition (`{}`,
869
+ * `{ AND: [] }`) is TRUE for every row, so the whole `OR` is TRUE: the clause
870
+ * is {@link ALWAYS_TRUE_SQL} and the params the other branches pushed are
871
+ * taken back. Every branch is still built, so each is validated exactly as the
872
+ * param collect validates it. {@link collectOrParams} is the mirror.
873
+ */
874
+ function buildOrClause(branches, params, build) {
875
+ const mark = params.length;
876
+ const parts = [];
877
+ let alwaysTrue = false;
878
+ for (const branch of branches) {
879
+ const sub = build(branch);
880
+ if (sub === null)
881
+ alwaysTrue = true;
882
+ else
883
+ parts.push(sub);
884
+ }
885
+ if (!alwaysTrue)
886
+ return `(${parts.join(' OR ')})`;
887
+ params.length = mark;
888
+ return ALWAYS_TRUE_SQL;
889
+ }
890
+ /** Param-collect mirror of {@link buildOrClause}; `collect` reports whether the branch compiled to a condition. */
891
+ function collectOrParams(branches, params, collect) {
892
+ const mark = params.length;
893
+ let alwaysTrue = false;
894
+ for (const branch of branches) {
895
+ if (!collect(branch))
896
+ alwaysTrue = true;
897
+ }
898
+ if (alwaysTrue)
899
+ params.length = mark;
900
+ }
692
901
  /**
693
902
  * Emit the SQL clause(s) for one scalar WHERE key onto `andClauses`, pushing
694
903
  * any params. The shape decision comes from the shared
@@ -731,18 +940,52 @@ function buildScalarClause(qi, key, value, params, andClauses) {
731
940
  table: qi.table,
732
941
  prefix: '',
733
942
  rawColumn,
943
+ pgType: getColumnPgType(qi, rawColumn),
734
944
  }));
735
945
  return;
736
- default:
946
+ default: {
737
947
  // 'equality': a plain object literal that matched no known filter shape
738
948
  // is almost always a misspelled operator (`startWith` for `startsWith`);
739
949
  // the guard also runs on the cache-hit param-collect path.
740
- assertBindableEqualityValue(qi, rawColumn, value, getColumnPgType(qi, rawColumn), qi.table);
950
+ const pgType = getColumnPgType(qi, rawColumn);
951
+ assertBindableEqualityValue(qi, rawColumn, value, pgType, qi.table);
952
+ if (isBareJsonDocument(qi, pgType, value)) {
953
+ andClauses.push(jsonDocumentEquals(qi, column, rawColumn, value, bindInto(qi, params)));
954
+ return;
955
+ }
741
956
  params.push(coerceWhereOperand(qi, qi.tableMeta, rawColumn, value));
742
957
  andClauses.push(`${column} = ${qi.p(params.length)}`);
743
958
  return;
959
+ }
744
960
  }
745
961
  }
962
+ /**
963
+ * A bare object on a JSON column is a whole-document equality, the same thing
964
+ * `equals` is. PostgreSQL's `jsonb = $1` already reads it so. MySQL compared
965
+ * the column with the bound TEXT, which matched no row (and its `NOT` every
966
+ * row), so there it compiles to the typed comparison `equals` uses. SQLite and
967
+ * SQL Server compared the stored text, so key order and whitespace decided the
968
+ * match; they refuse it (E017) as they refuse `equals`, and a bare array with
969
+ * it. Decided from the dialect, the column and the value's shape, all of which
970
+ * the cache key already carries (a bare object fingerprints as `obj(...)`; a
971
+ * bare array only ever refuses, so it changes no SQL).
972
+ */
973
+ function isBareJsonDocument(qi, pgType, value) {
974
+ if (qi.dialect.capabilities.jsonDocumentEquality === 'jsonb' || (0, filters_js_1.operandColumnKind)(pgType) !== 'json')
975
+ return false;
976
+ return (0, filters_js_1.isPlainRecord)(value) || (Array.isArray(value) && !comparesJsonDocuments(qi));
977
+ }
978
+ /** Whether the engine compares two whole JSON documents exactly (key order and whitespace aside). */
979
+ function comparesJsonDocuments(qi) {
980
+ return qi.dialect.capabilities.jsonDocumentEquality !== 'text';
981
+ }
982
+ /** A bind callback for the build passes: pushes the value and returns its placeholder. */
983
+ function bindInto(qi, params) {
984
+ return (value) => {
985
+ params.push(value);
986
+ return qi.p(params.length);
987
+ };
988
+ }
746
989
  /**
747
990
  * A {@link WhereHost} with no relations, used to fingerprint a sub-where
748
991
  * whose target table is unknown (`schema.tables[t]` miss). `walkWhere` reads
@@ -802,8 +1045,8 @@ function relationWhereScope(qi, targetTable, meta, targetRef) {
802
1045
  */
803
1046
  function aliasWhereScope(qi, targetTable, meta, alias) {
804
1047
  if (alias.includes('"')) {
805
- throw new errors_js_1.ValidationError(`Internal: aliasWhereScope was given the rendered reference ${alias} for table "${targetTable}", ` +
806
- 'but it takes a bare alias and quotes it itself. Use renderedRefWhereScope for an already-quoted reference.');
1048
+ throw new errors_js_1.InvariantError('where-scope-reference', `aliasWhereScope was given the rendered reference ${alias} for table "${targetTable}", but it takes a ` +
1049
+ 'bare alias and quotes it itself. Use renderedRefWhereScope for an already-quoted reference.');
807
1050
  }
808
1051
  return {
809
1052
  meta,
@@ -859,17 +1102,24 @@ function buildScopedWhere(qi, scope, where, params, depth = 0) {
859
1102
  for (const event of (0, where_compile_js_1.walkWhere)(scope.host, where)) {
860
1103
  switch (event.kind) {
861
1104
  case 'or':
862
- case 'and': {
863
1105
  // Same variable-arity rule as the top level: a nested relation filter
864
1106
  // or a relation `with` where can carry a caller-sized OR just as easily.
865
1107
  if (!internal)
866
1108
  qi.markVariableArity();
1109
+ clauses.push(buildOrClause(event.conditions, params, (cond) => {
1110
+ const sub = buildScopedWhere(qi, scope, cond, params, depth + 1);
1111
+ return sub === null ? null : `(${sub})`;
1112
+ }));
1113
+ break;
1114
+ case 'and': {
1115
+ if (!internal && !event.fixedArity)
1116
+ qi.markVariableArity();
867
1117
  const parts = event.conditions
868
1118
  .map((cond) => buildScopedWhere(qi, scope, cond, params, depth + 1))
869
1119
  .filter((s) => s !== null)
870
1120
  .map((s) => `(${s})`);
871
1121
  if (parts.length > 0)
872
- clauses.push(`(${parts.join(event.kind === 'or' ? ' OR ' : ' AND ')})`);
1122
+ clauses.push(`(${parts.join(' AND ')})`);
873
1123
  break;
874
1124
  }
875
1125
  case 'not': {
@@ -939,10 +1189,16 @@ function buildScopedScalarClause(qi, scope, field, value, params, clauses) {
939
1189
  table: scope.table,
940
1190
  prefix: scope.qualifier,
941
1191
  rawColumn: col,
1192
+ pgType: pgTypeForColumn(qi, meta, col),
942
1193
  }));
943
1194
  return;
944
1195
  }
945
- assertBindableEqualityValue(qi, col, value, pgTypeForColumn(qi, meta, col), scope.table);
1196
+ const pgType = pgTypeForColumn(qi, meta, col);
1197
+ assertBindableEqualityValue(qi, col, value, pgType, scope.table);
1198
+ if (isBareJsonDocument(qi, pgType, value)) {
1199
+ clauses.push(jsonDocumentEquals(qi, qCol, col, value, bindInto(qi, params)));
1200
+ return;
1201
+ }
946
1202
  params.push(coerceWhereOperand(qi, meta, col, value));
947
1203
  clauses.push(`${qCol} = ${qi.p(params.length)}`);
948
1204
  }
@@ -950,30 +1206,41 @@ function buildScopedScalarClause(qi, scope, field, value, params, clauses) {
950
1206
  * Cache-hit param-collect mirror of {@link buildScopedWhere}: pushes the exact
951
1207
  * same params in the exact same order (driven by the same {@link walkWhere}),
952
1208
  * without rebuilding SQL. Serves both {@link collectRelFilterParams} and
953
- * {@link collectAliasWhereParams}.
1209
+ * {@link collectAliasWhereParams}. Returns whether the clause compiles to a
1210
+ * condition (`buildScopedWhere` non-null).
954
1211
  */
955
1212
  function collectScopedWhereParams(qi, scope, where, params, depth = 0) {
956
1213
  (0, where_compile_js_1.assertWhereDepth)(depth);
1214
+ let emitted = false;
957
1215
  for (const event of (0, where_compile_js_1.walkWhere)(scope.host, where)) {
958
1216
  switch (event.kind) {
959
1217
  case 'or':
1218
+ collectOrParams(event.conditions, params, (cond) => collectScopedWhereParams(qi, scope, cond, params, depth + 1));
1219
+ emitted = true;
1220
+ break;
960
1221
  case 'and':
961
- for (const cond of event.conditions)
962
- collectScopedWhereParams(qi, scope, cond, params, depth + 1);
1222
+ for (const cond of event.conditions) {
1223
+ if (collectScopedWhereParams(qi, scope, cond, params, depth + 1))
1224
+ emitted = true;
1225
+ }
963
1226
  break;
964
1227
  case 'not':
965
- collectScopedWhereParams(qi, scope, event.condition, params, depth + 1);
1228
+ if (collectScopedWhereParams(qi, scope, event.condition, params, depth + 1))
1229
+ emitted = true;
966
1230
  break;
967
1231
  case 'relation':
968
1232
  // Same some→none→every→is→isNot (each: sub-where params then target
969
1233
  // global-filter params) as buildRelationFilter emits.
970
- collectRelationFilterParams(qi, event.relDef, event.filterObj, params, depth);
1234
+ if (collectRelationFilterParams(qi, event.relDef, event.filterObj, params, depth))
1235
+ emitted = true;
971
1236
  break;
972
1237
  case 'scalar':
973
1238
  collectScopedScalarParams(qi, scope, event.key, event.value, params);
1239
+ emitted = true;
974
1240
  break;
975
1241
  }
976
1242
  }
1243
+ return emitted;
977
1244
  }
978
1245
  /** Param-collect mirror of {@link buildScopedScalarClause}. */
979
1246
  function collectScopedScalarParams(qi, scope, field, value, params) {
@@ -995,15 +1262,26 @@ function collectScopedScalarParams(qi, scope, field, value, params) {
995
1262
  if (typeof value === 'object' && !Array.isArray(value) && (0, filters_js_1.isArrayFilter)(value)) {
996
1263
  const colType = pgTypeForColumn(qi, meta, col);
997
1264
  if (colType.startsWith('_')) {
998
- collectArrayFilterParams(qi, value, params);
1265
+ collectArrayFilterParams(qi, value, params, `${qi.q(scope.table)}.${qi.q(col)}`);
999
1266
  return;
1000
1267
  }
1001
1268
  }
1002
1269
  if ((0, filters_js_1.isWhereOperator)(value)) {
1003
- collectOperatorParams(qi, col, value, params, { meta, table: scope.table, prefix: '', rawColumn: col });
1270
+ collectOperatorParams(qi, col, value, params, {
1271
+ meta,
1272
+ table: scope.table,
1273
+ prefix: '',
1274
+ rawColumn: col,
1275
+ pgType: pgTypeForColumn(qi, meta, col),
1276
+ });
1277
+ return;
1278
+ }
1279
+ const pgType = pgTypeForColumn(qi, meta, col);
1280
+ assertBindableEqualityValue(qi, col, value, pgType, scope.table);
1281
+ if (isBareJsonDocument(qi, pgType, value)) {
1282
+ jsonDocumentEquals(qi, '', col, value, collectOnly(params));
1004
1283
  return;
1005
1284
  }
1006
- assertBindableEqualityValue(qi, col, value, pgTypeForColumn(qi, meta, col), scope.table);
1007
1285
  params.push(coerceWhereOperand(qi, meta, col, value));
1008
1286
  }
1009
1287
  /**
@@ -1158,24 +1436,23 @@ depth = 0) {
1158
1436
  return gf ? ` AND ${gf}` : '';
1159
1437
  };
1160
1438
  // "some": EXISTS (SELECT 1 FROM target WHERE correlation AND filter AND gf)
1161
- // A `null` branch is skipped (never reaches buildSubWhereForRelation, which
1162
- // would throw on Object.keys(null)), matching collectRelationFilterParams,
1163
- // which also skips null. Unreachable via normalization today, guarded anyway.
1164
- if (filterObj.some !== undefined && filterObj.some !== null) {
1439
+ // `some` / `none` / `every` hold a where object here: the where walk refuses
1440
+ // any other value (relationFilterValueProblem), so none compiles to nothing.
1441
+ if (filterObj.some !== undefined) {
1165
1442
  const subWhere = filterObj.some;
1166
1443
  const filterClause = buildSubWhereForRelation(qi, targetTable, qt, subWhere, params, depth + 1);
1167
1444
  const filterAnd = filterClause ? ` AND ${filterClause}` : '';
1168
1445
  clauses.push(`EXISTS (SELECT 1 FROM ${fromTarget} WHERE ${correlation}${filterAnd}${gfAnd()})`);
1169
1446
  }
1170
1447
  // "none": NOT EXISTS (SELECT 1 FROM target WHERE correlation AND filter AND gf)
1171
- if (filterObj.none !== undefined && filterObj.none !== null) {
1448
+ if (filterObj.none !== undefined) {
1172
1449
  const subWhere = filterObj.none;
1173
1450
  const filterClause = buildSubWhereForRelation(qi, targetTable, qt, subWhere, params, depth + 1);
1174
1451
  const filterAnd = filterClause ? ` AND ${filterClause}` : '';
1175
1452
  clauses.push(`NOT EXISTS (SELECT 1 FROM ${fromTarget} WHERE ${correlation}${filterAnd}${gfAnd()})`);
1176
1453
  }
1177
1454
  // "every": NOT EXISTS (SELECT 1 FROM target WHERE correlation AND gf AND NOT (filter))
1178
- if (filterObj.every !== undefined && filterObj.every !== null) {
1455
+ if (filterObj.every !== undefined) {
1179
1456
  const subWhere = filterObj.every;
1180
1457
  const filterClause = buildSubWhereForRelation(qi, targetTable, qt, subWhere, params, depth + 1);
1181
1458
  if (filterClause) {
@@ -1246,16 +1523,96 @@ function pgTypeForColumn(_qi, meta, column) {
1246
1523
  * SQL-template cache is unaffected, and it is applied on the cache-hit
1247
1524
  * param-collect path as well as the build path.
1248
1525
  *
1526
+ * On MySQL a number, bigint or boolean compared with a text column binds as
1527
+ * text ({@link textComparedOperand}), and so does each such element of a list.
1528
+ *
1249
1529
  * `timestamptz` and every non-temporal column are returned by identity.
1250
1530
  */
1251
1531
  function coerceWhereOperand(qi, meta, column, value) {
1252
- if (!(value instanceof Date) && !Array.isArray(value))
1532
+ if (value instanceof Date || Array.isArray(value)) {
1533
+ const dbType = pgTypeForColumn(qi, meta, column);
1534
+ const coerced = (0, utils_js_1.coerceTemporalValue)(dbType, value, utcDateTimeRewrite(qi, dbType));
1535
+ return Array.isArray(coerced) ? textComparedList(qi, dbType, coerced) : coerced;
1536
+ }
1537
+ if (!qi.dialect.capabilities.numericTextComparison || typeof value === 'string' || typeof value === 'object') {
1538
+ return value;
1539
+ }
1540
+ return textComparedOperand(qi, pgTypeForColumn(qi, meta, column), value);
1541
+ }
1542
+ /**
1543
+ * Column types MySQL compares with a NUMBER numerically, converting each
1544
+ * stored text to a number: `'0zz' = 0` and `'abc' = 0` are both true, and a
1545
+ * boolean is the number 1 or 0 there. A request body that carries `0` or
1546
+ * `false` where a token string belongs would match the first row whose token
1547
+ * does not start with a digit. Exported so the write path's re-read keys use
1548
+ * this one list rather than a copy.
1549
+ */
1550
+ exports.TEXT_COMPARED_TYPES = new Set([
1551
+ 'char',
1552
+ 'varchar',
1553
+ 'character',
1554
+ 'character varying',
1555
+ 'bpchar',
1556
+ 'nchar',
1557
+ 'nvarchar',
1558
+ 'text',
1559
+ 'tinytext',
1560
+ 'mediumtext',
1561
+ 'longtext',
1562
+ 'binary',
1563
+ 'varbinary',
1564
+ 'tinyblob',
1565
+ 'blob',
1566
+ 'mediumblob',
1567
+ 'longblob',
1568
+ 'uuid',
1569
+ 'citext',
1570
+ ]);
1571
+ /**
1572
+ * A value bound against a column of `columnType`, as MySQL must receive it to
1573
+ * compare it as the value it is: a number or bigint becomes its decimal text
1574
+ * and a boolean `'1'` / `'0'` (what MySQL stores when a boolean is written to
1575
+ * a text column), so the comparison is text with text. Every other engine,
1576
+ * value and column type is returned unchanged. THE rule for every value Turbine
1577
+ * binds against a column on MySQL: the where compiler applies it to each
1578
+ * operand, and the writes apply it to the keys they re-read a row by.
1579
+ */
1580
+ function textComparedOperand(qi, columnType, value) {
1581
+ if (!qi.dialect.capabilities.numericTextComparison)
1582
+ return value;
1583
+ if (typeof value !== 'number' && typeof value !== 'bigint' && typeof value !== 'boolean')
1253
1584
  return value;
1254
- return (0, utils_js_1.coerceTemporalValue)(pgTypeForColumn(qi, meta, column), value,
1255
- // Same PostgreSQL + `utcTimestamps` gate as the write path (see
1256
- // `utcDateTimeWrites` in writes.ts): only the read/write-symmetric engine
1257
- // gets the zone-less rewrite. Time-of-day always rewrites.
1258
- qi.dialect.name === 'postgresql' && qi.utcTimestamps !== false);
1585
+ if (columnType === undefined || !exports.TEXT_COMPARED_TYPES.has(columnType.toLowerCase()))
1586
+ return value;
1587
+ return typeof value === 'boolean' ? (value ? '1' : '0') : String(value);
1588
+ }
1589
+ /** {@link textComparedOperand} for each element of an `in` / `notIn` list; the list itself when none changes. */
1590
+ function textComparedList(qi, columnType, list) {
1591
+ if (!qi.dialect.capabilities.numericTextComparison)
1592
+ return list;
1593
+ let out;
1594
+ for (let i = 0; i < list.length; i++) {
1595
+ const next = textComparedOperand(qi, columnType, list[i]);
1596
+ if (next === list[i])
1597
+ continue;
1598
+ out ??= list.slice();
1599
+ out[i] = next;
1600
+ }
1601
+ return out ?? list;
1602
+ }
1603
+ /**
1604
+ * Whether a `Date` bound against `dbType` gets the zone-less UTC rewrite (the
1605
+ * write path's gate). A `date` column takes the UTC date part on every SQL
1606
+ * engine: the write path stores `YYYY-MM-DD` there, and a full timestamp
1607
+ * compared with that text matches nothing. A zone-less `timestamp` is
1608
+ * rewritten on PostgreSQL only, because MySQL reads a zone-less literal in the
1609
+ * session time zone. `utcTimestamps: false` turns both off. Time-of-day
1610
+ * columns always rewrite.
1611
+ */
1612
+ function utcDateTimeRewrite(qi, dbType) {
1613
+ if (qi.utcTimestamps === false)
1614
+ return false;
1615
+ return qi.dialect.capabilities.utcTimestampRewrite || (0, schema_js_1.localDateTimeKind)(dbType) === 'date';
1259
1616
  }
1260
1617
  /**
1261
1618
  * The Postgres enum type name for a column, when the schema knows one.
@@ -1267,13 +1624,13 @@ function coerceWhereOperand(qi, meta, column, value) {
1267
1624
  * value as text and Postgres refuses the implicit text→enum coercion
1268
1625
  * ("column X is of type Y but expression is of type text").
1269
1626
  *
1270
- * Postgres-only by construction: gated on the active dialect being
1271
- * `postgresql` AND on `schema.enums` having entries (only PG introspection
1272
- * produces them, `defineSchema` and the other engines leave it empty), so
1273
- * SQLite/MySQL/MSSQL/PowDB output is byte-identical.
1627
+ * Postgres-only by construction: gated on the engine having named enum types
1628
+ * (`namedEnumTypes`) AND on `schema.enums` having entries (only PG
1629
+ * introspection produces them, `defineSchema` and the other engines leave it
1630
+ * empty), so SQLite/MySQL/MSSQL/PowDB output is byte-identical.
1274
1631
  */
1275
1632
  function enumTypeForColumn(qi, column) {
1276
- if (qi.dialect.name !== 'postgresql')
1633
+ if (!qi.dialect.capabilities.namedEnumTypes)
1277
1634
  return null;
1278
1635
  const enums = qi.schema.enums;
1279
1636
  if (!enums)
@@ -1302,25 +1659,14 @@ function enumCastSuffix(qi, column) {
1302
1659
  }
1303
1660
  /**
1304
1661
  * Equality-fallthrough guard shared by every SQL-build path AND every
1305
- * cache-hit param-collect path. A plain object literal that matched no known
1306
- * filter shape on a non-JSON column is almost always a misspelled operator
1307
- * (`startWith` for `startsWith`); binding it as `col = $1` silently returns
1308
- * wrong rows. Class instances (Buffer for bytea, Decimal wrappers, ...) are
1309
- * legitimate bind values and pass through, as do objects on json/jsonb
1310
- * columns (object equality).
1311
- */
1312
- function assertBindableEqualityValue(qi, rawColumn, value, columnPgType, table) {
1313
- if (!(0, filters_js_1.isUnmatchedPlainObject)(value))
1314
- return;
1315
- if (isJsonColumnType(qi, columnPgType))
1316
- return;
1317
- const badKeys = Object.keys(value);
1318
- throw new errors_js_1.ValidationError(badKeys.length === 0
1319
- ? `Empty filter object on "${rawColumn}" for table "${table}". ` +
1320
- `Provide a value or an operator like { gt: 1 }.`
1321
- : `Unknown operator${badKeys.length > 1 ? 's' : ''} ` +
1322
- `${badKeys.map((k) => `"${k}"`).join(', ')} on "${rawColumn}" for table "${table}". ` +
1323
- `Supported operators: ${[...utils_js_1.OPERATOR_KEYS].join(', ')}.`);
1662
+ * cache-hit param-collect path: a bare value must fit its column
1663
+ * ({@link assertEqualityOperand}). A plain object literal that matched no
1664
+ * known filter shape on a non-JSON column is almost always a misspelled
1665
+ * operator (`startWith` for `startsWith`), and binding it as `col = $1` would
1666
+ * silently return wrong rows. Objects on json/jsonb columns are JSON values.
1667
+ */
1668
+ function assertBindableEqualityValue(_qi, rawColumn, value, columnPgType, table) {
1669
+ (0, filters_js_1.assertEqualityOperand)(value, { column: rawColumn, table, kind: (0, filters_js_1.operandColumnKind)(columnPgType) });
1324
1670
  }
1325
1671
  /**
1326
1672
  * Build the user-supplied `where` filter of a relation `with` clause against
@@ -1339,7 +1685,7 @@ function buildAliasWhere(qi, targetTable, targetMeta, alias, where, params) {
1339
1685
  function collectAliasWhereParams(qi, targetTable, targetMeta, where, params) {
1340
1686
  // The alias identifier is irrelevant to param collection (it only shapes SQL
1341
1687
  // text), so reuse the relation scope's host binding for `targetMeta`.
1342
- collectScopedWhereParams(qi, aliasWhereScope(qi, targetTable, targetMeta, ''), where, params);
1688
+ return collectScopedWhereParams(qi, aliasWhereScope(qi, targetTable, targetMeta, ''), where, params);
1343
1689
  }
1344
1690
  /**
1345
1691
  * Value-invariant, shape-aware fingerprint for a relation `with` clause's
@@ -1388,11 +1734,20 @@ function columnRefSql(qi, ref, ctx, mode) {
1388
1734
  * Each operator key becomes its own clause, all ANDed together.
1389
1735
  *
1390
1736
  * `equals`/`not`/`gt`/`gte`/`lt`/`lte` also accept a {@link ColumnRef}
1391
- * (`{ col: 'otherField' }`) which compiles to a column-to-column comparison
1737
+ * (`col('otherField')`) which compiles to a column-to-column comparison
1392
1738
  * against `refCtx`: no param bound, so `collectOperatorParams` mirrors by
1393
1739
  * pushing nothing and the referenced name lives in the fingerprint.
1740
+ *
1741
+ * `not` also accepts an operator object, compiled as `NOT (<inner clauses>)`
1742
+ * (see {@link WhereOperator.not}).
1394
1743
  */
1395
1744
  function buildOperatorClauses(qi, column, op, params, refCtx) {
1745
+ const operand = operandContext(qi, column, refCtx);
1746
+ (0, filters_js_1.assertOperatorOperands)(op, operand);
1747
+ return buildOperatorClausesChecked(qi, column, op, params, refCtx, operand);
1748
+ }
1749
+ /** {@link buildOperatorClauses} after validation; recursive for a `not` operator object. */
1750
+ function buildOperatorClausesChecked(qi, column, op, params, refCtx, operand) {
1396
1751
  const clauses = [];
1397
1752
  // Temporal bind rewrite, identical to `collectOperatorParams`. Value-only, so
1398
1753
  // the emitted SQL (and therefore the template cache) is untouched.
@@ -1406,7 +1761,6 @@ function buildOperatorClauses(qi, column, op, params, refCtx) {
1406
1761
  clauses.push(`${column} = ${columnRefSql(qi, op.equals, refCtx, op.mode)}`);
1407
1762
  }
1408
1763
  else {
1409
- (0, filters_js_1.assertBindableEqualsOperand)(op.equals, column);
1410
1764
  if (insensitive)
1411
1765
  assertInsensitiveOperand(op.equals, 'equals', column);
1412
1766
  params.push(cv(op.equals));
@@ -1450,12 +1804,25 @@ function buildOperatorClauses(qi, column, op, params, refCtx) {
1450
1804
  }
1451
1805
  }
1452
1806
  if (op.not !== undefined) {
1807
+ const negatedDocument = op.not === null || (0, filters_js_1.isColumnRef)(op.not) ? NO_JSON_NEGATION : jsonNegatedDocument(qi, op.not, operand);
1453
1808
  if (op.not === null) {
1454
1809
  clauses.push(`${column} IS NOT NULL`);
1455
1810
  }
1456
1811
  else if ((0, filters_js_1.isColumnRef)(op.not)) {
1457
1812
  clauses.push(`${column} != ${columnRefSql(qi, op.not, refCtx, op.mode)}`);
1458
1813
  }
1814
+ else if (negatedDocument !== NO_JSON_NEGATION) {
1815
+ const equality = jsonDocumentEquals(qi, column, column, negatedDocument, (v) => {
1816
+ params.push(v);
1817
+ return qi.p(params.length);
1818
+ });
1819
+ clauses.push(`NOT (${equality})`);
1820
+ }
1821
+ else if (isNegatedOperatorObject(op.not, operand)) {
1822
+ const inner = buildOperatorClausesChecked(qi, column, notOperand(op), params, refCtx, operand);
1823
+ if (inner.length > 0)
1824
+ clauses.push(`NOT (${inner.join(' AND ')})`);
1825
+ }
1459
1826
  else {
1460
1827
  if (insensitive)
1461
1828
  assertInsensitiveOperand(op.not, 'not', column);
@@ -1512,9 +1879,8 @@ function requireFullTextSearch(qi) {
1512
1879
  'Use `contains` (LIKE) on this engine, or run the query on PostgreSQL.');
1513
1880
  }
1514
1881
  /**
1515
- * Gate the pathless JSON containment filters (`contains`, and the `equals`
1516
- * spelling that compiles to the same expression) on
1517
- * {@link Dialect.supportsJsonContains}.
1882
+ * Gate the pathless JSON filters, `contains` (containment) and `equals`
1883
+ * (whole-document equality), on {@link Dialect.supportsJsonContains}.
1518
1884
  *
1519
1885
  * The engine this exists for is SQLite, whose emulation compares a decoded
1520
1886
  * `json_each.value` against a param bound as JSON TEXT and therefore matched
@@ -1533,11 +1899,12 @@ function requireFullTextSearch(qi) {
1533
1899
  function requireJsonContains(qi, clause) {
1534
1900
  if (qi.dialect.supportsJsonContains)
1535
1901
  return;
1536
- throw new errors_js_1.UnsupportedFeatureError(`JSON containment (\`${clause}\` without \`path\`)`, qi.dialect.name, `This engine has no equivalent of PostgreSQL's \`@>\`, and the emulation Turbine used for ` +
1537
- `\`${clause}\` matched no rows for any operand rather than failing, so it is refused instead. ` +
1902
+ const what = clause === 'equals' ? 'way to compare two whole JSON documents' : "JSON containment (PostgreSQL's `@>`)";
1903
+ throw new errors_js_1.UnsupportedFeatureError(clause === 'equals'
1904
+ ? 'JSON document equality (`equals` without `path`)'
1905
+ : 'JSON containment (`contains` without `path`)', qi.dialect.name, `This engine has no ${what}, so the filter is refused rather than emulated. ` +
1538
1906
  'Filter on the specific value instead: `{ path: [...], equals: ... }` compiles to a real JSON ' +
1539
- 'extraction (`json_extract`) rather than an emulation. For structural containment, run the ' +
1540
- 'query on PostgreSQL or MySQL.');
1907
+ 'extraction (`json_extract`). To compare or match whole documents, run the query on PostgreSQL or MySQL.');
1541
1908
  }
1542
1909
  /**
1543
1910
  * Gate the array filter operators (`has` / `hasEvery` / `hasSome` / `isEmpty`)
@@ -1611,15 +1978,7 @@ function pushVectorParam(qi, field, _rawColumn, to, params) {
1611
1978
  * ambiguous and was never valid in Prisma either).
1612
1979
  */
1613
1980
  function normalizeRelationFilter(_qi, relDef, filterObj) {
1614
- if ((relDef.type === 'belongsTo' || relDef.type === 'hasOne') &&
1615
- !('some' in filterObj) &&
1616
- !('every' in filterObj) &&
1617
- !('none' in filterObj) &&
1618
- !('is' in filterObj) &&
1619
- !('isNot' in filterObj)) {
1620
- return { is: filterObj };
1621
- }
1622
- return filterObj;
1981
+ return (0, where_compile_js_1.normalizeToOneRelationFilter)(relDef, filterObj);
1623
1982
  }
1624
1983
  /**
1625
1984
  * Case-insensitive json/jsonb column-type check. Postgres reports lowercase
@@ -1667,7 +2026,11 @@ function getArrayElementType(_qi, pgType) {
1667
2026
  */
1668
2027
  /**
1669
2028
  * One LIKE comparison, honoring `mode: 'insensitive'` through the dialect and
1670
- * always carrying the `ESCAPE '\'` clause that pairs with {@link escapeLike}.
2029
+ * always carrying the `ESCAPE` clause that pairs with {@link likeOperand}: the
2030
+ * dialect's `likeEscapeCharacter`, else `\` (the character {@link escapeLike}
2031
+ * escapes with). MySQL names `!`, because `'\'` is a syntax error there unless
2032
+ * the session runs with `NO_BACKSLASH_ESCAPES`, which a caller-supplied pool
2033
+ * need not.
1671
2034
  *
1672
2035
  * Shared by the scalar `contains` / `startsWith` / `endsWith` operators and by
1673
2036
  * the JSON substring operators, so the two can never drift into escaping their
@@ -1675,7 +2038,7 @@ function getArrayElementType(_qi, pgType) {
1675
2038
  */
1676
2039
  function buildLikeClause(qi, column, paramRef, insensitive) {
1677
2040
  const base = insensitive ? qi.dialect.buildInsensitiveLike(column, paramRef) : `${column} LIKE ${paramRef}`;
1678
- return `${base} ESCAPE '\\'`;
2041
+ return `${base} ESCAPE '${qi.dialect.likeEscapeCharacter ?? '\\'}'`;
1679
2042
  }
1680
2043
  /**
1681
2044
  * One equality-family comparison (`=` / `!=`) honoring `mode: 'insensitive'`.
@@ -1725,11 +2088,12 @@ function insensitiveInClause(qi, column, paramRef, negated, operator) {
1725
2088
  * on that engine.
1726
2089
  *
1727
2090
  * Folding the elements in SQL instead is not reachable from here for the other
1728
- * dialects: each unpacks a bound IN list in a subquery of its OWN shape
1729
- * (`json_each` / `JSON_TABLE` / `OPENJSON`) whose projection is written by the
1730
- * dialect, not by this module, and the length-independence the SQL-template
1731
- * cache depends on rules out the other portable form, one placeholder per
1732
- * element. Restoring the operator on those engines means a dialect hook that
2091
+ * dialects: each unpacks a bound IN list in its OWN way (`json_each` on SQLite,
2092
+ * `OPENJSON` on SQL Server, one placeholder per element expanded by the MySQL
2093
+ * pool shim after the statement leaves the builder), that shape is the
2094
+ * dialect's and not this module's, and the length-independence the
2095
+ * SQL-template cache depends on rules out emitting one placeholder per element
2096
+ * here. Restoring the operator on those engines means a dialect hook that
1733
2097
  * projects the folded element, one correct implementation per engine. Until
1734
2098
  * then a typed refusal is the honest outcome. `equals`, `not` and the LIKE
1735
2099
  * operators are unaffected: they fold a single operand the engine can reach.
@@ -1738,7 +2102,7 @@ function insensitiveInClause(qi, column, paramRef, negated, operator) {
1738
2102
  * every other gate in this module.
1739
2103
  */
1740
2104
  function requireInsensitiveList(qi, column, operator) {
1741
- if (qi.dialect.name === 'postgresql')
2105
+ if (qi.dialect.capabilities.insensitiveInList)
1742
2106
  return;
1743
2107
  throw new errors_js_1.UnsupportedFeatureError(`mode: 'insensitive' on \`${operator}\` (${column})`, qi.dialect.name, "Case-folding a bound list needs the engine to lower every element in SQL, which this dialect's " +
1744
2108
  'IN-list form cannot express. Lowering them in JavaScript instead would fold the list and the ' +
@@ -1762,54 +2126,6 @@ function assertInsensitiveOperand(value, operator, column) {
1762
2126
  `(received ${Array.isArray(value) ? 'a list with a non-string element' : typeof value}). ` +
1763
2127
  `Case folding applies to text; drop the mode or compare a string.`);
1764
2128
  }
1765
- /**
1766
- * Refuse a {@link JsonFilter} carrying a key that is not a JSON operator, and
1767
- * refuse a filter that selects a `path` but never compares it.
1768
- *
1769
- * Both shapes used to compile to NOTHING. `buildJsonFilterClauses` only ever
1770
- * emitted a clause for a key it recognized, so `{ path: ['title'],
1771
- * string_contains: 'x' }`, the Prisma spelling, and an easy typo besides -
1772
- * produced an empty clause list, the predicate vanished, and the query
1773
- * returned EVERY row. Inside an `AND` it silently dropped that conjunct, so a
1774
- * tenant scope written this way widened to the whole table. The equivalent
1775
- * typo on a scalar column has always thrown; this closes the inconsistency.
1776
- *
1777
- * Called from both the SQL-build path and the cache-hit param-collect path, so
1778
- * a warmed SQL cache cannot skip the check.
1779
- */
1780
- function assertJsonFilterKeys(filter, column) {
1781
- const obj = filter;
1782
- const present = Object.keys(obj).filter((k) => obj[k] !== undefined);
1783
- for (const key of present) {
1784
- if (filters_js_1.JSON_FILTER_KEYS.has(key))
1785
- continue;
1786
- const suggestion = JSON_PRISMA_SPELLINGS[key];
1787
- throw new errors_js_1.ValidationError(`Unknown JSON filter operator "${key}" on ${column}.` +
1788
- (suggestion ? ` Did you mean \`${suggestion}\`?` : '') +
1789
- ` Supported operators: ${[...filters_js_1.JSON_FILTER_KEYS].sort().join(', ')}.`);
1790
- }
1791
- // `mode` and `path` are modifiers, not comparisons: a filter made only of
1792
- // those compares nothing, which is exactly the silent no-op shape above.
1793
- if (present.length > 0 && present.every((k) => k === 'path' || k === 'mode')) {
1794
- throw new errors_js_1.ValidationError(`JSON filter on ${column} selects a \`path\` but has no comparison. ` +
1795
- `Add one of: ${[...filters_js_1.JSON_FILTER_KEYS]
1796
- .filter((k) => k !== 'path' && k !== 'mode')
1797
- .sort()
1798
- .join(', ')}.`);
1799
- }
1800
- }
1801
- /**
1802
- * The Prisma spelling of each JSON operator, so a migrator who writes the
1803
- * name they already know gets told the turbine one instead of a bare list.
1804
- */
1805
- const JSON_PRISMA_SPELLINGS = {
1806
- string_contains: 'stringContains',
1807
- string_starts_with: 'stringStartsWith',
1808
- string_ends_with: 'stringEndsWith',
1809
- array_contains: 'contains',
1810
- startsWith: 'stringStartsWith',
1811
- endsWith: 'stringEndsWith',
1812
- };
1813
2129
  /**
1814
2130
  * Validate and enumerate the substring comparisons on a JSON filter, in the
1815
2131
  * fixed {@link JSON_STRING_OPERATORS} order. Shared by the build and collect
@@ -1856,6 +2172,136 @@ function jsonRangeEntries(_qi, filter, column) {
1856
2172
  }
1857
2173
  return entries;
1858
2174
  }
2175
+ /**
2176
+ * The `{ path, equals }` operand after the checks every engine shares: a
2177
+ * function, symbol or column reference is refused, a non-finite number is
2178
+ * refused (JSON.stringify would turn it into `null`, which matches JSON null),
2179
+ * and a Date becomes the ISO string JSON stores it as.
2180
+ */
2181
+ function jsonPathEqualsValue(value, column) {
2182
+ if (typeof value === 'function' || typeof value === 'symbol' || (0, filters_js_1.isColumnRef)(value)) {
2183
+ throw new errors_js_1.ValidationError(`JSON path 'equals' on ${column} takes a JSON value (string, number, boolean, null, array or object), ` +
2184
+ `received ${(0, filters_js_1.isColumnRef)(value) ? 'a column reference' : `a ${typeof value}`}.`);
2185
+ }
2186
+ const scalar = value instanceof Date ? value.toISOString() : value;
2187
+ if (typeof scalar === 'number' && !Number.isFinite(scalar)) {
2188
+ throw new errors_js_1.ValidationError(`JSON path 'equals' on ${column} takes a finite number: JSON has no form for ${scalar}.`);
2189
+ }
2190
+ return scalar;
2191
+ }
2192
+ /** The engine's typed `{ path, equals }` hook; the jsonb form lives here, so an engine using it never takes one. */
2193
+ function jsonPathEqualsHook(qi) {
2194
+ return qi.dialect.capabilities.jsonDocumentEquality === 'jsonb' ? undefined : qi.dialect.buildJsonPathEquals;
2195
+ }
2196
+ /** The hook input, identical on the build and collect passes but for `column`, `pathRef` and `bind`. */
2197
+ function jsonPathEqualsInput(qi, filter, label, column, pathRef, bind) {
2198
+ return {
2199
+ column,
2200
+ pathRef,
2201
+ path: filter.path,
2202
+ value: jsonPathEqualsValue(filter.equals, label),
2203
+ label,
2204
+ bind,
2205
+ encodePath: (path) => jsonPathParam(qi, path),
2206
+ };
2207
+ }
2208
+ /**
2209
+ * A whole-document JSON `equals` (no `path`): the column equals the operand as
2210
+ * a JSON value, on the build and collect passes alike (`bind` pushes; on the
2211
+ * collect pass the returned SQL is discarded). PostgreSQL compares jsonb, which
2212
+ * ignores object key order and whitespace; the cast makes a `json` column
2213
+ * comparable too (it has no `=` operator). MySQL compares through its typed
2214
+ * hook; an engine without one refuses (E017) through
2215
+ * {@link requireJsonContains}.
2216
+ */
2217
+ function jsonDocumentEquals(qi, column, label, value, bind) {
2218
+ const v = jsonPathEqualsValue(value, label);
2219
+ if (qi.dialect.capabilities.jsonDocumentEquality === 'jsonb') {
2220
+ return `${column}::jsonb = ${bind(typeof v === 'bigint' ? v.toString() : JSON.stringify(v))}::jsonb`;
2221
+ }
2222
+ const hook = qi.dialect.buildJsonPathEquals;
2223
+ if (hook) {
2224
+ return hook.call(qi.dialect, {
2225
+ column,
2226
+ pathRef: undefined,
2227
+ path: [],
2228
+ value: v,
2229
+ label,
2230
+ bind,
2231
+ encodePath: (path) => jsonPathParam(qi, path),
2232
+ });
2233
+ }
2234
+ requireJsonContains(qi, 'equals');
2235
+ throw new errors_js_1.UnsupportedFeatureError(`JSON \`equals\` on the whole document (${label})`, qi.dialect.name, 'This engine has no exact comparison of JSON documents. Compare a value with `{ path: [...], equals }`.');
2236
+ }
2237
+ /** A bind callback for the collect passes: pushes the value, emits no SQL. */
2238
+ function collectOnly(params) {
2239
+ return (value) => {
2240
+ params.push(value);
2241
+ return '';
2242
+ };
2243
+ }
2244
+ /**
2245
+ * The operand of a `not` on a JSON column that negates a whole-document
2246
+ * equality, or `NO_JSON_NEGATION` when the `not` keeps its bound-value form.
2247
+ * `not: { equals: X }` is the negation of `equals: X` everywhere it compiles
2248
+ * (an engine without an exact comparison refuses it rather than comparing a
2249
+ * document `{"equals": X}`). A plain `not: X` means the same thing on the
2250
+ * engines that compare whole documents exactly (PostgreSQL, MySQL). On SQLite
2251
+ * and SQL Server a document (object or array) is refused the same way, since
2252
+ * comparing it as text lets key order decide; a scalar keeps comparing as the
2253
+ * bound value, as before.
2254
+ */
2255
+ function jsonNegatedDocument(qi, not, operand) {
2256
+ if (operand.kind !== 'json' || not === null || not === undefined || (0, filters_js_1.isColumnRef)(not))
2257
+ return NO_JSON_NEGATION;
2258
+ if ((0, filters_js_1.isWhereOperator)(not)) {
2259
+ const keys = Object.keys(not).filter((k) => not[k] !== undefined);
2260
+ if (keys.length === 1 && keys[0] === 'equals')
2261
+ return not.equals;
2262
+ }
2263
+ if (comparesJsonDocuments(qi))
2264
+ return not;
2265
+ return (0, filters_js_1.isPlainRecord)(not) || Array.isArray(not) ? not : NO_JSON_NEGATION;
2266
+ }
2267
+ const NO_JSON_NEGATION = Symbol('no JSON negation');
2268
+ /**
2269
+ * The bound operand of `{ path, equals }`, shared by the build and collect
2270
+ * paths so both bind (and refuse) identically.
2271
+ *
2272
+ * PostgreSQL compares JSON to JSON, so the operand is its JSON text: `5`,
2273
+ * `"5"`, `null`, `[1,2]` and `{"a":1}` each match only that JSON value at the
2274
+ * path (numbers compare numerically, so `5` also matches `5.0`). A Date is the
2275
+ * ISO string JSON would store, and a bigint is a JSON number.
2276
+ *
2277
+ * MySQL and SQL Server compare typed through `Dialect.buildJsonPathEquals`
2278
+ * and never reach this function. SQLite compares the value its path
2279
+ * extraction returns, so only a value with a text form can match: a string,
2280
+ * number, boolean, bigint or Date.
2281
+ * There a number and a numeric string match the same rows, except on SQLite,
2282
+ * whose extraction returns typed values: a number binds as a number there, and
2283
+ * a boolean is refused because SQLite reads `true` and `1` alike. `null`,
2284
+ * objects and arrays matched no row on those engines and are refused (E017).
2285
+ */
2286
+ function jsonPathEqualsOperand(qi, value, column) {
2287
+ const scalar = jsonPathEqualsValue(value, column);
2288
+ if (qi.dialect.capabilities.jsonDocumentEquality === 'jsonb') {
2289
+ return typeof scalar === 'bigint' ? scalar.toString() : JSON.stringify(scalar);
2290
+ }
2291
+ if (scalar === null || typeof scalar === 'object') {
2292
+ throw new errors_js_1.UnsupportedFeatureError(`JSON path \`equals\` with ${scalar === null ? 'null' : Array.isArray(scalar) ? 'an array' : 'an object'} (${column})`, qi.dialect.name, 'This engine compares the text extracted at the path, which has no form for null, arrays or objects, so ' +
2293
+ 'the comparison would match no row. Compare a scalar at a deeper path, or run the query on PostgreSQL.');
2294
+ }
2295
+ if (qi.dialect.capabilities.jsonExtractTyped) {
2296
+ if (typeof scalar === 'number')
2297
+ return scalar;
2298
+ if (typeof scalar === 'boolean') {
2299
+ throw new errors_js_1.UnsupportedFeatureError(`JSON path \`equals\` with a boolean (${column})`, qi.dialect.name, 'SQLite extracts JSON true and false as the integers 1 and 0, so a boolean comparison would also match ' +
2300
+ 'those numbers. Compare with 1 / 0 if the documents hold only booleans there.');
2301
+ }
2302
+ }
2303
+ return String(scalar);
2304
+ }
1859
2305
  /**
1860
2306
  * Build SQL clauses for JSONB filter operators on a column.
1861
2307
  * Supports: path, equals, contains, hasKey, gt, gte, lt, lte.
@@ -1865,33 +2311,49 @@ function jsonRangeEntries(_qi, filter, column) {
1865
2311
  * stays byte-identical to {@link collectJsonFilterParams}.
1866
2312
  */
1867
2313
  function buildJsonFilterClauses(qi, column, filter, params) {
1868
- assertJsonFilterKeys(filter, column);
2314
+ (0, filters_js_1.assertJsonFilterKeys)(filter, column);
1869
2315
  const clauses = [];
1870
2316
  // Lazily bind the path once; reuse the same $N in every extraction clause.
1871
2317
  let pathParamIdx = null;
1872
- const pathExtract = () => {
2318
+ const pathRef = () => {
1873
2319
  if (pathParamIdx === null) {
1874
2320
  // Only reached when a path-requiring clause validated filter.path.
1875
2321
  params.push(jsonPathParam(qi, filter.path, filter.path));
1876
2322
  pathParamIdx = params.length;
1877
2323
  }
1878
- return qi.dialect.buildJsonPathExtract(column, qi.p(pathParamIdx));
2324
+ return qi.p(pathParamIdx);
1879
2325
  };
2326
+ const pathExtract = () => qi.dialect.buildJsonPathExtract(column, pathRef());
1880
2327
  if (filter.path !== undefined && filter.equals !== undefined) {
1881
- // Path access + equals: column #>> $N::text[] = $M
1882
- const extract = pathExtract();
1883
- params.push(String(filter.equals));
1884
- clauses.push(`${extract} = ${qi.p(params.length)}`);
2328
+ // Path access + equals. PostgreSQL compares the JSON VALUE at the path,
2329
+ // typed: `(column #> $N::text[])::jsonb = $M::jsonb`, so 5 and "5" differ,
2330
+ // and null, arrays and objects compare as JSON. MySQL and SQL Server bring
2331
+ // their own typed form (Dialect.buildJsonPathEquals); SQLite compares the
2332
+ // extracted value (see jsonPathEqualsOperand).
2333
+ const ref = pathRef();
2334
+ const hook = jsonPathEqualsHook(qi);
2335
+ if (hook) {
2336
+ // The engine's typed comparison (MySQL, SQL Server).
2337
+ clauses.push(hook.call(qi.dialect, jsonPathEqualsInput(qi, filter, column, column, ref, (v) => {
2338
+ params.push(v);
2339
+ return qi.p(params.length);
2340
+ })));
2341
+ }
2342
+ else {
2343
+ const value = jsonPathEqualsOperand(qi, filter.equals, column);
2344
+ params.push(value);
2345
+ clauses.push(qi.dialect.capabilities.jsonDocumentEquality === 'jsonb'
2346
+ ? `(${column} #> ${ref}::text[])::jsonb = ${qi.p(params.length)}::jsonb`
2347
+ : `${qi.dialect.buildJsonPathExtract(column, ref)} = ${qi.p(params.length)}`);
2348
+ }
1885
2349
  }
1886
2350
  else if (filter.equals !== undefined) {
1887
- // Containment equality: column @> $N::jsonb. A pathless `equals` compiles
1888
- // to the SAME containment expression `contains` does, so it inherits the
1889
- // same engine limitation and is refused under its own clause name (the two
1890
- // spellings are far enough apart that naming the wrong one would send the
1891
- // reader to the wrong line).
1892
- requireJsonContains(qi, 'equals');
1893
- params.push(JSON.stringify(filter.equals));
1894
- clauses.push(qi.dialect.buildJsonContains(column, qi.p(params.length)));
2351
+ // Whole-document equality, NOT containment (`@>` matched every document
2352
+ // holding the operand's keys).
2353
+ clauses.push(jsonDocumentEquals(qi, column, column, filter.equals, (v) => {
2354
+ params.push(v);
2355
+ return qi.p(params.length);
2356
+ }));
1895
2357
  }
1896
2358
  if (filter.contains !== undefined) {
1897
2359
  // Containment: column @> $N::jsonb
@@ -1932,12 +2394,19 @@ function buildJsonFilterClauses(qi, column, filter, params) {
1932
2394
  * params) and fail at runtime with the engine's bad-JSON-path error. The
1933
2395
  * encoded path stays a bound parameter, never spliced into SQL text, so
1934
2396
  * the build/collect param mirrors stay in lockstep and injection-safe.
2397
+ *
2398
+ * A key segment is written as a JSON string literal (`JSON.stringify`), the
2399
+ * form the engines' JSONPath grammars read a quoted key in: a backslash, a
2400
+ * quote and a control character are each escaped. Escaping only the quote
2401
+ * read a key holding a backslash as an escape sequence, so the key `a\b` was
2402
+ * looked up as a different key and matched nothing, a key ending in a
2403
+ * backslash was a bad path, and MySQL refused a key holding a newline.
1935
2404
  */
1936
2405
  function jsonPathParam(qi, path, nativeForm) {
1937
2406
  if (qi.dialect.jsonPathSupport === 'native')
1938
2407
  return nativeForm ?? path.map(String);
1939
2408
  return `$${path
1940
- .map((seg) => typeof seg === 'number' || /^\d+$/.test(String(seg)) ? `[${seg}]` : `."${String(seg).replace(/"/g, '\\"')}"`)
2409
+ .map((seg) => typeof seg === 'number' || /^\d+$/.test(String(seg)) ? `[${seg}]` : `.${JSON.stringify(String(seg))}`)
1941
2410
  .join('')}`;
1942
2411
  }
1943
2412
  /**
@@ -1948,7 +2417,7 @@ function jsonPathParam(qi, path, nativeForm) {
1948
2417
  * SQL Server have no `::` operator) as a float cast.
1949
2418
  */
1950
2419
  function castJsonNumeric(qi, extract) {
1951
- if (qi.dialect.name === 'postgresql')
2420
+ if (qi.dialect.capabilities.jsonExactNumericCast)
1952
2421
  return `(${extract})::numeric`;
1953
2422
  return qi.dialect.castAggregate ? qi.dialect.castAggregate(`(${extract})`, 'float') : `(${extract})::numeric`;
1954
2423
  }
@@ -1958,6 +2427,7 @@ function castJsonNumeric(qi, extract) {
1958
2427
  */
1959
2428
  function buildArrayFilterClauses(qi, column, filter, params, pgType) {
1960
2429
  requireArrayColumns(qi);
2430
+ (0, filters_js_1.assertArrayFilter)(filter, column);
1961
2431
  const clauses = [];
1962
2432
  const elementType = getArrayElementType(qi, pgType);
1963
2433
  if (filter.has !== undefined) {