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
@@ -10,11 +10,46 @@
10
10
  * instance (built once in its constructor) exposing exactly the class-resident
11
11
  * primitives this module needs. See builder.ts for the thin delegating methods.
12
12
  */
13
- import { UnsupportedFeatureError, ValidationError } from '../errors.js';
13
+ import { getErrorMessageMode, UnsupportedFeatureError, ValidationError } from '../errors.js';
14
14
  import { camelToSnake, normalizeKeyColumns } from '../schema.js';
15
15
  import { assertBindableEqualsOperand, findArrayUniqueKey, findJsonUniqueKey, isArrayFilter, isColumnRef, isJsonFilter, isUnmatchedPlainObject, isWhereOperator, JSON_FILTER_KEYS, JSON_RANGE_OPERATORS, JSON_STRING_OPERATORS, VECTOR_DISTANCE_COMPARATORS, VECTOR_METRIC_OPERATORS, validateTextSearchConfig, } from './filters.js';
16
- import { coerceTemporalValue, escapeLike, OPERATOR_KEYS, ownLookup } from './utils.js';
17
- import { classifyScalarForSql, fingerprintScalarToken, walkWhere, } from './where-compile.js';
16
+ import { coerceTemporalValue, escapeLike, isInternalCombinator, markInternalCombinator, OPERATOR_KEYS, ownLookup, } from './utils.js';
17
+ import { assertWhereDepth, classifyScalarForSql, fingerprintScalarToken, walkWhere, } from './where-compile.js';
18
+ /**
19
+ * LIKE-escape one bound operand through the ACTIVE dialect, falling back to the
20
+ * shared {@link escapeLike} (`\`, `%`, `_`) when the dialect declares no
21
+ * override. See {@link Dialect.escapeLikePattern} for why an engine would need
22
+ * one (T-SQL also treats `[` as a character-class opener, so the standard set
23
+ * leaves `{ contains: '[draft]' }` matching any title containing d, r, a, f or
24
+ * t).
25
+ *
26
+ * Every LIKE operand in this module goes through here, the scalar
27
+ * `contains`/`startsWith`/`endsWith` family and the JSON substring operators,
28
+ * on BOTH the SQL-build and the cache-hit param-collect side. That is the point
29
+ * of routing it through one function: the two sides must escape identically or
30
+ * a warmed template binds a differently-escaped value than the one its SQL was
31
+ * compiled for.
32
+ */
33
+ function likeOperand(qi, value) {
34
+ return qi.dialect.escapeLikePattern?.(value) ?? escapeLike(value);
35
+ }
36
+ /**
37
+ * Render a caller-supplied VALUE for an error message, or a neutral placeholder
38
+ * when the process is in the default `'safe'` error-message mode.
39
+ *
40
+ * SECURITY.md states that where-clause values are rendered as key names only.
41
+ * These JSON-operator diagnostics used to interpolate the operand verbatim, and
42
+ * the `path`-missing branch fires on a perfectly VALID value whose only problem
43
+ * is a missing `path`, so the leak was not limited to malformed input: a
44
+ * `stringContains` search term reached the error text (and from there a log
45
+ * aggregator or an API error body) on a query the caller merely wrote wrong.
46
+ *
47
+ * Same gate the `NotFoundError` where-rendering already uses, so one setting
48
+ * governs both.
49
+ */
50
+ function valueForMessage(value) {
51
+ return getErrorMessageMode() === 'verbose' ? JSON.stringify(value) : '<value>';
52
+ }
18
53
  /**
19
54
  * Produce a value-invariant fingerprint of a where clause.
20
55
  * Same keys + same operator shapes + same combinator structure => same string.
@@ -22,22 +57,23 @@ import { classifyScalarForSql, fingerprintScalarToken, walkWhere, } from './wher
22
57
  *
23
58
  * @internal Exposed as package-private for testing via class access.
24
59
  */
25
- export function fingerprintWhere(qi, where) {
60
+ export function fingerprintWhere(qi, where, depth = 0) {
61
+ assertWhereDepth(depth);
26
62
  const parts = [];
27
63
  for (const event of walkWhere(qi.whereHost, where)) {
28
64
  switch (event.kind) {
29
65
  case 'or':
30
- parts.push(`OR[${event.conditions.map((cond) => fingerprintWhere(qi, cond)).join(',')}]`);
66
+ parts.push(`OR[${event.conditions.map((cond) => fingerprintWhere(qi, cond, depth + 1)).join(',')}]`);
31
67
  break;
32
68
  case 'and':
33
- parts.push(`AND[${event.conditions.map((cond) => fingerprintWhere(qi, cond)).join(',')}]`);
69
+ parts.push(`AND[${event.conditions.map((cond) => fingerprintWhere(qi, cond, depth + 1)).join(',')}]`);
34
70
  break;
35
71
  case 'not':
36
- parts.push(`NOT(${fingerprintWhere(qi, event.condition)})`);
72
+ parts.push(`NOT(${fingerprintWhere(qi, event.condition, depth + 1)})`);
37
73
  break;
38
74
  case 'relation':
39
75
  // { posts: { some: { published: true } } } → `posts:{some(...)}`
40
- parts.push(`${event.key}:{${fingerprintRelationParts(qi, event.relDef, event.filterObj).join(',')}}`);
76
+ parts.push(`${event.key}:{${fingerprintRelationParts(qi, event.relDef, event.filterObj, depth + 1).join(',')}}`);
41
77
  break;
42
78
  case 'scalar':
43
79
  // Column-blind scalar token (see fingerprintScalarToken): the value's
@@ -55,28 +91,28 @@ export function fingerprintWhere(qi, where) {
55
91
  * {@link fingerprintRelFilter} so the FULL inner shape is captured (two
56
92
  * different sub-wheres must never collide on one cached SQL text).
57
93
  */
58
- export function fingerprintRelationParts(qi, relDef, filterObj) {
94
+ export function fingerprintRelationParts(qi, relDef, filterObj, depth = 0) {
59
95
  const relParts = [];
60
96
  if (filterObj.some !== undefined)
61
97
  relParts.push(filterObj.some === null
62
98
  ? 'some(null)'
63
- : `some(${fingerprintRelFilter(qi, relDef.to, filterObj.some)})`);
99
+ : `some(${fingerprintRelFilter(qi, relDef.to, filterObj.some, depth + 1)})`);
64
100
  if (filterObj.every !== undefined)
65
101
  relParts.push(filterObj.every === null
66
102
  ? 'every(null)'
67
- : `every(${fingerprintRelFilter(qi, relDef.to, filterObj.every)})`);
103
+ : `every(${fingerprintRelFilter(qi, relDef.to, filterObj.every, depth + 1)})`);
68
104
  if (filterObj.none !== undefined)
69
105
  relParts.push(filterObj.none === null
70
106
  ? 'none(null)'
71
- : `none(${fingerprintRelFilter(qi, relDef.to, filterObj.none)})`);
107
+ : `none(${fingerprintRelFilter(qi, relDef.to, filterObj.none, depth + 1)})`);
72
108
  if (filterObj.is !== undefined)
73
109
  relParts.push(filterObj.is === null
74
110
  ? 'is(null)'
75
- : `is(${fingerprintRelFilter(qi, relDef.to, filterObj.is)})`);
111
+ : `is(${fingerprintRelFilter(qi, relDef.to, filterObj.is, depth + 1)})`);
76
112
  if (filterObj.isNot !== undefined)
77
113
  relParts.push(filterObj.isNot === null
78
114
  ? 'isNot(null)'
79
- : `isNot(${fingerprintRelFilter(qi, relDef.to, filterObj.isNot)})`);
115
+ : `isNot(${fingerprintRelFilter(qi, relDef.to, filterObj.isNot, depth + 1)})`);
80
116
  return relParts;
81
117
  }
82
118
  /**
@@ -85,10 +121,10 @@ export function fingerprintRelationParts(qi, relDef, filterObj) {
85
121
  * unknown, an empty-relations host makes every key scalar (matching the old
86
122
  * `meta?.relations` short-circuit).
87
123
  */
88
- export function fingerprintRelFilter(qi, targetTable, subWhere) {
124
+ export function fingerprintRelFilter(qi, targetTable, subWhere, depth = 0) {
89
125
  const meta = qi.schema.tables[targetTable];
90
126
  const host = meta ? scopedWhereHost(qi, meta) : emptyRelationsHost(qi, targetTable);
91
- return fingerprintScopedWhere(qi, host, subWhere);
127
+ return fingerprintScopedWhere(qi, host, subWhere, depth);
92
128
  }
93
129
  /**
94
130
  * Walk a where clause and push ONLY values into `params`, in the EXACT same
@@ -97,7 +133,8 @@ export function fingerprintRelFilter(qi, targetTable, subWhere) {
97
133
  *
98
134
  * @internal Exposed as package-private for testing.
99
135
  */
100
- export function collectWhereParams(qi, where, params) {
136
+ export function collectWhereParams(qi, where, params, depth = 0) {
137
+ assertWhereDepth(depth);
101
138
  // ONE canonical walk (shared with fingerprintWhere + buildWhereClause), so
102
139
  // the key order + combinator structure cannot drift out of lockstep.
103
140
  for (const event of walkWhere(qi.whereHost, where)) {
@@ -105,13 +142,13 @@ export function collectWhereParams(qi, where, params) {
105
142
  case 'or':
106
143
  case 'and':
107
144
  for (const cond of event.conditions)
108
- collectWhereParams(qi, cond, params);
145
+ collectWhereParams(qi, cond, params, depth + 1);
109
146
  break;
110
147
  case 'not':
111
- collectWhereParams(qi, event.condition, params);
148
+ collectWhereParams(qi, event.condition, params, depth + 1);
112
149
  break;
113
150
  case 'relation':
114
- collectRelationFilterParams(qi, event.relDef, event.filterObj, params);
151
+ collectRelationFilterParams(qi, event.relDef, event.filterObj, params, depth + 1);
115
152
  break;
116
153
  case 'scalar':
117
154
  collectScalarParams(qi, event.key, event.value, params);
@@ -180,40 +217,40 @@ export function collectScalarParams(qi, key, value, params) {
180
217
  * Shared by every collect site that mirrors buildRelationFilter
181
218
  * (collectWhereParams, collectRelFilterParams, collectAliasWhereParams).
182
219
  */
183
- export function collectRelationFilterParams(qi, relDef, filterObj, params) {
220
+ export function collectRelationFilterParams(qi, relDef, filterObj, params, depth = 0) {
184
221
  const target = relDef.to;
185
222
  if (filterObj.some !== undefined && filterObj.some !== null) {
186
- collectRelFilterParams(qi, target, filterObj.some, params);
223
+ collectRelFilterParams(qi, target, filterObj.some, params, depth + 1);
187
224
  collectTargetGlobalFilterExists(qi, target, params);
188
225
  }
189
226
  if (filterObj.none !== undefined && filterObj.none !== null) {
190
- collectRelFilterParams(qi, target, filterObj.none, params);
227
+ collectRelFilterParams(qi, target, filterObj.none, params, depth + 1);
191
228
  collectTargetGlobalFilterExists(qi, target, params);
192
229
  }
193
230
  if (filterObj.every !== undefined && filterObj.every !== null) {
194
231
  // gf is only emitted (build) when the `every` sub-where compiles to a
195
232
  // filter, otherwise `every` is trivially true and no subquery is built.
196
- if (buildSubWhereForRelation(qi, target, filterObj.every, []) !== null) {
197
- collectRelFilterParams(qi, target, filterObj.every, params);
233
+ if (buildSubWhereForRelation(qi, target, filterObj.every, [], depth + 1) !== null) {
234
+ collectRelFilterParams(qi, target, filterObj.every, params, depth + 1);
198
235
  collectTargetGlobalFilterExists(qi, target, params);
199
236
  }
200
237
  }
201
238
  if (filterObj.is !== undefined) {
202
239
  if (filterObj.is !== null)
203
- collectRelFilterParams(qi, target, filterObj.is, params);
240
+ collectRelFilterParams(qi, target, filterObj.is, params, depth + 1);
204
241
  collectTargetGlobalFilterExists(qi, target, params);
205
242
  }
206
243
  if (filterObj.isNot !== undefined) {
207
244
  if (filterObj.isNot !== null)
208
- collectRelFilterParams(qi, target, filterObj.isNot, params);
245
+ collectRelFilterParams(qi, target, filterObj.isNot, params, depth + 1);
209
246
  collectTargetGlobalFilterExists(qi, target, params);
210
247
  }
211
248
  }
212
- export function collectRelFilterParams(qi, targetTable, subWhere, params) {
249
+ export function collectRelFilterParams(qi, targetTable, subWhere, params, depth = 0) {
213
250
  const meta = qi.schema.tables[targetTable];
214
251
  if (!meta)
215
252
  return;
216
- collectScopedWhereParams(qi, relationWhereScope(qi, targetTable, meta), subWhere, params);
253
+ collectScopedWhereParams(qi, relationWhereScope(qi, targetTable, meta), subWhere, params, depth);
217
254
  }
218
255
  /**
219
256
  * Collect params from operator clauses. Mirrors buildOperatorClauses:
@@ -250,11 +287,11 @@ export function collectOperatorParams(qi, column, op, params, refCtx) {
250
287
  if (op.notIn !== undefined)
251
288
  params.push(qi.inParam(cv(op.notIn)));
252
289
  if (op.contains !== undefined)
253
- params.push(`%${escapeLike(op.contains)}%`);
290
+ params.push(`%${likeOperand(qi, op.contains)}%`);
254
291
  if (op.startsWith !== undefined)
255
- params.push(`${escapeLike(op.startsWith)}%`);
292
+ params.push(`${likeOperand(qi, op.startsWith)}%`);
256
293
  if (op.endsWith !== undefined)
257
- params.push(`%${escapeLike(op.endsWith)}`);
294
+ params.push(`%${likeOperand(qi, op.endsWith)}`);
258
295
  }
259
296
  /**
260
297
  * Collect params from JSON filter. Mirrors buildJsonFilterClauses exactly:
@@ -277,9 +314,14 @@ export function collectJsonFilterParams(qi, filter, params, column) {
277
314
  params.push(String(filter.equals));
278
315
  }
279
316
  else if (filter.equals !== undefined) {
317
+ // Mirrors the build path's refusal, and thrown before any param is pushed,
318
+ // like the text-search gate a few cases above. Not defensive: it is what
319
+ // stops a warmed template serving a shape the cold build refuses.
320
+ requireJsonContains(qi, 'equals');
280
321
  params.push(JSON.stringify(filter.equals));
281
322
  }
282
323
  if (filter.contains !== undefined) {
324
+ requireJsonContains(qi, 'contains');
283
325
  params.push(JSON.stringify(filter.contains));
284
326
  }
285
327
  if (filter.hasKey !== undefined) {
@@ -291,7 +333,7 @@ export function collectJsonFilterParams(qi, filter, params, column) {
291
333
  }
292
334
  for (const { pattern, value } of jsonStringEntries(filter, column)) {
293
335
  pushPathOnce();
294
- params.push(pattern(escapeLike(value)));
336
+ params.push(pattern(likeOperand(qi, value)));
295
337
  }
296
338
  }
297
339
  /** Collect params from array filter. Mirrors buildArrayFilterClauses. */
@@ -308,16 +350,15 @@ export function collectArrayFilterParams(qi, filter, params) {
308
350
  /**
309
351
  * Collect params for a vector distance WHERE filter. Mirrors
310
352
  * {@link buildVectorFilterClauses}: the `$n::vector` query vector first, then
311
- * the comparison threshold(s).
353
+ * the comparison threshold(s), both enumerated AND validated by the shared
354
+ * {@link vectorThresholdEntries} (see there for the production-only bug that
355
+ * inlining the loop on both sides produced).
312
356
  */
313
357
  export function collectVectorFilterParams(qi, field, rawColumn, filter, params) {
314
358
  const dist = filter.distance;
315
359
  pushVectorParam(qi, field, rawColumn, dist.to, params);
316
- for (const cmp of Object.keys(VECTOR_DISTANCE_COMPARATORS)) {
317
- const threshold = dist[cmp];
318
- if (threshold !== undefined)
319
- params.push(threshold);
320
- }
360
+ for (const { threshold } of vectorThresholdEntries(filter, field))
361
+ params.push(threshold);
321
362
  }
322
363
  /** Build WHERE clause from a where object (supports operators, NULL, OR) */
323
364
  export function buildWhere(qi, where) {
@@ -364,7 +405,10 @@ export function mergeGlobalFilter(qi, userWhere) {
364
405
  return userWhere;
365
406
  if (userWhere === undefined)
366
407
  return gf;
367
- return { AND: [userWhere, gf] };
408
+ // Branded: this `AND` is Turbine's, always exactly two branches, so it must
409
+ // not put the statement on the variable-arity (unnamed) path. See
410
+ // {@link markInternalCombinator}.
411
+ return markInternalCombinator({ AND: [userWhere, gf] });
368
412
  }
369
413
  /**
370
414
  * SQL clause for `targetTable`'s global filter rendered against `alias`
@@ -483,15 +527,26 @@ allowFullTableScan) {
483
527
  * Returns null if no conditions exist.
484
528
  * Supports: equality, operators, NULL, OR, AND, NOT, relation filters (some/every/none).
485
529
  */
486
- export function buildWhereClause(qi, where, params) {
530
+ export function buildWhereClause(qi, where, params, depth = 0) {
531
+ assertWhereDepth(depth);
487
532
  const andClauses = [];
533
+ // A combinator ARRAY is the one shape whose branch COUNT is written into the
534
+ // SQL text, so the statement stops being a fixed shape (see
535
+ // markVariableArity). Marked here, on the BUILD walk, because the mark
536
+ // is read immediately after `build()` inside `acquireSql`, so it decides the
537
+ // prepared-statement name of the very entry being created. The internal
538
+ // wrapper the global-filter merge synthesizes is exempt, see
539
+ // INTERNAL_COMBINATOR.
540
+ const internal = isInternalCombinator(where);
488
541
  // ONE canonical walk (shared with fingerprintWhere + collectWhereParams).
489
542
  for (const event of walkWhere(qi.whereHost, where)) {
490
543
  switch (event.kind) {
491
544
  case 'or': {
545
+ if (!internal)
546
+ qi.markVariableArity();
492
547
  const orClauses = [];
493
548
  for (const orCond of event.conditions) {
494
- const sub = buildWhereClause(qi, orCond, params);
549
+ const sub = buildWhereClause(qi, orCond, params, depth + 1);
495
550
  if (sub)
496
551
  orClauses.push(sub);
497
552
  }
@@ -500,21 +555,23 @@ export function buildWhereClause(qi, where, params) {
500
555
  break;
501
556
  }
502
557
  case 'and':
558
+ if (!internal)
559
+ qi.markVariableArity();
503
560
  for (const andCond of event.conditions) {
504
- const sub = buildWhereClause(qi, andCond, params);
561
+ const sub = buildWhereClause(qi, andCond, params, depth + 1);
505
562
  if (sub)
506
563
  andClauses.push(sub);
507
564
  }
508
565
  break;
509
566
  case 'not': {
510
- const sub = buildWhereClause(qi, event.condition, params);
567
+ const sub = buildWhereClause(qi, event.condition, params, depth + 1);
511
568
  if (sub)
512
569
  andClauses.push(`NOT (${sub})`);
513
570
  break;
514
571
  }
515
572
  case 'relation': {
516
573
  // { posts: { some: { published: true } } } → EXISTS / NOT EXISTS
517
- const relClause = buildRelationFilter(qi, event.key, event.relDef, event.filterObj, params);
574
+ const relClause = buildRelationFilter(qi, event.key, event.relDef, event.filterObj, params, undefined, depth);
518
575
  if (relClause)
519
576
  andClauses.push(relClause);
520
577
  break;
@@ -639,14 +696,20 @@ export function aliasWhereScope(qi, targetTable, meta, alias) {
639
696
  * former hand-mirrored walkers, since it renders the same clauses in the same
640
697
  * ({@link walkWhere}-canonical) key order.
641
698
  */
642
- export function buildScopedWhere(qi, scope, where, params) {
699
+ export function buildScopedWhere(qi, scope, where, params, depth = 0) {
700
+ assertWhereDepth(depth);
643
701
  const clauses = [];
702
+ const internal = isInternalCombinator(where);
644
703
  for (const event of walkWhere(scope.host, where)) {
645
704
  switch (event.kind) {
646
705
  case 'or':
647
706
  case 'and': {
707
+ // Same variable-arity rule as the top level: a nested relation filter
708
+ // or a relation `with` where can carry a caller-sized OR just as easily.
709
+ if (!internal)
710
+ qi.markVariableArity();
648
711
  const parts = event.conditions
649
- .map((cond) => buildScopedWhere(qi, scope, cond, params))
712
+ .map((cond) => buildScopedWhere(qi, scope, cond, params, depth + 1))
650
713
  .filter((s) => s !== null)
651
714
  .map((s) => `(${s})`);
652
715
  if (parts.length > 0)
@@ -654,13 +717,13 @@ export function buildScopedWhere(qi, scope, where, params) {
654
717
  break;
655
718
  }
656
719
  case 'not': {
657
- const sub = buildScopedWhere(qi, scope, event.condition, params);
720
+ const sub = buildScopedWhere(qi, scope, event.condition, params, depth + 1);
658
721
  if (sub)
659
722
  clauses.push(`NOT (${sub})`);
660
723
  break;
661
724
  }
662
725
  case 'relation': {
663
- const c = buildRelationFilter(qi, event.key, event.relDef, event.filterObj, params, scope.relationParent);
726
+ const c = buildRelationFilter(qi, event.key, event.relDef, event.filterObj, params, scope.relationParent, depth);
664
727
  if (c)
665
728
  clauses.push(c);
666
729
  break;
@@ -733,21 +796,22 @@ export function buildScopedScalarClause(qi, scope, field, value, params, clauses
733
796
  * without rebuilding SQL. Serves both {@link collectRelFilterParams} and
734
797
  * {@link collectAliasWhereParams}.
735
798
  */
736
- export function collectScopedWhereParams(qi, scope, where, params) {
799
+ export function collectScopedWhereParams(qi, scope, where, params, depth = 0) {
800
+ assertWhereDepth(depth);
737
801
  for (const event of walkWhere(scope.host, where)) {
738
802
  switch (event.kind) {
739
803
  case 'or':
740
804
  case 'and':
741
805
  for (const cond of event.conditions)
742
- collectScopedWhereParams(qi, scope, cond, params);
806
+ collectScopedWhereParams(qi, scope, cond, params, depth + 1);
743
807
  break;
744
808
  case 'not':
745
- collectScopedWhereParams(qi, scope, event.condition, params);
809
+ collectScopedWhereParams(qi, scope, event.condition, params, depth + 1);
746
810
  break;
747
811
  case 'relation':
748
812
  // Same some→none→every→is→isNot (each: sub-where params then target
749
813
  // global-filter params) as buildRelationFilter emits.
750
- collectRelationFilterParams(qi, event.relDef, event.filterObj, params);
814
+ collectRelationFilterParams(qi, event.relDef, event.filterObj, params, depth);
751
815
  break;
752
816
  case 'scalar':
753
817
  collectScopedScalarParams(qi, scope, event.key, event.value, params);
@@ -791,21 +855,22 @@ export function collectScopedScalarParams(qi, scope, field, value, params) {
791
855
  * may differ from the pre-unification walkers as long as collisions stay
792
856
  * impossible.
793
857
  */
794
- export function fingerprintScopedWhere(qi, host, where) {
858
+ export function fingerprintScopedWhere(qi, host, where, depth = 0) {
859
+ assertWhereDepth(depth);
795
860
  const parts = [];
796
861
  for (const event of walkWhere(host, where)) {
797
862
  switch (event.kind) {
798
863
  case 'or':
799
- parts.push(`OR[${event.conditions.map((c) => fingerprintScopedWhere(qi, host, c)).join(',')}]`);
864
+ parts.push(`OR[${event.conditions.map((c) => fingerprintScopedWhere(qi, host, c, depth + 1)).join(',')}]`);
800
865
  break;
801
866
  case 'and':
802
- parts.push(`AND[${event.conditions.map((c) => fingerprintScopedWhere(qi, host, c)).join(',')}]`);
867
+ parts.push(`AND[${event.conditions.map((c) => fingerprintScopedWhere(qi, host, c, depth + 1)).join(',')}]`);
803
868
  break;
804
869
  case 'not':
805
- parts.push(`NOT(${fingerprintScopedWhere(qi, host, event.condition)})`);
870
+ parts.push(`NOT(${fingerprintScopedWhere(qi, host, event.condition, depth + 1)})`);
806
871
  break;
807
872
  case 'relation':
808
- parts.push(`${event.key}:{${fingerprintRelationParts(qi, event.relDef, event.filterObj).join(',')}}`);
873
+ parts.push(`${event.key}:{${fingerprintRelationParts(qi, event.relDef, event.filterObj, depth).join(',')}}`);
809
874
  break;
810
875
  case 'scalar':
811
876
  parts.push(`${event.key}:${fingerprintScalarToken(event.value)}`);
@@ -818,7 +883,14 @@ export function fingerprintScopedWhere(qi, host, where) {
818
883
  * Build relation filter SQL: WHERE EXISTS / NOT EXISTS subquery
819
884
  * Supports: some (EXISTS), every (NOT EXISTS ... NOT), none (NOT EXISTS)
820
885
  */
821
- export function buildRelationFilter(qi, _relName, relDef, filterObj, params, parentTable) {
886
+ export function buildRelationFilter(qi, _relName, relDef, filterObj, params, parentTable,
887
+ /**
888
+ * Nesting depth of the WHERE walk that reached this relation filter. Each
889
+ * relation descent is a level too: `{ posts: { some: { comments: { some:
890
+ * … } } } }` recurses through here just as `NOT` does, and is exactly as
891
+ * unbounded without a cap (see {@link assertWhereDepth}).
892
+ */
893
+ depth = 0) {
822
894
  const targetTable = relDef.to;
823
895
  const targetMeta = qi.schema.tables[targetTable];
824
896
  if (!targetMeta)
@@ -890,21 +962,21 @@ export function buildRelationFilter(qi, _relName, relDef, filterObj, params, par
890
962
  // which also skips null. Unreachable via normalization today, guarded anyway.
891
963
  if (filterObj.some !== undefined && filterObj.some !== null) {
892
964
  const subWhere = filterObj.some;
893
- const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params);
965
+ const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params, depth + 1);
894
966
  const filterAnd = filterClause ? ` AND ${filterClause}` : '';
895
967
  clauses.push(`EXISTS (SELECT 1 FROM ${qt} WHERE ${correlation}${filterAnd}${gfAnd()})`);
896
968
  }
897
969
  // "none": NOT EXISTS (SELECT 1 FROM target WHERE correlation AND filter AND gf)
898
970
  if (filterObj.none !== undefined && filterObj.none !== null) {
899
971
  const subWhere = filterObj.none;
900
- const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params);
972
+ const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params, depth + 1);
901
973
  const filterAnd = filterClause ? ` AND ${filterClause}` : '';
902
974
  clauses.push(`NOT EXISTS (SELECT 1 FROM ${qt} WHERE ${correlation}${filterAnd}${gfAnd()})`);
903
975
  }
904
976
  // "every": NOT EXISTS (SELECT 1 FROM target WHERE correlation AND gf AND NOT (filter))
905
977
  if (filterObj.every !== undefined && filterObj.every !== null) {
906
978
  const subWhere = filterObj.every;
907
- const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params);
979
+ const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params, depth + 1);
908
980
  if (filterClause) {
909
981
  // gf params pushed AFTER filter params (collect mirrors this order), but
910
982
  // placed textually inside the domain so it restricts which rows count.
@@ -923,7 +995,7 @@ export function buildRelationFilter(qi, _relName, relDef, filterObj, params, par
923
995
  }
924
996
  else {
925
997
  const subWhere = filterObj.is;
926
- const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params);
998
+ const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params, depth + 1);
927
999
  const filterAnd = filterClause ? ` AND ${filterClause}` : '';
928
1000
  clauses.push(`EXISTS (SELECT 1 FROM ${qt} WHERE ${correlation}${filterAnd}${gfAnd()})`);
929
1001
  }
@@ -936,7 +1008,7 @@ export function buildRelationFilter(qi, _relName, relDef, filterObj, params, par
936
1008
  }
937
1009
  else {
938
1010
  const subWhere = filterObj.isNot;
939
- const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params);
1011
+ const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params, depth + 1);
940
1012
  const filterAnd = filterClause ? ` AND ${filterClause}` : '';
941
1013
  clauses.push(`NOT EXISTS (SELECT 1 FROM ${qt} WHERE ${correlation}${filterAnd}${gfAnd()})`);
942
1014
  }
@@ -947,11 +1019,11 @@ export function buildRelationFilter(qi, _relName, relDef, filterObj, params, par
947
1019
  * Build WHERE clause conditions for a relation filter subquery.
948
1020
  * Uses the target table's column mapping to resolve field names.
949
1021
  */
950
- export function buildSubWhereForRelation(qi, targetTable, subWhere, params) {
1022
+ export function buildSubWhereForRelation(qi, targetTable, subWhere, params, depth = 0) {
951
1023
  const meta = qi.schema.tables[targetTable];
952
1024
  if (!meta)
953
1025
  return null;
954
- return buildScopedWhere(qi, relationWhereScope(qi, targetTable, meta), subWhere, params);
1026
+ return buildScopedWhere(qi, relationWhereScope(qi, targetTable, meta), subWhere, params, depth);
955
1027
  }
956
1028
  /**
957
1029
  * Resolve a column's Postgres type from an arbitrary table's metadata
@@ -1195,15 +1267,15 @@ export function buildOperatorClauses(qi, column, op, params, refCtx) {
1195
1267
  }
1196
1268
  const insensitive = op.mode === 'insensitive';
1197
1269
  if (op.contains !== undefined) {
1198
- params.push(`%${escapeLike(op.contains)}%`);
1270
+ params.push(`%${likeOperand(qi, op.contains)}%`);
1199
1271
  clauses.push(buildLikeClause(qi, column, qi.p(params.length), insensitive));
1200
1272
  }
1201
1273
  if (op.startsWith !== undefined) {
1202
- params.push(`${escapeLike(op.startsWith)}%`);
1274
+ params.push(`${likeOperand(qi, op.startsWith)}%`);
1203
1275
  clauses.push(buildLikeClause(qi, column, qi.p(params.length), insensitive));
1204
1276
  }
1205
1277
  if (op.endsWith !== undefined) {
1206
- params.push(`%${escapeLike(op.endsWith)}`);
1278
+ params.push(`%${likeOperand(qi, op.endsWith)}`);
1207
1279
  clauses.push(buildLikeClause(qi, column, qi.p(params.length), insensitive));
1208
1280
  }
1209
1281
  return clauses;
@@ -1222,6 +1294,34 @@ export function requireFullTextSearch(qi) {
1222
1294
  throw new UnsupportedFeatureError('the full-text search filter (`search`)', qi.dialect.name, 'Full-text `search` compiles to PostgreSQL to_tsvector/to_tsquery. ' +
1223
1295
  'Use `contains` (LIKE) on this engine, or run the query on PostgreSQL.');
1224
1296
  }
1297
+ /**
1298
+ * Gate the pathless JSON containment filters (`contains`, and the `equals`
1299
+ * spelling that compiles to the same expression) on
1300
+ * {@link Dialect.supportsJsonContains}.
1301
+ *
1302
+ * The engine this exists for is SQLite, whose emulation compares a decoded
1303
+ * `json_each.value` against a param bound as JSON TEXT and therefore matched
1304
+ * NOTHING, for every operand type: object, array, string and number alike all
1305
+ * returned zero rows where PostgreSQL and MySQL returned the document (measured
1306
+ * in-process, table in dialect.ts). Fewer rows with no error is precisely what
1307
+ * the capability contract exists to convert into a refusal, and since the
1308
+ * feature never worked there, refusing it removes nothing.
1309
+ *
1310
+ * Called from BOTH the build and the param-collect side, and here that is
1311
+ * load-bearing rather than symmetric-for-its-own-sake: a {@link JsonFilter}
1312
+ * fingerprints by which KEYS are present, not by what they hold, so every
1313
+ * `contains` on a column shares one cache entry and a build-only gate would be
1314
+ * skipped for the entire warm life of that entry.
1315
+ */
1316
+ export function requireJsonContains(qi, clause) {
1317
+ if (qi.dialect.supportsJsonContains)
1318
+ return;
1319
+ throw new UnsupportedFeatureError(`JSON containment (\`${clause}\` without \`path\`)`, qi.dialect.name, `This engine has no equivalent of PostgreSQL's \`@>\`, and the emulation Turbine used for ` +
1320
+ `\`${clause}\` matched no rows for any operand rather than failing, so it is refused instead. ` +
1321
+ 'Filter on the specific value instead: `{ path: [...], equals: ... }` compiles to a real JSON ' +
1322
+ 'extraction (`json_extract`) rather than an emulation. For structural containment, run the ' +
1323
+ 'query on PostgreSQL or MySQL.');
1324
+ }
1225
1325
  /**
1226
1326
  * Gate the array filter operators (`has` / `hasEvery` / `hasSome` / `isEmpty`)
1227
1327
  * on {@link Dialect.supportsArrayColumns}. They compile to PostgreSQL array
@@ -1422,10 +1522,13 @@ export function jsonStringEntries(filter, column) {
1422
1522
  continue;
1423
1523
  if (filter.path === undefined) {
1424
1524
  throw new ValidationError(`[turbine] JSON operator '${op}' on ${column} requires a \`path\` ` +
1425
- `(e.g. { path: ['meta', 'title'], ${op}: ${JSON.stringify(value)} }).`);
1525
+ `(e.g. { path: ['meta', 'title'], ${op}: ${valueForMessage(value)} }).`);
1426
1526
  }
1427
1527
  if (typeof value !== 'string') {
1428
- throw new ValidationError(`[turbine] JSON operator '${op}' on ${column} requires a string, got ${JSON.stringify(value)}.`);
1528
+ // `typeof` rather than the value: it says everything the reader needs
1529
+ // (they passed a number where a string belongs) and carries no data.
1530
+ throw new ValidationError(`[turbine] JSON operator '${op}' on ${column} requires a string, got ${typeof value} ` +
1531
+ `(${valueForMessage(value)}).`);
1429
1532
  }
1430
1533
  entries.push({ op, pattern, value });
1431
1534
  }
@@ -1439,11 +1542,11 @@ export function jsonRangeEntries(_qi, filter, column) {
1439
1542
  continue;
1440
1543
  if (filter.path === undefined) {
1441
1544
  throw new ValidationError(`[turbine] JSON range operator '${op}' on ${column} requires a \`path\` ` +
1442
- `(e.g. { path: ['meta', 'score'], ${op}: ${JSON.stringify(value)} }).`);
1545
+ `(e.g. { path: ['meta', 'score'], ${op}: ${valueForMessage(value)} }).`);
1443
1546
  }
1444
1547
  if (typeof value !== 'number' && typeof value !== 'string') {
1445
1548
  throw new ValidationError(`[turbine] JSON range operator '${op}' on ${column} requires a number or string, ` +
1446
- `got ${JSON.stringify(value)}.`);
1549
+ `got ${typeof value} (${valueForMessage(value)}).`);
1447
1550
  }
1448
1551
  if (typeof value === 'number' && !Number.isFinite(value)) {
1449
1552
  throw new ValidationError(`[turbine] JSON range operator '${op}' on ${column} requires a finite number.`);
@@ -1480,12 +1583,18 @@ export function buildJsonFilterClauses(qi, column, filter, params) {
1480
1583
  clauses.push(`${extract} = ${qi.p(params.length)}`);
1481
1584
  }
1482
1585
  else if (filter.equals !== undefined) {
1483
- // Containment equality: column @> $N::jsonb
1586
+ // Containment equality: column @> $N::jsonb. A pathless `equals` compiles
1587
+ // to the SAME containment expression `contains` does, so it inherits the
1588
+ // same engine limitation and is refused under its own clause name (the two
1589
+ // spellings are far enough apart that naming the wrong one would send the
1590
+ // reader to the wrong line).
1591
+ requireJsonContains(qi, 'equals');
1484
1592
  params.push(JSON.stringify(filter.equals));
1485
1593
  clauses.push(qi.dialect.buildJsonContains(column, qi.p(params.length)));
1486
1594
  }
1487
1595
  if (filter.contains !== undefined) {
1488
1596
  // Containment: column @> $N::jsonb
1597
+ requireJsonContains(qi, 'contains');
1489
1598
  params.push(JSON.stringify(filter.contains));
1490
1599
  clauses.push(qi.dialect.buildJsonContains(column, qi.p(params.length)));
1491
1600
  }
@@ -1507,7 +1616,7 @@ export function buildJsonFilterClauses(qi, column, filter, params) {
1507
1616
  // `_` matches literally instead of turning into a wildcard.
1508
1617
  for (const { pattern, value } of jsonStringEntries(filter, column)) {
1509
1618
  const extract = pathExtract();
1510
- params.push(pattern(escapeLike(value)));
1619
+ params.push(pattern(likeOperand(qi, value)));
1511
1620
  clauses.push(buildLikeClause(qi, extract, qi.p(params.length), filter.mode === 'insensitive'));
1512
1621
  }
1513
1622
  return clauses;
@@ -1592,21 +1701,48 @@ export function buildVectorFilterClauses(qi, field, rawColumn, filter, params) {
1592
1701
  const placeholder = pushVectorParam(qi, field, rawColumn, dist.to, params);
1593
1702
  const distanceExpr = `${qi.q(rawColumn)} ${operator} ${placeholder}`;
1594
1703
  const clauses = [];
1704
+ for (const { sqlOp, threshold } of vectorThresholdEntries(filter, field)) {
1705
+ params.push(threshold);
1706
+ clauses.push(`${distanceExpr} ${sqlOp} ${qi.p(params.length)}`);
1707
+ }
1708
+ return clauses;
1709
+ }
1710
+ /**
1711
+ * Validate and enumerate the distance comparisons on a vector filter, in the
1712
+ * fixed {@link VECTOR_DISTANCE_COMPARATORS} order. Shared by the SQL-build path
1713
+ * ({@link buildVectorFilterClauses}) and the cache-hit param-collect path
1714
+ * ({@link collectVectorFilterParams}), exactly like {@link jsonRangeEntries}.
1715
+ *
1716
+ * IT IS THE VALIDATION THAT MAKES THIS SHARED, not the enumeration. Both sides
1717
+ * used to inline the same `for` loop, and only the build side checked the
1718
+ * threshold. That build side does not run on a cache HIT, and outside dev the
1719
+ * lockstep cross-check (which re-runs it, and is what masked this) is off, so a
1720
+ * warmed template bound `lt: NaN`, `lt: '5'` or `lt: { a: 1 }` straight into
1721
+ * the statement. NaN is the one that matters: Postgres sorts it above every
1722
+ * real distance, so `distance < NaN` matches EVERY row, i.e. the predicate
1723
+ * inverts, silently, in production only. Same bug class as the 0.19.2 /
1724
+ * 0.32.1 cache-hit drifts, and the same fix: one function, both paths.
1725
+ *
1726
+ * The "at least one comparison" refusal lives here too, so the collect path
1727
+ * cannot quietly accept a filter the build path rejects.
1728
+ */
1729
+ export function vectorThresholdEntries(filter, field) {
1730
+ const dist = filter.distance;
1731
+ const entries = [];
1595
1732
  for (const [cmp, sqlOp] of Object.entries(VECTOR_DISTANCE_COMPARATORS)) {
1596
1733
  const threshold = dist[cmp];
1597
1734
  if (threshold === undefined)
1598
1735
  continue;
1599
1736
  if (typeof threshold !== 'number' || !Number.isFinite(threshold)) {
1600
1737
  throw new ValidationError(`[turbine] Vector distance threshold "${cmp}" on "${field}" must be a finite number; ` +
1601
- `got ${JSON.stringify(threshold)}.`);
1738
+ `got ${typeof threshold} (${valueForMessage(threshold)}).`);
1602
1739
  }
1603
- params.push(threshold);
1604
- clauses.push(`${distanceExpr} ${sqlOp} ${qi.p(params.length)}`);
1740
+ entries.push({ sqlOp, threshold });
1605
1741
  }
1606
- if (clauses.length === 0) {
1742
+ if (entries.length === 0) {
1607
1743
  throw new ValidationError(`[turbine] Vector distance filter on "${field}" requires at least one comparison (lt / lte / gt / gte).`);
1608
1744
  }
1609
- return clauses;
1745
+ return entries;
1610
1746
  }
1611
1747
  /**
1612
1748
  * Build SQL clause for full-text search using to_tsvector @@ to_tsquery.