turbine-orm 0.65.0 → 0.66.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 (142) hide show
  1. package/README.md +34 -32
  2. package/dist/adapters/cockroachdb.js +21 -3
  3. package/dist/adapters/index.d.ts +15 -0
  4. package/dist/adapters/yugabytedb.js +20 -3
  5. package/dist/cjs/adapters/cockroachdb.js +21 -3
  6. package/dist/cjs/adapters/index.d.ts +15 -0
  7. package/dist/cjs/adapters/yugabytedb.js +20 -3
  8. package/dist/cjs/cli/destructive.d.ts +18 -4
  9. package/dist/cjs/cli/destructive.js +230 -122
  10. package/dist/cjs/cli/index.d.ts +21 -4
  11. package/dist/cjs/cli/index.js +119 -22
  12. package/dist/cjs/cli/mcp.d.ts +28 -8
  13. package/dist/cjs/cli/mcp.js +170 -127
  14. package/dist/cjs/cli/migrate.d.ts +134 -13
  15. package/dist/cjs/cli/migrate.js +349 -241
  16. package/dist/cjs/cli/pii-predicate-guard.d.ts +112 -0
  17. package/dist/cjs/cli/pii-predicate-guard.js +390 -0
  18. package/dist/cjs/cli/prisma-resolve.js +75 -4
  19. package/dist/cjs/cli/prisma-schema.d.ts +17 -1
  20. package/dist/cjs/cli/prisma-schema.js +83 -17
  21. package/dist/cjs/cli/sql-statements.d.ts +125 -0
  22. package/dist/cjs/cli/sql-statements.js +378 -0
  23. package/dist/cjs/cli/studio.js +49 -118
  24. package/dist/cjs/cli/ui.d.ts +1 -1
  25. package/dist/cjs/client.d.ts +43 -0
  26. package/dist/cjs/client.js +125 -6
  27. package/dist/cjs/dialect.d.ts +123 -0
  28. package/dist/cjs/dialect.js +33 -0
  29. package/dist/cjs/errors.d.ts +74 -1
  30. package/dist/cjs/errors.js +239 -25
  31. package/dist/cjs/index-advisor.d.ts +33 -1
  32. package/dist/cjs/index-advisor.js +32 -1
  33. package/dist/cjs/introspect.d.ts +48 -0
  34. package/dist/cjs/introspect.js +222 -91
  35. package/dist/cjs/mssql.js +43 -1
  36. package/dist/cjs/mysql.d.ts +5 -2
  37. package/dist/cjs/mysql.js +202 -17
  38. package/dist/cjs/nested-write.js +6 -1
  39. package/dist/cjs/pipeline-submittable.js +17 -3
  40. package/dist/cjs/pipeline.js +75 -9
  41. package/dist/cjs/powdb.d.ts +23 -0
  42. package/dist/cjs/powdb.js +33 -1
  43. package/dist/cjs/powql.d.ts +61 -9
  44. package/dist/cjs/powql.js +186 -49
  45. package/dist/cjs/prisma-compat.js +160 -41
  46. package/dist/cjs/query/aggregates.d.ts +1 -1
  47. package/dist/cjs/query/aggregates.js +80 -18
  48. package/dist/cjs/query/batched-loader.d.ts +10 -0
  49. package/dist/cjs/query/batched-loader.js +268 -7
  50. package/dist/cjs/query/builder.d.ts +73 -0
  51. package/dist/cjs/query/builder.js +225 -28
  52. package/dist/cjs/query/filters.d.ts +162 -0
  53. package/dist/cjs/query/filters.js +250 -1
  54. package/dist/cjs/query/relations.d.ts +10 -10
  55. package/dist/cjs/query/relations.js +93 -12
  56. package/dist/cjs/query/types.d.ts +14 -1
  57. package/dist/cjs/query/utils.d.ts +146 -2
  58. package/dist/cjs/query/utils.js +210 -4
  59. package/dist/cjs/query/warn-registry.d.ts +10 -0
  60. package/dist/cjs/query/warn-registry.js +10 -0
  61. package/dist/cjs/query/where-compile.d.ts +30 -0
  62. package/dist/cjs/query/where-compile.js +41 -0
  63. package/dist/cjs/query/where.d.ts +128 -13
  64. package/dist/cjs/query/where.js +215 -77
  65. package/dist/cjs/query/writes.d.ts +1 -1
  66. package/dist/cjs/query/writes.js +39 -15
  67. package/dist/cjs/schema-builder.d.ts +2 -1
  68. package/dist/cjs/schema-sql.d.ts +94 -4
  69. package/dist/cjs/schema-sql.js +506 -30
  70. package/dist/cjs/schema.d.ts +3 -1
  71. package/dist/cjs/sqlite.d.ts +6 -0
  72. package/dist/cjs/sqlite.js +151 -10
  73. package/dist/cjs/typed-sql.d.ts +29 -1
  74. package/dist/cjs/typed-sql.js +30 -12
  75. package/dist/cli/destructive.d.ts +18 -4
  76. package/dist/cli/destructive.js +229 -121
  77. package/dist/cli/index.d.ts +21 -4
  78. package/dist/cli/index.js +120 -24
  79. package/dist/cli/mcp.d.ts +28 -8
  80. package/dist/cli/mcp.js +172 -129
  81. package/dist/cli/migrate.d.ts +134 -13
  82. package/dist/cli/migrate.js +347 -238
  83. package/dist/cli/pii-predicate-guard.d.ts +112 -0
  84. package/dist/cli/pii-predicate-guard.js +386 -0
  85. package/dist/cli/prisma-resolve.js +75 -4
  86. package/dist/cli/prisma-schema.d.ts +17 -1
  87. package/dist/cli/prisma-schema.js +83 -17
  88. package/dist/cli/sql-statements.d.ts +125 -0
  89. package/dist/cli/sql-statements.js +373 -0
  90. package/dist/cli/studio.js +49 -118
  91. package/dist/cli/ui.d.ts +1 -1
  92. package/dist/client.d.ts +43 -0
  93. package/dist/client.js +126 -7
  94. package/dist/dialect.d.ts +123 -0
  95. package/dist/dialect.js +33 -0
  96. package/dist/errors.d.ts +74 -1
  97. package/dist/errors.js +228 -19
  98. package/dist/index-advisor.d.ts +33 -1
  99. package/dist/index-advisor.js +31 -1
  100. package/dist/introspect.d.ts +48 -0
  101. package/dist/introspect.js +221 -91
  102. package/dist/mssql.js +44 -2
  103. package/dist/mysql.d.ts +5 -2
  104. package/dist/mysql.js +203 -18
  105. package/dist/nested-write.js +7 -2
  106. package/dist/pipeline-submittable.js +18 -4
  107. package/dist/pipeline.js +76 -10
  108. package/dist/powdb.d.ts +23 -0
  109. package/dist/powdb.js +33 -2
  110. package/dist/powql.d.ts +61 -9
  111. package/dist/powql.js +187 -50
  112. package/dist/prisma-compat.js +160 -41
  113. package/dist/query/aggregates.d.ts +1 -1
  114. package/dist/query/aggregates.js +82 -20
  115. package/dist/query/batched-loader.d.ts +10 -0
  116. package/dist/query/batched-loader.js +270 -9
  117. package/dist/query/builder.d.ts +73 -0
  118. package/dist/query/builder.js +226 -30
  119. package/dist/query/filters.d.ts +162 -0
  120. package/dist/query/filters.js +246 -1
  121. package/dist/query/relations.d.ts +10 -10
  122. package/dist/query/relations.js +94 -14
  123. package/dist/query/types.d.ts +14 -1
  124. package/dist/query/utils.d.ts +146 -2
  125. package/dist/query/utils.js +204 -3
  126. package/dist/query/warn-registry.d.ts +10 -0
  127. package/dist/query/warn-registry.js +10 -0
  128. package/dist/query/where-compile.d.ts +30 -0
  129. package/dist/query/where-compile.js +40 -1
  130. package/dist/query/where.d.ts +128 -13
  131. package/dist/query/where.js +216 -80
  132. package/dist/query/writes.d.ts +1 -1
  133. package/dist/query/writes.js +40 -16
  134. package/dist/schema-builder.d.ts +2 -1
  135. package/dist/schema-sql.d.ts +94 -4
  136. package/dist/schema-sql.js +505 -30
  137. package/dist/schema.d.ts +3 -1
  138. package/dist/sqlite.d.ts +6 -0
  139. package/dist/sqlite.js +151 -10
  140. package/dist/typed-sql.d.ts +29 -1
  141. package/dist/typed-sql.js +30 -12
  142. package/package.json +6 -4
@@ -58,6 +58,7 @@ exports.resolveColumnRef = resolveColumnRef;
58
58
  exports.columnRefSql = columnRefSql;
59
59
  exports.buildOperatorClauses = buildOperatorClauses;
60
60
  exports.requireFullTextSearch = requireFullTextSearch;
61
+ exports.requireJsonContains = requireJsonContains;
61
62
  exports.requireArrayColumns = requireArrayColumns;
62
63
  exports.vectorOperator = vectorOperator;
63
64
  exports.pushVectorParam = pushVectorParam;
@@ -73,6 +74,7 @@ exports.jsonPathParam = jsonPathParam;
73
74
  exports.castJsonNumeric = castJsonNumeric;
74
75
  exports.buildArrayFilterClauses = buildArrayFilterClauses;
75
76
  exports.buildVectorFilterClauses = buildVectorFilterClauses;
77
+ exports.vectorThresholdEntries = vectorThresholdEntries;
76
78
  exports.buildTextSearchClause = buildTextSearchClause;
77
79
  exports.getColumnArrayType = getColumnArrayType;
78
80
  const errors_js_1 = require("../errors.js");
@@ -80,6 +82,41 @@ const schema_js_1 = require("../schema.js");
80
82
  const filters_js_1 = require("./filters.js");
81
83
  const utils_js_1 = require("./utils.js");
82
84
  const where_compile_js_1 = require("./where-compile.js");
85
+ /**
86
+ * LIKE-escape one bound operand through the ACTIVE dialect, falling back to the
87
+ * shared {@link escapeLike} (`\`, `%`, `_`) when the dialect declares no
88
+ * override. See {@link Dialect.escapeLikePattern} for why an engine would need
89
+ * one (T-SQL also treats `[` as a character-class opener, so the standard set
90
+ * leaves `{ contains: '[draft]' }` matching any title containing d, r, a, f or
91
+ * t).
92
+ *
93
+ * Every LIKE operand in this module goes through here, the scalar
94
+ * `contains`/`startsWith`/`endsWith` family and the JSON substring operators,
95
+ * on BOTH the SQL-build and the cache-hit param-collect side. That is the point
96
+ * of routing it through one function: the two sides must escape identically or
97
+ * a warmed template binds a differently-escaped value than the one its SQL was
98
+ * compiled for.
99
+ */
100
+ function likeOperand(qi, value) {
101
+ return qi.dialect.escapeLikePattern?.(value) ?? (0, utils_js_1.escapeLike)(value);
102
+ }
103
+ /**
104
+ * Render a caller-supplied VALUE for an error message, or a neutral placeholder
105
+ * when the process is in the default `'safe'` error-message mode.
106
+ *
107
+ * SECURITY.md states that where-clause values are rendered as key names only.
108
+ * These JSON-operator diagnostics used to interpolate the operand verbatim, and
109
+ * the `path`-missing branch fires on a perfectly VALID value whose only problem
110
+ * is a missing `path`, so the leak was not limited to malformed input: a
111
+ * `stringContains` search term reached the error text (and from there a log
112
+ * aggregator or an API error body) on a query the caller merely wrote wrong.
113
+ *
114
+ * Same gate the `NotFoundError` where-rendering already uses, so one setting
115
+ * governs both.
116
+ */
117
+ function valueForMessage(value) {
118
+ return (0, errors_js_1.getErrorMessageMode)() === 'verbose' ? JSON.stringify(value) : '<value>';
119
+ }
83
120
  /**
84
121
  * Produce a value-invariant fingerprint of a where clause.
85
122
  * Same keys + same operator shapes + same combinator structure => same string.
@@ -87,22 +124,23 @@ const where_compile_js_1 = require("./where-compile.js");
87
124
  *
88
125
  * @internal Exposed as package-private for testing via class access.
89
126
  */
90
- function fingerprintWhere(qi, where) {
127
+ function fingerprintWhere(qi, where, depth = 0) {
128
+ (0, where_compile_js_1.assertWhereDepth)(depth);
91
129
  const parts = [];
92
130
  for (const event of (0, where_compile_js_1.walkWhere)(qi.whereHost, where)) {
93
131
  switch (event.kind) {
94
132
  case 'or':
95
- parts.push(`OR[${event.conditions.map((cond) => fingerprintWhere(qi, cond)).join(',')}]`);
133
+ parts.push(`OR[${event.conditions.map((cond) => fingerprintWhere(qi, cond, depth + 1)).join(',')}]`);
96
134
  break;
97
135
  case 'and':
98
- parts.push(`AND[${event.conditions.map((cond) => fingerprintWhere(qi, cond)).join(',')}]`);
136
+ parts.push(`AND[${event.conditions.map((cond) => fingerprintWhere(qi, cond, depth + 1)).join(',')}]`);
99
137
  break;
100
138
  case 'not':
101
- parts.push(`NOT(${fingerprintWhere(qi, event.condition)})`);
139
+ parts.push(`NOT(${fingerprintWhere(qi, event.condition, depth + 1)})`);
102
140
  break;
103
141
  case 'relation':
104
142
  // { posts: { some: { published: true } } } → `posts:{some(...)}`
105
- parts.push(`${event.key}:{${fingerprintRelationParts(qi, event.relDef, event.filterObj).join(',')}}`);
143
+ parts.push(`${event.key}:{${fingerprintRelationParts(qi, event.relDef, event.filterObj, depth + 1).join(',')}}`);
106
144
  break;
107
145
  case 'scalar':
108
146
  // Column-blind scalar token (see fingerprintScalarToken): the value's
@@ -120,28 +158,28 @@ function fingerprintWhere(qi, where) {
120
158
  * {@link fingerprintRelFilter} so the FULL inner shape is captured (two
121
159
  * different sub-wheres must never collide on one cached SQL text).
122
160
  */
123
- function fingerprintRelationParts(qi, relDef, filterObj) {
161
+ function fingerprintRelationParts(qi, relDef, filterObj, depth = 0) {
124
162
  const relParts = [];
125
163
  if (filterObj.some !== undefined)
126
164
  relParts.push(filterObj.some === null
127
165
  ? 'some(null)'
128
- : `some(${fingerprintRelFilter(qi, relDef.to, filterObj.some)})`);
166
+ : `some(${fingerprintRelFilter(qi, relDef.to, filterObj.some, depth + 1)})`);
129
167
  if (filterObj.every !== undefined)
130
168
  relParts.push(filterObj.every === null
131
169
  ? 'every(null)'
132
- : `every(${fingerprintRelFilter(qi, relDef.to, filterObj.every)})`);
170
+ : `every(${fingerprintRelFilter(qi, relDef.to, filterObj.every, depth + 1)})`);
133
171
  if (filterObj.none !== undefined)
134
172
  relParts.push(filterObj.none === null
135
173
  ? 'none(null)'
136
- : `none(${fingerprintRelFilter(qi, relDef.to, filterObj.none)})`);
174
+ : `none(${fingerprintRelFilter(qi, relDef.to, filterObj.none, depth + 1)})`);
137
175
  if (filterObj.is !== undefined)
138
176
  relParts.push(filterObj.is === null
139
177
  ? 'is(null)'
140
- : `is(${fingerprintRelFilter(qi, relDef.to, filterObj.is)})`);
178
+ : `is(${fingerprintRelFilter(qi, relDef.to, filterObj.is, depth + 1)})`);
141
179
  if (filterObj.isNot !== undefined)
142
180
  relParts.push(filterObj.isNot === null
143
181
  ? 'isNot(null)'
144
- : `isNot(${fingerprintRelFilter(qi, relDef.to, filterObj.isNot)})`);
182
+ : `isNot(${fingerprintRelFilter(qi, relDef.to, filterObj.isNot, depth + 1)})`);
145
183
  return relParts;
146
184
  }
147
185
  /**
@@ -150,10 +188,10 @@ function fingerprintRelationParts(qi, relDef, filterObj) {
150
188
  * unknown, an empty-relations host makes every key scalar (matching the old
151
189
  * `meta?.relations` short-circuit).
152
190
  */
153
- function fingerprintRelFilter(qi, targetTable, subWhere) {
191
+ function fingerprintRelFilter(qi, targetTable, subWhere, depth = 0) {
154
192
  const meta = qi.schema.tables[targetTable];
155
193
  const host = meta ? scopedWhereHost(qi, meta) : emptyRelationsHost(qi, targetTable);
156
- return fingerprintScopedWhere(qi, host, subWhere);
194
+ return fingerprintScopedWhere(qi, host, subWhere, depth);
157
195
  }
158
196
  /**
159
197
  * Walk a where clause and push ONLY values into `params`, in the EXACT same
@@ -162,7 +200,8 @@ function fingerprintRelFilter(qi, targetTable, subWhere) {
162
200
  *
163
201
  * @internal Exposed as package-private for testing.
164
202
  */
165
- function collectWhereParams(qi, where, params) {
203
+ function collectWhereParams(qi, where, params, depth = 0) {
204
+ (0, where_compile_js_1.assertWhereDepth)(depth);
166
205
  // ONE canonical walk (shared with fingerprintWhere + buildWhereClause), so
167
206
  // the key order + combinator structure cannot drift out of lockstep.
168
207
  for (const event of (0, where_compile_js_1.walkWhere)(qi.whereHost, where)) {
@@ -170,13 +209,13 @@ function collectWhereParams(qi, where, params) {
170
209
  case 'or':
171
210
  case 'and':
172
211
  for (const cond of event.conditions)
173
- collectWhereParams(qi, cond, params);
212
+ collectWhereParams(qi, cond, params, depth + 1);
174
213
  break;
175
214
  case 'not':
176
- collectWhereParams(qi, event.condition, params);
215
+ collectWhereParams(qi, event.condition, params, depth + 1);
177
216
  break;
178
217
  case 'relation':
179
- collectRelationFilterParams(qi, event.relDef, event.filterObj, params);
218
+ collectRelationFilterParams(qi, event.relDef, event.filterObj, params, depth + 1);
180
219
  break;
181
220
  case 'scalar':
182
221
  collectScalarParams(qi, event.key, event.value, params);
@@ -245,40 +284,40 @@ function collectScalarParams(qi, key, value, params) {
245
284
  * Shared by every collect site that mirrors buildRelationFilter
246
285
  * (collectWhereParams, collectRelFilterParams, collectAliasWhereParams).
247
286
  */
248
- function collectRelationFilterParams(qi, relDef, filterObj, params) {
287
+ function collectRelationFilterParams(qi, relDef, filterObj, params, depth = 0) {
249
288
  const target = relDef.to;
250
289
  if (filterObj.some !== undefined && filterObj.some !== null) {
251
- collectRelFilterParams(qi, target, filterObj.some, params);
290
+ collectRelFilterParams(qi, target, filterObj.some, params, depth + 1);
252
291
  collectTargetGlobalFilterExists(qi, target, params);
253
292
  }
254
293
  if (filterObj.none !== undefined && filterObj.none !== null) {
255
- collectRelFilterParams(qi, target, filterObj.none, params);
294
+ collectRelFilterParams(qi, target, filterObj.none, params, depth + 1);
256
295
  collectTargetGlobalFilterExists(qi, target, params);
257
296
  }
258
297
  if (filterObj.every !== undefined && filterObj.every !== null) {
259
298
  // gf is only emitted (build) when the `every` sub-where compiles to a
260
299
  // filter, otherwise `every` is trivially true and no subquery is built.
261
- if (buildSubWhereForRelation(qi, target, filterObj.every, []) !== null) {
262
- collectRelFilterParams(qi, target, filterObj.every, params);
300
+ if (buildSubWhereForRelation(qi, target, filterObj.every, [], depth + 1) !== null) {
301
+ collectRelFilterParams(qi, target, filterObj.every, params, depth + 1);
263
302
  collectTargetGlobalFilterExists(qi, target, params);
264
303
  }
265
304
  }
266
305
  if (filterObj.is !== undefined) {
267
306
  if (filterObj.is !== null)
268
- collectRelFilterParams(qi, target, filterObj.is, params);
307
+ collectRelFilterParams(qi, target, filterObj.is, params, depth + 1);
269
308
  collectTargetGlobalFilterExists(qi, target, params);
270
309
  }
271
310
  if (filterObj.isNot !== undefined) {
272
311
  if (filterObj.isNot !== null)
273
- collectRelFilterParams(qi, target, filterObj.isNot, params);
312
+ collectRelFilterParams(qi, target, filterObj.isNot, params, depth + 1);
274
313
  collectTargetGlobalFilterExists(qi, target, params);
275
314
  }
276
315
  }
277
- function collectRelFilterParams(qi, targetTable, subWhere, params) {
316
+ function collectRelFilterParams(qi, targetTable, subWhere, params, depth = 0) {
278
317
  const meta = qi.schema.tables[targetTable];
279
318
  if (!meta)
280
319
  return;
281
- collectScopedWhereParams(qi, relationWhereScope(qi, targetTable, meta), subWhere, params);
320
+ collectScopedWhereParams(qi, relationWhereScope(qi, targetTable, meta), subWhere, params, depth);
282
321
  }
283
322
  /**
284
323
  * Collect params from operator clauses. Mirrors buildOperatorClauses:
@@ -315,11 +354,11 @@ function collectOperatorParams(qi, column, op, params, refCtx) {
315
354
  if (op.notIn !== undefined)
316
355
  params.push(qi.inParam(cv(op.notIn)));
317
356
  if (op.contains !== undefined)
318
- params.push(`%${(0, utils_js_1.escapeLike)(op.contains)}%`);
357
+ params.push(`%${likeOperand(qi, op.contains)}%`);
319
358
  if (op.startsWith !== undefined)
320
- params.push(`${(0, utils_js_1.escapeLike)(op.startsWith)}%`);
359
+ params.push(`${likeOperand(qi, op.startsWith)}%`);
321
360
  if (op.endsWith !== undefined)
322
- params.push(`%${(0, utils_js_1.escapeLike)(op.endsWith)}`);
361
+ params.push(`%${likeOperand(qi, op.endsWith)}`);
323
362
  }
324
363
  /**
325
364
  * Collect params from JSON filter. Mirrors buildJsonFilterClauses exactly:
@@ -342,9 +381,14 @@ function collectJsonFilterParams(qi, filter, params, column) {
342
381
  params.push(String(filter.equals));
343
382
  }
344
383
  else if (filter.equals !== undefined) {
384
+ // Mirrors the build path's refusal, and thrown before any param is pushed,
385
+ // like the text-search gate a few cases above. Not defensive: it is what
386
+ // stops a warmed template serving a shape the cold build refuses.
387
+ requireJsonContains(qi, 'equals');
345
388
  params.push(JSON.stringify(filter.equals));
346
389
  }
347
390
  if (filter.contains !== undefined) {
391
+ requireJsonContains(qi, 'contains');
348
392
  params.push(JSON.stringify(filter.contains));
349
393
  }
350
394
  if (filter.hasKey !== undefined) {
@@ -356,7 +400,7 @@ function collectJsonFilterParams(qi, filter, params, column) {
356
400
  }
357
401
  for (const { pattern, value } of jsonStringEntries(filter, column)) {
358
402
  pushPathOnce();
359
- params.push(pattern((0, utils_js_1.escapeLike)(value)));
403
+ params.push(pattern(likeOperand(qi, value)));
360
404
  }
361
405
  }
362
406
  /** Collect params from array filter. Mirrors buildArrayFilterClauses. */
@@ -373,16 +417,15 @@ function collectArrayFilterParams(qi, filter, params) {
373
417
  /**
374
418
  * Collect params for a vector distance WHERE filter. Mirrors
375
419
  * {@link buildVectorFilterClauses}: the `$n::vector` query vector first, then
376
- * the comparison threshold(s).
420
+ * the comparison threshold(s), both enumerated AND validated by the shared
421
+ * {@link vectorThresholdEntries} (see there for the production-only bug that
422
+ * inlining the loop on both sides produced).
377
423
  */
378
424
  function collectVectorFilterParams(qi, field, rawColumn, filter, params) {
379
425
  const dist = filter.distance;
380
426
  pushVectorParam(qi, field, rawColumn, dist.to, params);
381
- for (const cmp of Object.keys(filters_js_1.VECTOR_DISTANCE_COMPARATORS)) {
382
- const threshold = dist[cmp];
383
- if (threshold !== undefined)
384
- params.push(threshold);
385
- }
427
+ for (const { threshold } of vectorThresholdEntries(filter, field))
428
+ params.push(threshold);
386
429
  }
387
430
  /** Build WHERE clause from a where object (supports operators, NULL, OR) */
388
431
  function buildWhere(qi, where) {
@@ -429,7 +472,10 @@ function mergeGlobalFilter(qi, userWhere) {
429
472
  return userWhere;
430
473
  if (userWhere === undefined)
431
474
  return gf;
432
- return { AND: [userWhere, gf] };
475
+ // Branded: this `AND` is Turbine's, always exactly two branches, so it must
476
+ // not put the statement on the variable-arity (unnamed) path. See
477
+ // {@link markInternalCombinator}.
478
+ return (0, utils_js_1.markInternalCombinator)({ AND: [userWhere, gf] });
433
479
  }
434
480
  /**
435
481
  * SQL clause for `targetTable`'s global filter rendered against `alias`
@@ -548,15 +594,26 @@ allowFullTableScan) {
548
594
  * Returns null if no conditions exist.
549
595
  * Supports: equality, operators, NULL, OR, AND, NOT, relation filters (some/every/none).
550
596
  */
551
- function buildWhereClause(qi, where, params) {
597
+ function buildWhereClause(qi, where, params, depth = 0) {
598
+ (0, where_compile_js_1.assertWhereDepth)(depth);
552
599
  const andClauses = [];
600
+ // A combinator ARRAY is the one shape whose branch COUNT is written into the
601
+ // SQL text, so the statement stops being a fixed shape (see
602
+ // markVariableArity). Marked here, on the BUILD walk, because the mark
603
+ // is read immediately after `build()` inside `acquireSql`, so it decides the
604
+ // prepared-statement name of the very entry being created. The internal
605
+ // wrapper the global-filter merge synthesizes is exempt, see
606
+ // INTERNAL_COMBINATOR.
607
+ const internal = (0, utils_js_1.isInternalCombinator)(where);
553
608
  // ONE canonical walk (shared with fingerprintWhere + collectWhereParams).
554
609
  for (const event of (0, where_compile_js_1.walkWhere)(qi.whereHost, where)) {
555
610
  switch (event.kind) {
556
611
  case 'or': {
612
+ if (!internal)
613
+ qi.markVariableArity();
557
614
  const orClauses = [];
558
615
  for (const orCond of event.conditions) {
559
- const sub = buildWhereClause(qi, orCond, params);
616
+ const sub = buildWhereClause(qi, orCond, params, depth + 1);
560
617
  if (sub)
561
618
  orClauses.push(sub);
562
619
  }
@@ -565,21 +622,23 @@ function buildWhereClause(qi, where, params) {
565
622
  break;
566
623
  }
567
624
  case 'and':
625
+ if (!internal)
626
+ qi.markVariableArity();
568
627
  for (const andCond of event.conditions) {
569
- const sub = buildWhereClause(qi, andCond, params);
628
+ const sub = buildWhereClause(qi, andCond, params, depth + 1);
570
629
  if (sub)
571
630
  andClauses.push(sub);
572
631
  }
573
632
  break;
574
633
  case 'not': {
575
- const sub = buildWhereClause(qi, event.condition, params);
634
+ const sub = buildWhereClause(qi, event.condition, params, depth + 1);
576
635
  if (sub)
577
636
  andClauses.push(`NOT (${sub})`);
578
637
  break;
579
638
  }
580
639
  case 'relation': {
581
640
  // { posts: { some: { published: true } } } → EXISTS / NOT EXISTS
582
- const relClause = buildRelationFilter(qi, event.key, event.relDef, event.filterObj, params);
641
+ const relClause = buildRelationFilter(qi, event.key, event.relDef, event.filterObj, params, undefined, depth);
583
642
  if (relClause)
584
643
  andClauses.push(relClause);
585
644
  break;
@@ -704,14 +763,20 @@ function aliasWhereScope(qi, targetTable, meta, alias) {
704
763
  * former hand-mirrored walkers, since it renders the same clauses in the same
705
764
  * ({@link walkWhere}-canonical) key order.
706
765
  */
707
- function buildScopedWhere(qi, scope, where, params) {
766
+ function buildScopedWhere(qi, scope, where, params, depth = 0) {
767
+ (0, where_compile_js_1.assertWhereDepth)(depth);
708
768
  const clauses = [];
769
+ const internal = (0, utils_js_1.isInternalCombinator)(where);
709
770
  for (const event of (0, where_compile_js_1.walkWhere)(scope.host, where)) {
710
771
  switch (event.kind) {
711
772
  case 'or':
712
773
  case 'and': {
774
+ // Same variable-arity rule as the top level: a nested relation filter
775
+ // or a relation `with` where can carry a caller-sized OR just as easily.
776
+ if (!internal)
777
+ qi.markVariableArity();
713
778
  const parts = event.conditions
714
- .map((cond) => buildScopedWhere(qi, scope, cond, params))
779
+ .map((cond) => buildScopedWhere(qi, scope, cond, params, depth + 1))
715
780
  .filter((s) => s !== null)
716
781
  .map((s) => `(${s})`);
717
782
  if (parts.length > 0)
@@ -719,13 +784,13 @@ function buildScopedWhere(qi, scope, where, params) {
719
784
  break;
720
785
  }
721
786
  case 'not': {
722
- const sub = buildScopedWhere(qi, scope, event.condition, params);
787
+ const sub = buildScopedWhere(qi, scope, event.condition, params, depth + 1);
723
788
  if (sub)
724
789
  clauses.push(`NOT (${sub})`);
725
790
  break;
726
791
  }
727
792
  case 'relation': {
728
- const c = buildRelationFilter(qi, event.key, event.relDef, event.filterObj, params, scope.relationParent);
793
+ const c = buildRelationFilter(qi, event.key, event.relDef, event.filterObj, params, scope.relationParent, depth);
729
794
  if (c)
730
795
  clauses.push(c);
731
796
  break;
@@ -798,21 +863,22 @@ function buildScopedScalarClause(qi, scope, field, value, params, clauses) {
798
863
  * without rebuilding SQL. Serves both {@link collectRelFilterParams} and
799
864
  * {@link collectAliasWhereParams}.
800
865
  */
801
- function collectScopedWhereParams(qi, scope, where, params) {
866
+ function collectScopedWhereParams(qi, scope, where, params, depth = 0) {
867
+ (0, where_compile_js_1.assertWhereDepth)(depth);
802
868
  for (const event of (0, where_compile_js_1.walkWhere)(scope.host, where)) {
803
869
  switch (event.kind) {
804
870
  case 'or':
805
871
  case 'and':
806
872
  for (const cond of event.conditions)
807
- collectScopedWhereParams(qi, scope, cond, params);
873
+ collectScopedWhereParams(qi, scope, cond, params, depth + 1);
808
874
  break;
809
875
  case 'not':
810
- collectScopedWhereParams(qi, scope, event.condition, params);
876
+ collectScopedWhereParams(qi, scope, event.condition, params, depth + 1);
811
877
  break;
812
878
  case 'relation':
813
879
  // Same some→none→every→is→isNot (each: sub-where params then target
814
880
  // global-filter params) as buildRelationFilter emits.
815
- collectRelationFilterParams(qi, event.relDef, event.filterObj, params);
881
+ collectRelationFilterParams(qi, event.relDef, event.filterObj, params, depth);
816
882
  break;
817
883
  case 'scalar':
818
884
  collectScopedScalarParams(qi, scope, event.key, event.value, params);
@@ -856,21 +922,22 @@ function collectScopedScalarParams(qi, scope, field, value, params) {
856
922
  * may differ from the pre-unification walkers as long as collisions stay
857
923
  * impossible.
858
924
  */
859
- function fingerprintScopedWhere(qi, host, where) {
925
+ function fingerprintScopedWhere(qi, host, where, depth = 0) {
926
+ (0, where_compile_js_1.assertWhereDepth)(depth);
860
927
  const parts = [];
861
928
  for (const event of (0, where_compile_js_1.walkWhere)(host, where)) {
862
929
  switch (event.kind) {
863
930
  case 'or':
864
- parts.push(`OR[${event.conditions.map((c) => fingerprintScopedWhere(qi, host, c)).join(',')}]`);
931
+ parts.push(`OR[${event.conditions.map((c) => fingerprintScopedWhere(qi, host, c, depth + 1)).join(',')}]`);
865
932
  break;
866
933
  case 'and':
867
- parts.push(`AND[${event.conditions.map((c) => fingerprintScopedWhere(qi, host, c)).join(',')}]`);
934
+ parts.push(`AND[${event.conditions.map((c) => fingerprintScopedWhere(qi, host, c, depth + 1)).join(',')}]`);
868
935
  break;
869
936
  case 'not':
870
- parts.push(`NOT(${fingerprintScopedWhere(qi, host, event.condition)})`);
937
+ parts.push(`NOT(${fingerprintScopedWhere(qi, host, event.condition, depth + 1)})`);
871
938
  break;
872
939
  case 'relation':
873
- parts.push(`${event.key}:{${fingerprintRelationParts(qi, event.relDef, event.filterObj).join(',')}}`);
940
+ parts.push(`${event.key}:{${fingerprintRelationParts(qi, event.relDef, event.filterObj, depth).join(',')}}`);
874
941
  break;
875
942
  case 'scalar':
876
943
  parts.push(`${event.key}:${(0, where_compile_js_1.fingerprintScalarToken)(event.value)}`);
@@ -883,7 +950,14 @@ function fingerprintScopedWhere(qi, host, where) {
883
950
  * Build relation filter SQL: WHERE EXISTS / NOT EXISTS subquery
884
951
  * Supports: some (EXISTS), every (NOT EXISTS ... NOT), none (NOT EXISTS)
885
952
  */
886
- function buildRelationFilter(qi, _relName, relDef, filterObj, params, parentTable) {
953
+ function buildRelationFilter(qi, _relName, relDef, filterObj, params, parentTable,
954
+ /**
955
+ * Nesting depth of the WHERE walk that reached this relation filter. Each
956
+ * relation descent is a level too: `{ posts: { some: { comments: { some:
957
+ * … } } } }` recurses through here just as `NOT` does, and is exactly as
958
+ * unbounded without a cap (see {@link assertWhereDepth}).
959
+ */
960
+ depth = 0) {
887
961
  const targetTable = relDef.to;
888
962
  const targetMeta = qi.schema.tables[targetTable];
889
963
  if (!targetMeta)
@@ -955,21 +1029,21 @@ function buildRelationFilter(qi, _relName, relDef, filterObj, params, parentTabl
955
1029
  // which also skips null. Unreachable via normalization today, guarded anyway.
956
1030
  if (filterObj.some !== undefined && filterObj.some !== null) {
957
1031
  const subWhere = filterObj.some;
958
- const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params);
1032
+ const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params, depth + 1);
959
1033
  const filterAnd = filterClause ? ` AND ${filterClause}` : '';
960
1034
  clauses.push(`EXISTS (SELECT 1 FROM ${qt} WHERE ${correlation}${filterAnd}${gfAnd()})`);
961
1035
  }
962
1036
  // "none": NOT EXISTS (SELECT 1 FROM target WHERE correlation AND filter AND gf)
963
1037
  if (filterObj.none !== undefined && filterObj.none !== null) {
964
1038
  const subWhere = filterObj.none;
965
- const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params);
1039
+ const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params, depth + 1);
966
1040
  const filterAnd = filterClause ? ` AND ${filterClause}` : '';
967
1041
  clauses.push(`NOT EXISTS (SELECT 1 FROM ${qt} WHERE ${correlation}${filterAnd}${gfAnd()})`);
968
1042
  }
969
1043
  // "every": NOT EXISTS (SELECT 1 FROM target WHERE correlation AND gf AND NOT (filter))
970
1044
  if (filterObj.every !== undefined && filterObj.every !== null) {
971
1045
  const subWhere = filterObj.every;
972
- const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params);
1046
+ const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params, depth + 1);
973
1047
  if (filterClause) {
974
1048
  // gf params pushed AFTER filter params (collect mirrors this order), but
975
1049
  // placed textually inside the domain so it restricts which rows count.
@@ -988,7 +1062,7 @@ function buildRelationFilter(qi, _relName, relDef, filterObj, params, parentTabl
988
1062
  }
989
1063
  else {
990
1064
  const subWhere = filterObj.is;
991
- const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params);
1065
+ const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params, depth + 1);
992
1066
  const filterAnd = filterClause ? ` AND ${filterClause}` : '';
993
1067
  clauses.push(`EXISTS (SELECT 1 FROM ${qt} WHERE ${correlation}${filterAnd}${gfAnd()})`);
994
1068
  }
@@ -1001,7 +1075,7 @@ function buildRelationFilter(qi, _relName, relDef, filterObj, params, parentTabl
1001
1075
  }
1002
1076
  else {
1003
1077
  const subWhere = filterObj.isNot;
1004
- const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params);
1078
+ const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params, depth + 1);
1005
1079
  const filterAnd = filterClause ? ` AND ${filterClause}` : '';
1006
1080
  clauses.push(`NOT EXISTS (SELECT 1 FROM ${qt} WHERE ${correlation}${filterAnd}${gfAnd()})`);
1007
1081
  }
@@ -1012,11 +1086,11 @@ function buildRelationFilter(qi, _relName, relDef, filterObj, params, parentTabl
1012
1086
  * Build WHERE clause conditions for a relation filter subquery.
1013
1087
  * Uses the target table's column mapping to resolve field names.
1014
1088
  */
1015
- function buildSubWhereForRelation(qi, targetTable, subWhere, params) {
1089
+ function buildSubWhereForRelation(qi, targetTable, subWhere, params, depth = 0) {
1016
1090
  const meta = qi.schema.tables[targetTable];
1017
1091
  if (!meta)
1018
1092
  return null;
1019
- return buildScopedWhere(qi, relationWhereScope(qi, targetTable, meta), subWhere, params);
1093
+ return buildScopedWhere(qi, relationWhereScope(qi, targetTable, meta), subWhere, params, depth);
1020
1094
  }
1021
1095
  /**
1022
1096
  * Resolve a column's Postgres type from an arbitrary table's metadata
@@ -1260,15 +1334,15 @@ function buildOperatorClauses(qi, column, op, params, refCtx) {
1260
1334
  }
1261
1335
  const insensitive = op.mode === 'insensitive';
1262
1336
  if (op.contains !== undefined) {
1263
- params.push(`%${(0, utils_js_1.escapeLike)(op.contains)}%`);
1337
+ params.push(`%${likeOperand(qi, op.contains)}%`);
1264
1338
  clauses.push(buildLikeClause(qi, column, qi.p(params.length), insensitive));
1265
1339
  }
1266
1340
  if (op.startsWith !== undefined) {
1267
- params.push(`${(0, utils_js_1.escapeLike)(op.startsWith)}%`);
1341
+ params.push(`${likeOperand(qi, op.startsWith)}%`);
1268
1342
  clauses.push(buildLikeClause(qi, column, qi.p(params.length), insensitive));
1269
1343
  }
1270
1344
  if (op.endsWith !== undefined) {
1271
- params.push(`%${(0, utils_js_1.escapeLike)(op.endsWith)}`);
1345
+ params.push(`%${likeOperand(qi, op.endsWith)}`);
1272
1346
  clauses.push(buildLikeClause(qi, column, qi.p(params.length), insensitive));
1273
1347
  }
1274
1348
  return clauses;
@@ -1287,6 +1361,34 @@ function requireFullTextSearch(qi) {
1287
1361
  throw new errors_js_1.UnsupportedFeatureError('the full-text search filter (`search`)', qi.dialect.name, 'Full-text `search` compiles to PostgreSQL to_tsvector/to_tsquery. ' +
1288
1362
  'Use `contains` (LIKE) on this engine, or run the query on PostgreSQL.');
1289
1363
  }
1364
+ /**
1365
+ * Gate the pathless JSON containment filters (`contains`, and the `equals`
1366
+ * spelling that compiles to the same expression) on
1367
+ * {@link Dialect.supportsJsonContains}.
1368
+ *
1369
+ * The engine this exists for is SQLite, whose emulation compares a decoded
1370
+ * `json_each.value` against a param bound as JSON TEXT and therefore matched
1371
+ * NOTHING, for every operand type: object, array, string and number alike all
1372
+ * returned zero rows where PostgreSQL and MySQL returned the document (measured
1373
+ * in-process, table in dialect.ts). Fewer rows with no error is precisely what
1374
+ * the capability contract exists to convert into a refusal, and since the
1375
+ * feature never worked there, refusing it removes nothing.
1376
+ *
1377
+ * Called from BOTH the build and the param-collect side, and here that is
1378
+ * load-bearing rather than symmetric-for-its-own-sake: a {@link JsonFilter}
1379
+ * fingerprints by which KEYS are present, not by what they hold, so every
1380
+ * `contains` on a column shares one cache entry and a build-only gate would be
1381
+ * skipped for the entire warm life of that entry.
1382
+ */
1383
+ function requireJsonContains(qi, clause) {
1384
+ if (qi.dialect.supportsJsonContains)
1385
+ return;
1386
+ 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 ` +
1387
+ `\`${clause}\` matched no rows for any operand rather than failing, so it is refused instead. ` +
1388
+ 'Filter on the specific value instead: `{ path: [...], equals: ... }` compiles to a real JSON ' +
1389
+ 'extraction (`json_extract`) rather than an emulation. For structural containment, run the ' +
1390
+ 'query on PostgreSQL or MySQL.');
1391
+ }
1290
1392
  /**
1291
1393
  * Gate the array filter operators (`has` / `hasEvery` / `hasSome` / `isEmpty`)
1292
1394
  * on {@link Dialect.supportsArrayColumns}. They compile to PostgreSQL array
@@ -1487,10 +1589,13 @@ function jsonStringEntries(filter, column) {
1487
1589
  continue;
1488
1590
  if (filter.path === undefined) {
1489
1591
  throw new errors_js_1.ValidationError(`[turbine] JSON operator '${op}' on ${column} requires a \`path\` ` +
1490
- `(e.g. { path: ['meta', 'title'], ${op}: ${JSON.stringify(value)} }).`);
1592
+ `(e.g. { path: ['meta', 'title'], ${op}: ${valueForMessage(value)} }).`);
1491
1593
  }
1492
1594
  if (typeof value !== 'string') {
1493
- throw new errors_js_1.ValidationError(`[turbine] JSON operator '${op}' on ${column} requires a string, got ${JSON.stringify(value)}.`);
1595
+ // `typeof` rather than the value: it says everything the reader needs
1596
+ // (they passed a number where a string belongs) and carries no data.
1597
+ throw new errors_js_1.ValidationError(`[turbine] JSON operator '${op}' on ${column} requires a string, got ${typeof value} ` +
1598
+ `(${valueForMessage(value)}).`);
1494
1599
  }
1495
1600
  entries.push({ op, pattern, value });
1496
1601
  }
@@ -1504,11 +1609,11 @@ function jsonRangeEntries(_qi, filter, column) {
1504
1609
  continue;
1505
1610
  if (filter.path === undefined) {
1506
1611
  throw new errors_js_1.ValidationError(`[turbine] JSON range operator '${op}' on ${column} requires a \`path\` ` +
1507
- `(e.g. { path: ['meta', 'score'], ${op}: ${JSON.stringify(value)} }).`);
1612
+ `(e.g. { path: ['meta', 'score'], ${op}: ${valueForMessage(value)} }).`);
1508
1613
  }
1509
1614
  if (typeof value !== 'number' && typeof value !== 'string') {
1510
1615
  throw new errors_js_1.ValidationError(`[turbine] JSON range operator '${op}' on ${column} requires a number or string, ` +
1511
- `got ${JSON.stringify(value)}.`);
1616
+ `got ${typeof value} (${valueForMessage(value)}).`);
1512
1617
  }
1513
1618
  if (typeof value === 'number' && !Number.isFinite(value)) {
1514
1619
  throw new errors_js_1.ValidationError(`[turbine] JSON range operator '${op}' on ${column} requires a finite number.`);
@@ -1545,12 +1650,18 @@ function buildJsonFilterClauses(qi, column, filter, params) {
1545
1650
  clauses.push(`${extract} = ${qi.p(params.length)}`);
1546
1651
  }
1547
1652
  else if (filter.equals !== undefined) {
1548
- // Containment equality: column @> $N::jsonb
1653
+ // Containment equality: column @> $N::jsonb. A pathless `equals` compiles
1654
+ // to the SAME containment expression `contains` does, so it inherits the
1655
+ // same engine limitation and is refused under its own clause name (the two
1656
+ // spellings are far enough apart that naming the wrong one would send the
1657
+ // reader to the wrong line).
1658
+ requireJsonContains(qi, 'equals');
1549
1659
  params.push(JSON.stringify(filter.equals));
1550
1660
  clauses.push(qi.dialect.buildJsonContains(column, qi.p(params.length)));
1551
1661
  }
1552
1662
  if (filter.contains !== undefined) {
1553
1663
  // Containment: column @> $N::jsonb
1664
+ requireJsonContains(qi, 'contains');
1554
1665
  params.push(JSON.stringify(filter.contains));
1555
1666
  clauses.push(qi.dialect.buildJsonContains(column, qi.p(params.length)));
1556
1667
  }
@@ -1572,7 +1683,7 @@ function buildJsonFilterClauses(qi, column, filter, params) {
1572
1683
  // `_` matches literally instead of turning into a wildcard.
1573
1684
  for (const { pattern, value } of jsonStringEntries(filter, column)) {
1574
1685
  const extract = pathExtract();
1575
- params.push(pattern((0, utils_js_1.escapeLike)(value)));
1686
+ params.push(pattern(likeOperand(qi, value)));
1576
1687
  clauses.push(buildLikeClause(qi, extract, qi.p(params.length), filter.mode === 'insensitive'));
1577
1688
  }
1578
1689
  return clauses;
@@ -1657,21 +1768,48 @@ function buildVectorFilterClauses(qi, field, rawColumn, filter, params) {
1657
1768
  const placeholder = pushVectorParam(qi, field, rawColumn, dist.to, params);
1658
1769
  const distanceExpr = `${qi.q(rawColumn)} ${operator} ${placeholder}`;
1659
1770
  const clauses = [];
1771
+ for (const { sqlOp, threshold } of vectorThresholdEntries(filter, field)) {
1772
+ params.push(threshold);
1773
+ clauses.push(`${distanceExpr} ${sqlOp} ${qi.p(params.length)}`);
1774
+ }
1775
+ return clauses;
1776
+ }
1777
+ /**
1778
+ * Validate and enumerate the distance comparisons on a vector filter, in the
1779
+ * fixed {@link VECTOR_DISTANCE_COMPARATORS} order. Shared by the SQL-build path
1780
+ * ({@link buildVectorFilterClauses}) and the cache-hit param-collect path
1781
+ * ({@link collectVectorFilterParams}), exactly like {@link jsonRangeEntries}.
1782
+ *
1783
+ * IT IS THE VALIDATION THAT MAKES THIS SHARED, not the enumeration. Both sides
1784
+ * used to inline the same `for` loop, and only the build side checked the
1785
+ * threshold. That build side does not run on a cache HIT, and outside dev the
1786
+ * lockstep cross-check (which re-runs it, and is what masked this) is off, so a
1787
+ * warmed template bound `lt: NaN`, `lt: '5'` or `lt: { a: 1 }` straight into
1788
+ * the statement. NaN is the one that matters: Postgres sorts it above every
1789
+ * real distance, so `distance < NaN` matches EVERY row, i.e. the predicate
1790
+ * inverts, silently, in production only. Same bug class as the 0.19.2 /
1791
+ * 0.32.1 cache-hit drifts, and the same fix: one function, both paths.
1792
+ *
1793
+ * The "at least one comparison" refusal lives here too, so the collect path
1794
+ * cannot quietly accept a filter the build path rejects.
1795
+ */
1796
+ function vectorThresholdEntries(filter, field) {
1797
+ const dist = filter.distance;
1798
+ const entries = [];
1660
1799
  for (const [cmp, sqlOp] of Object.entries(filters_js_1.VECTOR_DISTANCE_COMPARATORS)) {
1661
1800
  const threshold = dist[cmp];
1662
1801
  if (threshold === undefined)
1663
1802
  continue;
1664
1803
  if (typeof threshold !== 'number' || !Number.isFinite(threshold)) {
1665
1804
  throw new errors_js_1.ValidationError(`[turbine] Vector distance threshold "${cmp}" on "${field}" must be a finite number; ` +
1666
- `got ${JSON.stringify(threshold)}.`);
1805
+ `got ${typeof threshold} (${valueForMessage(threshold)}).`);
1667
1806
  }
1668
- params.push(threshold);
1669
- clauses.push(`${distanceExpr} ${sqlOp} ${qi.p(params.length)}`);
1807
+ entries.push({ sqlOp, threshold });
1670
1808
  }
1671
- if (clauses.length === 0) {
1809
+ if (entries.length === 0) {
1672
1810
  throw new errors_js_1.ValidationError(`[turbine] Vector distance filter on "${field}" requires at least one comparison (lt / lte / gt / gte).`);
1673
1811
  }
1674
- return clauses;
1812
+ return entries;
1675
1813
  }
1676
1814
  /**
1677
1815
  * Build SQL clause for full-text search using to_tsvector @@ to_tsquery.