turbine-orm 0.35.0 → 0.36.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 (68) hide show
  1. package/README.md +18 -16
  2. package/dist/cjs/cli/index.js +109 -16
  3. package/dist/cjs/cli/migrate.js +78 -3
  4. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  5. package/dist/cjs/cli/studio.js +333 -22
  6. package/dist/cjs/cli/ui.js +7 -1
  7. package/dist/cjs/dialect.js +1 -1
  8. package/dist/cjs/generate.js +23 -2
  9. package/dist/cjs/index.js +2 -1
  10. package/dist/cjs/mssql.js +22 -5
  11. package/dist/cjs/powdb.js +41 -1
  12. package/dist/cjs/powql.js +80 -25
  13. package/dist/cjs/query/aggregates.js +683 -0
  14. package/dist/cjs/query/batched-loader.js +2 -0
  15. package/dist/cjs/query/builder.js +297 -4504
  16. package/dist/cjs/query/filters.js +12 -0
  17. package/dist/cjs/query/relations.js +1698 -0
  18. package/dist/cjs/query/where-compile.js +180 -0
  19. package/dist/cjs/query/where.js +1491 -0
  20. package/dist/cjs/query/writes.js +680 -0
  21. package/dist/cjs/schema-builder.js +6 -0
  22. package/dist/cjs/schema-metadata.js +4 -0
  23. package/dist/cjs/schema-sql.js +265 -3
  24. package/dist/cjs/sqlite.js +1 -1
  25. package/dist/cli/index.d.ts +8 -2
  26. package/dist/cli/index.js +111 -18
  27. package/dist/cli/migrate.d.ts +24 -1
  28. package/dist/cli/migrate.js +77 -3
  29. package/dist/cli/studio-ui.generated.js +1 -1
  30. package/dist/cli/studio.d.ts +46 -13
  31. package/dist/cli/studio.js +331 -23
  32. package/dist/cli/ui.js +7 -1
  33. package/dist/dialect.d.ts +15 -6
  34. package/dist/dialect.js +1 -1
  35. package/dist/generate.js +23 -2
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/mssql.js +22 -5
  39. package/dist/powdb.d.ts +20 -0
  40. package/dist/powdb.js +40 -0
  41. package/dist/powql.d.ts +33 -1
  42. package/dist/powql.js +80 -25
  43. package/dist/query/aggregates.d.ts +74 -0
  44. package/dist/query/aggregates.js +641 -0
  45. package/dist/query/batched-loader.d.ts +6 -0
  46. package/dist/query/batched-loader.js +2 -0
  47. package/dist/query/builder.d.ts +62 -829
  48. package/dist/query/builder.js +302 -4509
  49. package/dist/query/deferred.d.ts +7 -0
  50. package/dist/query/filters.d.ts +7 -0
  51. package/dist/query/filters.js +11 -0
  52. package/dist/query/relations.d.ts +441 -0
  53. package/dist/query/relations.js +1627 -0
  54. package/dist/query/types.d.ts +15 -0
  55. package/dist/query/where-compile.d.ts +139 -0
  56. package/dist/query/where-compile.js +175 -0
  57. package/dist/query/where.d.ts +494 -0
  58. package/dist/query/where.js +1431 -0
  59. package/dist/query/writes.d.ts +131 -0
  60. package/dist/query/writes.js +626 -0
  61. package/dist/schema-builder.d.ts +18 -3
  62. package/dist/schema-builder.js +6 -0
  63. package/dist/schema-metadata.js +4 -0
  64. package/dist/schema-sql.d.ts +60 -3
  65. package/dist/schema-sql.js +261 -4
  66. package/dist/schema.d.ts +10 -0
  67. package/dist/sqlite.js +1 -1
  68. package/package.json +2 -2
@@ -0,0 +1,1491 @@
1
+ "use strict";
2
+ /**
3
+ * turbine-orm: WHERE-clause compilation (extracted from builder.ts)
4
+ *
5
+ * The whole WHERE web: the top-level build/collect/fingerprint trio, the
6
+ * table-scoped trio for relation-filter EXISTS sub-wheres and relation
7
+ * `with`-clause wheres, the leaf JSON/array/vector/text-search clause builders,
8
+ * operator-clause + column-reference compilation, and the client-level
9
+ * global-filter helpers. All functions take a {@link BuilderCtx} as their first
10
+ * argument: the privacy-preserving view of the owning {@link QueryInterface}
11
+ * instance (built once in its constructor) exposing exactly the class-resident
12
+ * primitives this module needs. See builder.ts for the thin delegating methods.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.fingerprintWhere = fingerprintWhere;
16
+ exports.fingerprintRelationParts = fingerprintRelationParts;
17
+ exports.fingerprintRelFilter = fingerprintRelFilter;
18
+ exports.collectWhereParams = collectWhereParams;
19
+ exports.collectScalarParams = collectScalarParams;
20
+ exports.collectRelationFilterParams = collectRelationFilterParams;
21
+ exports.collectRelFilterParams = collectRelFilterParams;
22
+ exports.collectOperatorParams = collectOperatorParams;
23
+ exports.collectJsonFilterParams = collectJsonFilterParams;
24
+ exports.collectArrayFilterParams = collectArrayFilterParams;
25
+ exports.collectVectorFilterParams = collectVectorFilterParams;
26
+ exports.buildWhere = buildWhere;
27
+ exports.resolveGlobalFilter = resolveGlobalFilter;
28
+ exports.mergeGlobalFilter = mergeGlobalFilter;
29
+ exports.targetGlobalFilterAlias = targetGlobalFilterAlias;
30
+ exports.collectTargetGlobalFilterAlias = collectTargetGlobalFilterAlias;
31
+ exports.targetGlobalFilterExists = targetGlobalFilterExists;
32
+ exports.collectTargetGlobalFilterExists = collectTargetGlobalFilterExists;
33
+ exports.globalFilterCacheSegment = globalFilterCacheSegment;
34
+ exports.userPredicateIsEmpty = userPredicateIsEmpty;
35
+ exports.assertMutationHasPredicate = assertMutationHasPredicate;
36
+ exports.buildWhereClause = buildWhereClause;
37
+ exports.buildScalarClause = buildScalarClause;
38
+ exports.emptyRelationsHost = emptyRelationsHost;
39
+ exports.scopedWhereHost = scopedWhereHost;
40
+ exports.relationWhereScope = relationWhereScope;
41
+ exports.aliasWhereScope = aliasWhereScope;
42
+ exports.buildScopedWhere = buildScopedWhere;
43
+ exports.buildScopedScalarClause = buildScopedScalarClause;
44
+ exports.collectScopedWhereParams = collectScopedWhereParams;
45
+ exports.collectScopedScalarParams = collectScopedScalarParams;
46
+ exports.fingerprintScopedWhere = fingerprintScopedWhere;
47
+ exports.buildRelationFilter = buildRelationFilter;
48
+ exports.buildSubWhereForRelation = buildSubWhereForRelation;
49
+ exports.pgTypeForColumn = pgTypeForColumn;
50
+ exports.enumTypeForColumn = enumTypeForColumn;
51
+ exports.enumCastSuffix = enumCastSuffix;
52
+ exports.assertBindableEqualityValue = assertBindableEqualityValue;
53
+ exports.buildAliasWhere = buildAliasWhere;
54
+ exports.collectAliasWhereParams = collectAliasWhereParams;
55
+ exports.fingerprintAliasWhere = fingerprintAliasWhere;
56
+ exports.resolveColumnRef = resolveColumnRef;
57
+ exports.columnRefSql = columnRefSql;
58
+ exports.buildOperatorClauses = buildOperatorClauses;
59
+ exports.vectorOperator = vectorOperator;
60
+ exports.pushVectorParam = pushVectorParam;
61
+ exports.normalizeRelationFilter = normalizeRelationFilter;
62
+ exports.isJsonColumnType = isJsonColumnType;
63
+ exports.getColumnPgType = getColumnPgType;
64
+ exports.getArrayElementType = getArrayElementType;
65
+ exports.jsonRangeEntries = jsonRangeEntries;
66
+ exports.buildJsonFilterClauses = buildJsonFilterClauses;
67
+ exports.jsonPathParam = jsonPathParam;
68
+ exports.castJsonNumeric = castJsonNumeric;
69
+ exports.buildArrayFilterClauses = buildArrayFilterClauses;
70
+ exports.buildVectorFilterClauses = buildVectorFilterClauses;
71
+ exports.buildTextSearchClause = buildTextSearchClause;
72
+ exports.getColumnArrayType = getColumnArrayType;
73
+ const errors_js_1 = require("../errors.js");
74
+ const schema_js_1 = require("../schema.js");
75
+ const filters_js_1 = require("./filters.js");
76
+ const utils_js_1 = require("./utils.js");
77
+ const where_compile_js_1 = require("./where-compile.js");
78
+ /**
79
+ * Produce a value-invariant fingerprint of a where clause.
80
+ * Same keys + same operator shapes + same combinator structure => same string.
81
+ * Different values (e.g. id=1 vs id=999) => identical fingerprint.
82
+ *
83
+ * @internal Exposed as package-private for testing via class access.
84
+ */
85
+ function fingerprintWhere(qi, where) {
86
+ const parts = [];
87
+ for (const event of (0, where_compile_js_1.walkWhere)(qi.whereHost, where)) {
88
+ switch (event.kind) {
89
+ case 'or':
90
+ parts.push(`OR[${event.conditions.map((cond) => fingerprintWhere(qi, cond)).join(',')}]`);
91
+ break;
92
+ case 'and':
93
+ parts.push(`AND[${event.conditions.map((cond) => fingerprintWhere(qi, cond)).join(',')}]`);
94
+ break;
95
+ case 'not':
96
+ parts.push(`NOT(${fingerprintWhere(qi, event.condition)})`);
97
+ break;
98
+ case 'relation':
99
+ // { posts: { some: { published: true } } } → `posts:{some(...)}`
100
+ parts.push(`${event.key}:{${fingerprintRelationParts(qi, event.relDef, event.filterObj).join(',')}}`);
101
+ break;
102
+ case 'scalar':
103
+ // Column-blind scalar token (see fingerprintScalarToken): the value's
104
+ // shape alone distinguishes the SQL, so no column lookup is needed.
105
+ parts.push(`${event.key}:${(0, where_compile_js_1.fingerprintScalarToken)(event.value)}`);
106
+ break;
107
+ }
108
+ }
109
+ return parts.join('&');
110
+ }
111
+ /**
112
+ * Fingerprint the present branches of a normalized relation filter, in the
113
+ * fixed order some→every→none→is→isNot. A `null` branch tokenizes as
114
+ * `<branch>(null)`; a present branch recurses through
115
+ * {@link fingerprintRelFilter} so the FULL inner shape is captured (two
116
+ * different sub-wheres must never collide on one cached SQL text).
117
+ */
118
+ function fingerprintRelationParts(qi, relDef, filterObj) {
119
+ const relParts = [];
120
+ if (filterObj.some !== undefined)
121
+ relParts.push(filterObj.some === null
122
+ ? 'some(null)'
123
+ : `some(${fingerprintRelFilter(qi, relDef.to, filterObj.some)})`);
124
+ if (filterObj.every !== undefined)
125
+ relParts.push(filterObj.every === null
126
+ ? 'every(null)'
127
+ : `every(${fingerprintRelFilter(qi, relDef.to, filterObj.every)})`);
128
+ if (filterObj.none !== undefined)
129
+ relParts.push(filterObj.none === null
130
+ ? 'none(null)'
131
+ : `none(${fingerprintRelFilter(qi, relDef.to, filterObj.none)})`);
132
+ if (filterObj.is !== undefined)
133
+ relParts.push(filterObj.is === null
134
+ ? 'is(null)'
135
+ : `is(${fingerprintRelFilter(qi, relDef.to, filterObj.is)})`);
136
+ if (filterObj.isNot !== undefined)
137
+ relParts.push(filterObj.isNot === null
138
+ ? 'isNot(null)'
139
+ : `isNot(${fingerprintRelFilter(qi, relDef.to, filterObj.isNot)})`);
140
+ return relParts;
141
+ }
142
+ /**
143
+ * Fingerprint a relation filter sub-where for some/every/none. Thin wrapper
144
+ * over the unified {@link fingerprintScopedWhere}. When the target table is
145
+ * unknown, an empty-relations host makes every key scalar (matching the old
146
+ * `meta?.relations` short-circuit).
147
+ */
148
+ function fingerprintRelFilter(qi, targetTable, subWhere) {
149
+ const meta = qi.schema.tables[targetTable];
150
+ const host = meta ? scopedWhereHost(qi, meta) : emptyRelationsHost(qi, targetTable);
151
+ return fingerprintScopedWhere(qi, host, subWhere);
152
+ }
153
+ /**
154
+ * Walk a where clause and push ONLY values into `params`, in the EXACT same
155
+ * order that `buildWhereClause` pushes them. Used on cache hit to fill params
156
+ * without rebuilding SQL.
157
+ *
158
+ * @internal Exposed as package-private for testing.
159
+ */
160
+ function collectWhereParams(qi, where, params) {
161
+ // ONE canonical walk (shared with fingerprintWhere + buildWhereClause), so
162
+ // the key order + combinator structure cannot drift out of lockstep.
163
+ for (const event of (0, where_compile_js_1.walkWhere)(qi.whereHost, where)) {
164
+ switch (event.kind) {
165
+ case 'or':
166
+ case 'and':
167
+ for (const cond of event.conditions)
168
+ collectWhereParams(qi, cond, params);
169
+ break;
170
+ case 'not':
171
+ collectWhereParams(qi, event.condition, params);
172
+ break;
173
+ case 'relation':
174
+ collectRelationFilterParams(qi, event.relDef, event.filterObj, params);
175
+ break;
176
+ case 'scalar':
177
+ collectScalarParams(qi, event.key, event.value, params);
178
+ break;
179
+ }
180
+ }
181
+ }
182
+ /**
183
+ * Push a scalar WHERE value's params, mirroring {@link buildScalarClause}'s
184
+ * emissions exactly. Both resolve the value's shape via the shared
185
+ * {@link classifyScalarForSql}, so a cache HIT binds each `$N` to the value
186
+ * the cached SQL expects. A JSON/array-shaped value on a non-JSON/array column
187
+ * (`jsonThrow`/`arrayThrow`) falls through to the equality path here, the
188
+ * same fall-through the collect path has always taken (the build path's typed
189
+ * error there is only reachable on a MISS, before anything is cached).
190
+ */
191
+ function collectScalarParams(qi, key, value, params) {
192
+ const rawColumn = qi.toColumn(key);
193
+ const cls = (0, where_compile_js_1.classifyScalarForSql)(qi.whereHost, rawColumn, value);
194
+ switch (cls.kind) {
195
+ case 'null':
196
+ // IS NULL is parameterless.
197
+ return;
198
+ case 'vector':
199
+ // Validate the same way the build path does so the collect path never
200
+ // diverges (it would throw before any param was pushed).
201
+ vectorOperator(qi, key, rawColumn, value.distance.metric);
202
+ collectVectorFilterParams(qi, key, rawColumn, value, params);
203
+ return;
204
+ case 'json':
205
+ collectJsonFilterParams(qi, value, params, qi.q(rawColumn));
206
+ return;
207
+ case 'array':
208
+ collectArrayFilterParams(qi, value, params);
209
+ return;
210
+ case 'textsearch':
211
+ params.push(value.search);
212
+ return;
213
+ case 'operator':
214
+ collectOperatorParams(qi, rawColumn, value, params, {
215
+ meta: qi.tableMeta,
216
+ table: qi.table,
217
+ prefix: '',
218
+ });
219
+ return;
220
+ default:
221
+ // 'equality' | 'jsonThrow' | 'arrayThrow': same strict validation as
222
+ // the build path, so a cache hit can never silently bind a
223
+ // misspelled-operator object.
224
+ assertBindableEqualityValue(qi, rawColumn, value, getColumnPgType(qi, rawColumn), qi.table);
225
+ params.push(value);
226
+ return;
227
+ }
228
+ }
229
+ /**
230
+ * Param-collect mirror of {@link buildRelationFilter} for one relation-filter
231
+ * object (`{ some/every/none/is/isNot }`, already normalized). Pushes, per
232
+ * present branch and in the canonical order some→none→every→is→isNot, the
233
+ * branch's sub-where params THEN the target table's global-filter params —
234
+ * exactly the order buildRelationFilter emits. When no global filter applies
235
+ * the gf calls are no-ops, so this stays byte-identical to the pre-0.28 path.
236
+ * Shared by every collect site that mirrors buildRelationFilter
237
+ * (collectWhereParams, collectRelFilterParams, collectAliasWhereParams).
238
+ */
239
+ function collectRelationFilterParams(qi, relDef, filterObj, params) {
240
+ const target = relDef.to;
241
+ if (filterObj.some !== undefined && filterObj.some !== null) {
242
+ collectRelFilterParams(qi, target, filterObj.some, params);
243
+ collectTargetGlobalFilterExists(qi, target, params);
244
+ }
245
+ if (filterObj.none !== undefined && filterObj.none !== null) {
246
+ collectRelFilterParams(qi, target, filterObj.none, params);
247
+ collectTargetGlobalFilterExists(qi, target, params);
248
+ }
249
+ if (filterObj.every !== undefined && filterObj.every !== null) {
250
+ // gf is only emitted (build) when the `every` sub-where compiles to a
251
+ // filter — otherwise `every` is trivially true and no subquery is built.
252
+ if (buildSubWhereForRelation(qi, target, filterObj.every, []) !== null) {
253
+ collectRelFilterParams(qi, target, filterObj.every, params);
254
+ collectTargetGlobalFilterExists(qi, target, params);
255
+ }
256
+ }
257
+ if (filterObj.is !== undefined) {
258
+ if (filterObj.is !== null)
259
+ collectRelFilterParams(qi, target, filterObj.is, params);
260
+ collectTargetGlobalFilterExists(qi, target, params);
261
+ }
262
+ if (filterObj.isNot !== undefined) {
263
+ if (filterObj.isNot !== null)
264
+ collectRelFilterParams(qi, target, filterObj.isNot, params);
265
+ collectTargetGlobalFilterExists(qi, target, params);
266
+ }
267
+ }
268
+ function collectRelFilterParams(qi, targetTable, subWhere, params) {
269
+ const meta = qi.schema.tables[targetTable];
270
+ if (!meta)
271
+ return;
272
+ collectScopedWhereParams(qi, relationWhereScope(qi, targetTable, meta), subWhere, params);
273
+ }
274
+ /**
275
+ * Collect params from operator clauses. Mirrors buildOperatorClauses:
276
+ * {@link ColumnRef} values compile into the SQL text, so they push NOTHING -
277
+ * but they re-run the same validation (unknown ref / insensitive mode) so a
278
+ * warmed cache can never skip a check the build path enforces.
279
+ */
280
+ function collectOperatorParams(qi, column, op, params, refCtx) {
281
+ const skipRef = (v) => {
282
+ if (!(0, filters_js_1.isColumnRef)(v))
283
+ return false;
284
+ if (refCtx)
285
+ resolveColumnRef(qi, v, refCtx, op.mode);
286
+ return true;
287
+ };
288
+ if (op.equals !== undefined && op.equals !== null && !skipRef(op.equals)) {
289
+ (0, filters_js_1.assertBindableEqualsOperand)(op.equals, `"${column}"`);
290
+ params.push(op.equals);
291
+ }
292
+ if (op.gt !== undefined && !skipRef(op.gt))
293
+ params.push(op.gt);
294
+ if (op.gte !== undefined && !skipRef(op.gte))
295
+ params.push(op.gte);
296
+ if (op.lt !== undefined && !skipRef(op.lt))
297
+ params.push(op.lt);
298
+ if (op.lte !== undefined && !skipRef(op.lte))
299
+ params.push(op.lte);
300
+ if (op.not !== undefined && op.not !== null && !skipRef(op.not))
301
+ params.push(op.not);
302
+ if (op.in !== undefined)
303
+ params.push(qi.inParam(op.in));
304
+ if (op.notIn !== undefined)
305
+ params.push(qi.inParam(op.notIn));
306
+ if (op.contains !== undefined)
307
+ params.push(`%${(0, utils_js_1.escapeLike)(op.contains)}%`);
308
+ if (op.startsWith !== undefined)
309
+ params.push(`${(0, utils_js_1.escapeLike)(op.startsWith)}%`);
310
+ if (op.endsWith !== undefined)
311
+ params.push(`%${(0, utils_js_1.escapeLike)(op.endsWith)}`);
312
+ }
313
+ /**
314
+ * Collect params from JSON filter. Mirrors buildJsonFilterClauses exactly:
315
+ * the `path` is bound at most once (its placeholder is shared by every
316
+ * extraction clause), then equals/contains/hasKey values, then the range
317
+ * comparison values in {@link JSON_RANGE_OPERATORS} order.
318
+ */
319
+ function collectJsonFilterParams(qi, filter, params, column) {
320
+ let pathPushed = false;
321
+ const pushPathOnce = () => {
322
+ if (!pathPushed) {
323
+ // Only reached when a path-requiring clause validated filter.path.
324
+ params.push(jsonPathParam(qi, filter.path, filter.path));
325
+ pathPushed = true;
326
+ }
327
+ };
328
+ if (filter.path !== undefined && filter.equals !== undefined) {
329
+ pushPathOnce();
330
+ params.push(String(filter.equals));
331
+ }
332
+ else if (filter.equals !== undefined) {
333
+ params.push(JSON.stringify(filter.equals));
334
+ }
335
+ if (filter.contains !== undefined) {
336
+ params.push(JSON.stringify(filter.contains));
337
+ }
338
+ if (filter.hasKey !== undefined) {
339
+ params.push(filter.hasKey);
340
+ }
341
+ for (const { value } of jsonRangeEntries(qi, filter, column)) {
342
+ pushPathOnce();
343
+ params.push(value);
344
+ }
345
+ }
346
+ /** Collect params from array filter. Mirrors buildArrayFilterClauses. */
347
+ function collectArrayFilterParams(_qi, filter, params) {
348
+ if (filter.has !== undefined)
349
+ params.push(filter.has);
350
+ if (filter.hasEvery !== undefined)
351
+ params.push(filter.hasEvery);
352
+ if (filter.hasSome !== undefined)
353
+ params.push(filter.hasSome);
354
+ // isEmpty has no params (IS NULL / IS NOT NULL)
355
+ }
356
+ /**
357
+ * Collect params for a vector distance WHERE filter. Mirrors
358
+ * {@link buildVectorFilterClauses}: the `$n::vector` query vector first, then
359
+ * the comparison threshold(s).
360
+ */
361
+ function collectVectorFilterParams(qi, field, rawColumn, filter, params) {
362
+ const dist = filter.distance;
363
+ pushVectorParam(qi, field, rawColumn, dist.to, params);
364
+ for (const cmp of Object.keys(filters_js_1.VECTOR_DISTANCE_COMPARATORS)) {
365
+ const threshold = dist[cmp];
366
+ if (threshold !== undefined)
367
+ params.push(threshold);
368
+ }
369
+ }
370
+ /** Build WHERE clause from a where object (supports operators, NULL, OR) */
371
+ function buildWhere(qi, where) {
372
+ const params = [];
373
+ const clause = buildWhereClause(qi, where, params);
374
+ if (!clause)
375
+ return { sql: '', params: [] };
376
+ return { sql: ` WHERE ${clause}`, params };
377
+ }
378
+ /**
379
+ * Resolve the configured global filter for `table`, evaluating a function
380
+ * filter, honoring the active query's `skipGlobalFilters`. Returns `null` when
381
+ * no filter applies, the query opted out, or the filter is empty.
382
+ */
383
+ function resolveGlobalFilter(qi, table, skip = qi.currentSkip) {
384
+ const filters = qi.globalFilters;
385
+ if (!filters)
386
+ return null;
387
+ if (skip === true)
388
+ return null;
389
+ if (Array.isArray(skip) && skip.includes(table))
390
+ return null;
391
+ const raw = filters[table];
392
+ if (raw === undefined)
393
+ return null;
394
+ const resolved = typeof raw === 'function' ? raw() : raw;
395
+ if (resolved === null || resolved === undefined)
396
+ return null;
397
+ const obj = resolved;
398
+ // An all-undefined filter (e.g. `{ tenantId: undefined }`) contributes
399
+ // nothing — treat it as absent so it never emits a dangling clause.
400
+ if (Object.keys(obj).every((k) => obj[k] === undefined))
401
+ return null;
402
+ return obj;
403
+ }
404
+ /**
405
+ * AND-merge this table's resolved global filter into a user `where`. Either
406
+ * side may be absent. When no filter applies the user where is returned by
407
+ * reference, so fingerprints/SQL stay byte-identical to the pre-0.28 path.
408
+ */
409
+ function mergeGlobalFilter(qi, userWhere) {
410
+ const gf = resolveGlobalFilter(qi, qi.table);
411
+ if (!gf)
412
+ return userWhere;
413
+ if (userWhere === undefined)
414
+ return gf;
415
+ return { AND: [userWhere, gf] };
416
+ }
417
+ /**
418
+ * SQL clause for `targetTable`'s global filter rendered against `alias`
419
+ * (relation subqueries, `_count`, relation `orderBy`). Pushes its params to
420
+ * `params`; returns `''` when no filter applies. Mirror:
421
+ * {@link collectTargetGlobalFilterAlias}.
422
+ */
423
+ function targetGlobalFilterAlias(qi, targetTable, alias, params) {
424
+ const gf = resolveGlobalFilter(qi, targetTable);
425
+ if (!gf)
426
+ return '';
427
+ const meta = qi.schema.tables[targetTable];
428
+ if (!meta)
429
+ return '';
430
+ return buildAliasWhere(qi, targetTable, meta, alias, gf, params) ?? '';
431
+ }
432
+ /** Param-collect mirror of {@link targetGlobalFilterAlias}. */
433
+ function collectTargetGlobalFilterAlias(qi, targetTable, params) {
434
+ const gf = resolveGlobalFilter(qi, targetTable);
435
+ if (!gf)
436
+ return;
437
+ const meta = qi.schema.tables[targetTable];
438
+ if (!meta)
439
+ return;
440
+ collectAliasWhereParams(qi, targetTable, meta, gf, params);
441
+ }
442
+ /**
443
+ * SQL clause for `targetTable`'s global filter rendered against the bare
444
+ * (unaliased) table name — the form used inside relation-filter `EXISTS`
445
+ * subqueries. Pushes its params; `''` when none. Mirror:
446
+ * {@link collectTargetGlobalFilterExists}.
447
+ */
448
+ function targetGlobalFilterExists(qi, targetTable, params) {
449
+ const gf = resolveGlobalFilter(qi, targetTable);
450
+ if (!gf)
451
+ return '';
452
+ return buildSubWhereForRelation(qi, targetTable, gf, params) ?? '';
453
+ }
454
+ /** Param-collect mirror of {@link targetGlobalFilterExists}. */
455
+ function collectTargetGlobalFilterExists(qi, targetTable, params) {
456
+ const gf = resolveGlobalFilter(qi, targetTable);
457
+ if (!gf)
458
+ return;
459
+ collectRelFilterParams(qi, targetTable, gf, params);
460
+ }
461
+ /**
462
+ * Value-invariant SQL-cache-key segment for the active global-filter
463
+ * environment. Relation-subquery / relation-filter / `_count` / relation-
464
+ * `orderBy` global filters are rendered at build time but their SHAPE is not
465
+ * otherwise in the where/with fingerprint, so this segment guards the cache:
466
+ * two different filter shapes never collide on one cached SQL text, while two
467
+ * function-filter results of the SAME shape (differing only in values) share
468
+ * the entry and bind their own params. Empty (`''`) when no filter applies, so
469
+ * cache keys stay byte-identical when the feature is unused.
470
+ */
471
+ function globalFilterCacheSegment(qi) {
472
+ const filters = qi.globalFilters;
473
+ if (!filters)
474
+ return '';
475
+ const parts = [];
476
+ for (const table of Object.keys(filters).sort()) {
477
+ // Function filters for OTHER tables may be request-scoped closures that
478
+ // throw outside their own context; a query on an unrelated table must not
479
+ // break on them. A throwing filter can't have contributed SQL to this
480
+ // query either (merging it would have thrown first), so a constant
481
+ // marker keeps the key shape-distinct without evaluating it.
482
+ let gf;
483
+ try {
484
+ gf = resolveGlobalFilter(qi, table);
485
+ }
486
+ catch {
487
+ parts.push(`${table}:!`);
488
+ continue;
489
+ }
490
+ if (gf) {
491
+ // Fingerprint with the FILTER's own table host, not the root table's:
492
+ // classifying another table's filter with the root host can mistake a
493
+ // relation filter for a scalar object and collide two shapes that
494
+ // compile to different SQL. Unknown meta falls back to the root host
495
+ // (same as scoped sub-wheres against an unknown target).
496
+ const meta = qi.schema.tables[table];
497
+ const host = meta && meta.name !== qi.tableMeta.name ? scopedWhereHost(qi, meta) : undefined;
498
+ parts.push(`${table}:${host ? fingerprintScopedWhere(qi, host, gf) : fingerprintWhere(qi, gf)}`);
499
+ }
500
+ }
501
+ return parts.length ? `|gf=${parts.join(';')}` : '';
502
+ }
503
+ /**
504
+ * True when the USER-supplied `where` compiles to no predicate (`{}`,
505
+ * `{ id: undefined }`, `{ OR: [{ a: undefined }] }`, …). This is the exact
506
+ * signal the empty-`where` guard needs — the compiled emptiness, NOT the
507
+ * fingerprint (which is non-empty for an all-undefined `OR`/`AND`). It ignores
508
+ * any configured global filter, so a global filter never lets an unguarded
509
+ * mass mutation through.
510
+ */
511
+ function userPredicateIsEmpty(qi, userWhere) {
512
+ const throwaway = [];
513
+ return buildWhereClause(qi, userWhere, throwaway) === null;
514
+ }
515
+ function assertMutationHasPredicate(qi, operation, whereSql, allowFullTableScan) {
516
+ if (whereSql.length > 0)
517
+ return;
518
+ if (allowFullTableScan === true)
519
+ return;
520
+ throw new errors_js_1.ValidationError(`[turbine] ${operation} on "${qi.table}" refused: the \`where\` clause is empty. ` +
521
+ `Pass \`allowFullTableScan: true\` to opt in, or check that your filter values are defined.`);
522
+ }
523
+ /**
524
+ * Build the inner WHERE expression (without the WHERE keyword).
525
+ * Returns null if no conditions exist.
526
+ * Supports: equality, operators, NULL, OR, AND, NOT, relation filters (some/every/none).
527
+ */
528
+ function buildWhereClause(qi, where, params) {
529
+ const andClauses = [];
530
+ // ONE canonical walk (shared with fingerprintWhere + collectWhereParams).
531
+ for (const event of (0, where_compile_js_1.walkWhere)(qi.whereHost, where)) {
532
+ switch (event.kind) {
533
+ case 'or': {
534
+ const orClauses = [];
535
+ for (const orCond of event.conditions) {
536
+ const sub = buildWhereClause(qi, orCond, params);
537
+ if (sub)
538
+ orClauses.push(sub);
539
+ }
540
+ if (orClauses.length > 0)
541
+ andClauses.push(`(${orClauses.join(' OR ')})`);
542
+ break;
543
+ }
544
+ case 'and':
545
+ for (const andCond of event.conditions) {
546
+ const sub = buildWhereClause(qi, andCond, params);
547
+ if (sub)
548
+ andClauses.push(sub);
549
+ }
550
+ break;
551
+ case 'not': {
552
+ const sub = buildWhereClause(qi, event.condition, params);
553
+ if (sub)
554
+ andClauses.push(`NOT (${sub})`);
555
+ break;
556
+ }
557
+ case 'relation': {
558
+ // { posts: { some: { published: true } } } → EXISTS / NOT EXISTS
559
+ const relClause = buildRelationFilter(qi, event.key, event.relDef, event.filterObj, params);
560
+ if (relClause)
561
+ andClauses.push(relClause);
562
+ break;
563
+ }
564
+ case 'scalar':
565
+ buildScalarClause(qi, event.key, event.value, params, andClauses);
566
+ break;
567
+ }
568
+ }
569
+ if (andClauses.length === 0)
570
+ return null;
571
+ return andClauses.join(' AND ');
572
+ }
573
+ /**
574
+ * Emit the SQL clause(s) for one scalar WHERE key onto `andClauses`, pushing
575
+ * any params. The shape decision comes from the shared
576
+ * {@link classifyScalarForSql} so {@link collectScalarParams} pushes an
577
+ * identical param list on a cache hit. The `*Throw` branches preserve the
578
+ * strict-validation errors for a JSON/array operator on the wrong column type.
579
+ */
580
+ function buildScalarClause(qi, key, value, params, andClauses) {
581
+ const rawColumn = qi.toColumn(key);
582
+ const column = qi.q(rawColumn);
583
+ const cls = (0, where_compile_js_1.classifyScalarForSql)(qi.whereHost, rawColumn, value);
584
+ switch (cls.kind) {
585
+ case 'null':
586
+ andClauses.push(`${column} IS NULL`);
587
+ return;
588
+ case 'vector':
589
+ andClauses.push(...buildVectorFilterClauses(qi, key, rawColumn, value, params));
590
+ return;
591
+ case 'json':
592
+ andClauses.push(...buildJsonFilterClauses(qi, column, value, params));
593
+ return;
594
+ case 'jsonThrow':
595
+ // A JSON-only operator on a non-JSON column was almost certainly a typo
596
+ // or schema mismatch. `contains`/`equals` are shared with WhereOperator
597
+ // (LIKE / equality), so only shape-unique keys reach here.
598
+ throw new errors_js_1.ValidationError(`[turbine] Column "${rawColumn}" on table "${qi.table}" is not a JSON column ` +
599
+ `(actual type: ${getColumnPgType(qi, rawColumn)}); cannot apply JSON operator '${cls.jsonKey}'.`);
600
+ case 'array':
601
+ andClauses.push(...buildArrayFilterClauses(qi, column, value, params, cls.colType));
602
+ return;
603
+ case 'arrayThrow':
604
+ throw new errors_js_1.ValidationError(`[turbine] Column "${rawColumn}" on table "${qi.table}" is not an array column ` +
605
+ `(actual type: ${getColumnPgType(qi, rawColumn)}); cannot apply array operator '${cls.arrayKey}'.`);
606
+ case 'textsearch':
607
+ andClauses.push(buildTextSearchClause(qi, column, value, params));
608
+ return;
609
+ case 'operator':
610
+ andClauses.push(...buildOperatorClauses(qi, column, value, params, {
611
+ meta: qi.tableMeta,
612
+ table: qi.table,
613
+ prefix: '',
614
+ }));
615
+ return;
616
+ default:
617
+ // 'equality': a plain object literal that matched no known filter shape
618
+ // is almost always a misspelled operator (`startWith` for `startsWith`);
619
+ // the guard also runs on the cache-hit param-collect path.
620
+ assertBindableEqualityValue(qi, rawColumn, value, getColumnPgType(qi, rawColumn), qi.table);
621
+ params.push(value);
622
+ andClauses.push(`${column} = ${qi.p(params.length)}`);
623
+ return;
624
+ }
625
+ }
626
+ /**
627
+ * A {@link WhereHost} with no relations — used to fingerprint a sub-where
628
+ * whose target table is unknown (`schema.tables[t]` miss). `walkWhere` reads
629
+ * only `tableMeta.relations`, so every key falls to the scalar path, matching
630
+ * the pre-unification `meta?.relations` short-circuit.
631
+ */
632
+ function emptyRelationsHost(qi, table) {
633
+ return {
634
+ tableMeta: { name: table, relations: {} },
635
+ normalizeRelationFilter: (relDef, filterObj) => normalizeRelationFilter(qi, relDef, filterObj),
636
+ getColumnPgType: () => 'text',
637
+ isJsonColumnType: (colType) => isJsonColumnType(qi, colType),
638
+ };
639
+ }
640
+ function scopedWhereHost(qi, meta) {
641
+ let host = qi.scopedHostCache.get(meta.name);
642
+ if (!host) {
643
+ host = {
644
+ tableMeta: meta,
645
+ normalizeRelationFilter: (relDef, filterObj) => normalizeRelationFilter(qi, relDef, filterObj),
646
+ getColumnPgType: (column) => pgTypeForColumn(qi, meta, column),
647
+ isJsonColumnType: (colType) => isJsonColumnType(qi, colType),
648
+ };
649
+ qi.scopedHostCache.set(meta.name, host);
650
+ }
651
+ return host;
652
+ }
653
+ /** Build the scope for a relation-filter EXISTS sub-where over the bare target table. */
654
+ function relationWhereScope(qi, targetTable, meta) {
655
+ return {
656
+ meta,
657
+ table: targetTable,
658
+ qualifier: `${qi.q(targetTable)}.`,
659
+ relationParent: targetTable,
660
+ host: scopedWhereHost(qi, meta),
661
+ unknownColumn: (field) => new errors_js_1.ValidationError(`[turbine] Unknown field "${field}" in relation filter for table "${targetTable}". ` +
662
+ `Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`),
663
+ };
664
+ }
665
+ /** Build the scope for a relation `with`-clause `where` compiled against `alias`. */
666
+ function aliasWhereScope(qi, targetTable, meta, alias) {
667
+ return {
668
+ meta,
669
+ table: targetTable,
670
+ qualifier: `${alias}.`,
671
+ relationParent: alias,
672
+ host: scopedWhereHost(qi, meta),
673
+ unknownColumn: (field) => new errors_js_1.ValidationError(`[turbine] Unknown column "${field}" in where for table "${targetTable}"`),
674
+ };
675
+ }
676
+ /**
677
+ * Compile a scoped sub-where to SQL. Serves BOTH the relation-filter EXISTS
678
+ * body ({@link buildSubWhereForRelation}) and the relation `with`-clause
679
+ * `where` ({@link buildAliasWhere}) — the emitted SQL is byte-identical to the
680
+ * former hand-mirrored walkers, since it renders the same clauses in the same
681
+ * ({@link walkWhere}-canonical) key order.
682
+ */
683
+ function buildScopedWhere(qi, scope, where, params) {
684
+ const clauses = [];
685
+ for (const event of (0, where_compile_js_1.walkWhere)(scope.host, where)) {
686
+ switch (event.kind) {
687
+ case 'or':
688
+ case 'and': {
689
+ const parts = event.conditions
690
+ .map((cond) => buildScopedWhere(qi, scope, cond, params))
691
+ .filter((s) => s !== null)
692
+ .map((s) => `(${s})`);
693
+ if (parts.length > 0)
694
+ clauses.push(`(${parts.join(event.kind === 'or' ? ' OR ' : ' AND ')})`);
695
+ break;
696
+ }
697
+ case 'not': {
698
+ const sub = buildScopedWhere(qi, scope, event.condition, params);
699
+ if (sub)
700
+ clauses.push(`NOT (${sub})`);
701
+ break;
702
+ }
703
+ case 'relation': {
704
+ const c = buildRelationFilter(qi, event.key, event.relDef, event.filterObj, params, scope.relationParent);
705
+ if (c)
706
+ clauses.push(c);
707
+ break;
708
+ }
709
+ case 'scalar':
710
+ buildScopedScalarClause(qi, scope, event.key, event.value, params, clauses);
711
+ break;
712
+ }
713
+ }
714
+ return clauses.length > 0 ? clauses.join(' AND ') : null;
715
+ }
716
+ /**
717
+ * Emit the SQL clause(s) for one scalar key of a scoped sub-where. Reproduces
718
+ * the null / JSON / array / operator / equality fall-through both former
719
+ * walkers shared (relation sub-wheres and alias wheres carry no vector or
720
+ * text-search scalar surface, so — unlike the top-level {@link buildScalarClause}
721
+ * — those shapes are not special-cased here and keep their historical
722
+ * equality-guard behavior).
723
+ */
724
+ function buildScopedScalarClause(qi, scope, field, value, params, clauses) {
725
+ const meta = scope.meta;
726
+ const col = meta.columnMap[field] ?? (0, schema_js_1.camelToSnake)(field);
727
+ if (!meta.allColumns.includes(col))
728
+ throw scope.unknownColumn(field);
729
+ const qCol = `${scope.qualifier}${qi.q(col)}`;
730
+ if (value === null) {
731
+ clauses.push(`${qCol} IS NULL`);
732
+ return;
733
+ }
734
+ if (typeof value === 'object' && !Array.isArray(value) && (0, filters_js_1.isJsonFilter)(value)) {
735
+ const colType = pgTypeForColumn(qi, meta, col);
736
+ if (isJsonColumnType(qi, colType)) {
737
+ clauses.push(...buildJsonFilterClauses(qi, qCol, value, params));
738
+ return;
739
+ }
740
+ const jsonKey = (0, filters_js_1.findJsonUniqueKey)(value);
741
+ if (jsonKey) {
742
+ throw new errors_js_1.ValidationError(`[turbine] Column "${col}" on table "${scope.table}" is not a JSON column ` +
743
+ `(actual type: ${colType}); cannot apply JSON operator '${jsonKey}'.`);
744
+ }
745
+ }
746
+ if (typeof value === 'object' && !Array.isArray(value) && (0, filters_js_1.isArrayFilter)(value)) {
747
+ const colType = pgTypeForColumn(qi, meta, col);
748
+ if (colType.startsWith('_')) {
749
+ clauses.push(...buildArrayFilterClauses(qi, qCol, value, params, colType));
750
+ return;
751
+ }
752
+ const arrayKey = (0, filters_js_1.findArrayUniqueKey)(value);
753
+ if (arrayKey) {
754
+ throw new errors_js_1.ValidationError(`[turbine] Column "${col}" on table "${scope.table}" is not an array column ` +
755
+ `(actual type: ${colType}); cannot apply array operator '${arrayKey}'.`);
756
+ }
757
+ }
758
+ if ((0, filters_js_1.isWhereOperator)(value)) {
759
+ clauses.push(...buildOperatorClauses(qi, qCol, value, params, {
760
+ meta,
761
+ table: scope.table,
762
+ prefix: scope.qualifier,
763
+ }));
764
+ return;
765
+ }
766
+ assertBindableEqualityValue(qi, col, value, pgTypeForColumn(qi, meta, col), scope.table);
767
+ params.push(value);
768
+ clauses.push(`${qCol} = ${qi.p(params.length)}`);
769
+ }
770
+ /**
771
+ * Cache-hit param-collect mirror of {@link buildScopedWhere}: pushes the exact
772
+ * same params in the exact same order (driven by the same {@link walkWhere}),
773
+ * without rebuilding SQL. Serves both {@link collectRelFilterParams} and
774
+ * {@link collectAliasWhereParams}.
775
+ */
776
+ function collectScopedWhereParams(qi, scope, where, params) {
777
+ for (const event of (0, where_compile_js_1.walkWhere)(scope.host, where)) {
778
+ switch (event.kind) {
779
+ case 'or':
780
+ case 'and':
781
+ for (const cond of event.conditions)
782
+ collectScopedWhereParams(qi, scope, cond, params);
783
+ break;
784
+ case 'not':
785
+ collectScopedWhereParams(qi, scope, event.condition, params);
786
+ break;
787
+ case 'relation':
788
+ // Same some→none→every→is→isNot (each: sub-where params then target
789
+ // global-filter params) as buildRelationFilter emits.
790
+ collectRelationFilterParams(qi, event.relDef, event.filterObj, params);
791
+ break;
792
+ case 'scalar':
793
+ collectScopedScalarParams(qi, scope, event.key, event.value, params);
794
+ break;
795
+ }
796
+ }
797
+ }
798
+ /** Param-collect mirror of {@link buildScopedScalarClause}. */
799
+ function collectScopedScalarParams(qi, scope, field, value, params) {
800
+ if (value === null)
801
+ return;
802
+ const meta = scope.meta;
803
+ const col = meta.columnMap[field] ?? (0, schema_js_1.camelToSnake)(field);
804
+ if (typeof value === 'object' && !Array.isArray(value) && (0, filters_js_1.isJsonFilter)(value)) {
805
+ const colType = pgTypeForColumn(qi, meta, col);
806
+ if (isJsonColumnType(qi, colType)) {
807
+ collectJsonFilterParams(qi, value, params, `${qi.q(scope.table)}.${qi.q(col)}`);
808
+ return;
809
+ }
810
+ }
811
+ if (typeof value === 'object' && !Array.isArray(value) && (0, filters_js_1.isArrayFilter)(value)) {
812
+ const colType = pgTypeForColumn(qi, meta, col);
813
+ if (colType.startsWith('_')) {
814
+ collectArrayFilterParams(qi, value, params);
815
+ return;
816
+ }
817
+ }
818
+ if ((0, filters_js_1.isWhereOperator)(value)) {
819
+ collectOperatorParams(qi, col, value, params, { meta, table: scope.table, prefix: '' });
820
+ return;
821
+ }
822
+ assertBindableEqualityValue(qi, col, value, pgTypeForColumn(qi, meta, col), scope.table);
823
+ params.push(value);
824
+ }
825
+ /**
826
+ * Value-invariant fingerprint of a scoped sub-where. Same canonical
827
+ * {@link walkWhere} as {@link fingerprintWhere}, so two shapes that compile to
828
+ * different SQL never collide on one cached SQL string. Serves both
829
+ * {@link fingerprintRelFilter} and {@link fingerprintAliasWhere}. Fingerprint
830
+ * bytes are process-local cache keys (never persisted), so their exact text
831
+ * may differ from the pre-unification walkers as long as collisions stay
832
+ * impossible.
833
+ */
834
+ function fingerprintScopedWhere(qi, host, where) {
835
+ const parts = [];
836
+ for (const event of (0, where_compile_js_1.walkWhere)(host, where)) {
837
+ switch (event.kind) {
838
+ case 'or':
839
+ parts.push(`OR[${event.conditions.map((c) => fingerprintScopedWhere(qi, host, c)).join(',')}]`);
840
+ break;
841
+ case 'and':
842
+ parts.push(`AND[${event.conditions.map((c) => fingerprintScopedWhere(qi, host, c)).join(',')}]`);
843
+ break;
844
+ case 'not':
845
+ parts.push(`NOT(${fingerprintScopedWhere(qi, host, event.condition)})`);
846
+ break;
847
+ case 'relation':
848
+ parts.push(`${event.key}:{${fingerprintRelationParts(qi, event.relDef, event.filterObj).join(',')}}`);
849
+ break;
850
+ case 'scalar':
851
+ parts.push(`${event.key}:${(0, where_compile_js_1.fingerprintScalarToken)(event.value)}`);
852
+ break;
853
+ }
854
+ }
855
+ return parts.join('&');
856
+ }
857
+ /**
858
+ * Build relation filter SQL: WHERE EXISTS / NOT EXISTS subquery
859
+ * Supports: some (EXISTS), every (NOT EXISTS ... NOT), none (NOT EXISTS)
860
+ */
861
+ function buildRelationFilter(qi, _relName, relDef, filterObj, params, parentTable) {
862
+ const targetTable = relDef.to;
863
+ const targetMeta = qi.schema.tables[targetTable];
864
+ if (!targetMeta)
865
+ return null;
866
+ const qt = qi.q(targetTable);
867
+ const qSelf = qi.q(parentTable ?? qi.table);
868
+ const clauses = [];
869
+ // Correlation: link child table to parent table (supports composite FKs)
870
+ let correlation;
871
+ if (relDef.type === 'hasMany' || relDef.type === 'hasOne') {
872
+ // parent.pk = child.fk
873
+ correlation = qi.dialect.buildCorrelation(qt, relDef.foreignKey, qSelf, relDef.referenceKey);
874
+ }
875
+ else {
876
+ // belongsTo: parent.fk = child.pk
877
+ correlation = qi.dialect.buildCorrelation(qt, relDef.referenceKey, qSelf, relDef.foreignKey);
878
+ }
879
+ // The target table's global filter (soft-delete / tenancy) restricts the
880
+ // DOMAIN of correlated rows in EVERY branch: `some`/`none`/`is`/`isNot`
881
+ // ignore filtered-out rows, and `every` quantifies over only the surviving
882
+ // rows ("every NON-deleted related row matches P"). It is ANDed into the
883
+ // correlation and its params pushed AFTER the per-branch filter — mirrored
884
+ // exactly in collectWhereParams' relation-filter branch. `qt` is the bare
885
+ // target table, matching the `FROM ${qt}` here (see targetGlobalFilterExists).
886
+ const gfAnd = () => {
887
+ const gf = targetGlobalFilterExists(qi, targetTable, params);
888
+ return gf ? ` AND ${gf}` : '';
889
+ };
890
+ // "some": EXISTS (SELECT 1 FROM target WHERE correlation AND filter AND gf)
891
+ // A `null` branch is skipped (never reaches buildSubWhereForRelation, which
892
+ // would throw on Object.keys(null)), matching collectRelationFilterParams,
893
+ // which also skips null. Unreachable via normalization today, guarded anyway.
894
+ if (filterObj.some !== undefined && filterObj.some !== null) {
895
+ const subWhere = filterObj.some;
896
+ const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params);
897
+ const filterAnd = filterClause ? ` AND ${filterClause}` : '';
898
+ clauses.push(`EXISTS (SELECT 1 FROM ${qt} WHERE ${correlation}${filterAnd}${gfAnd()})`);
899
+ }
900
+ // "none": NOT EXISTS (SELECT 1 FROM target WHERE correlation AND filter AND gf)
901
+ if (filterObj.none !== undefined && filterObj.none !== null) {
902
+ const subWhere = filterObj.none;
903
+ const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params);
904
+ const filterAnd = filterClause ? ` AND ${filterClause}` : '';
905
+ clauses.push(`NOT EXISTS (SELECT 1 FROM ${qt} WHERE ${correlation}${filterAnd}${gfAnd()})`);
906
+ }
907
+ // "every": NOT EXISTS (SELECT 1 FROM target WHERE correlation AND gf AND NOT (filter))
908
+ if (filterObj.every !== undefined && filterObj.every !== null) {
909
+ const subWhere = filterObj.every;
910
+ const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params);
911
+ if (filterClause) {
912
+ // gf params pushed AFTER filter params (collect mirrors this order), but
913
+ // placed textually inside the domain so it restricts which rows count.
914
+ const gf = gfAnd();
915
+ clauses.push(`NOT EXISTS (SELECT 1 FROM ${qt} WHERE ${correlation}${gf} AND NOT (${filterClause}))`);
916
+ }
917
+ else {
918
+ // "every" with empty filter = true (all match trivially) — gf irrelevant.
919
+ }
920
+ }
921
+ // "is": EXISTS — for to-one relations (same SQL as "some").
922
+ // `is: null` = "no related row" (Prisma semantics) → NOT EXISTS.
923
+ if (filterObj.is !== undefined) {
924
+ if (filterObj.is === null) {
925
+ clauses.push(`NOT EXISTS (SELECT 1 FROM ${qt} WHERE ${correlation}${gfAnd()})`);
926
+ }
927
+ else {
928
+ const subWhere = filterObj.is;
929
+ const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params);
930
+ const filterAnd = filterClause ? ` AND ${filterClause}` : '';
931
+ clauses.push(`EXISTS (SELECT 1 FROM ${qt} WHERE ${correlation}${filterAnd}${gfAnd()})`);
932
+ }
933
+ }
934
+ // "isNot": NOT EXISTS — for to-one relations (same SQL as "none").
935
+ // `isNot: null` = "a related row exists" → EXISTS.
936
+ if (filterObj.isNot !== undefined) {
937
+ if (filterObj.isNot === null) {
938
+ clauses.push(`EXISTS (SELECT 1 FROM ${qt} WHERE ${correlation}${gfAnd()})`);
939
+ }
940
+ else {
941
+ const subWhere = filterObj.isNot;
942
+ const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params);
943
+ const filterAnd = filterClause ? ` AND ${filterClause}` : '';
944
+ clauses.push(`NOT EXISTS (SELECT 1 FROM ${qt} WHERE ${correlation}${filterAnd}${gfAnd()})`);
945
+ }
946
+ }
947
+ return clauses.length > 0 ? clauses.join(' AND ') : null;
948
+ }
949
+ /**
950
+ * Build WHERE clause conditions for a relation filter subquery.
951
+ * Uses the target table's column mapping to resolve field names.
952
+ */
953
+ function buildSubWhereForRelation(qi, targetTable, subWhere, params) {
954
+ const meta = qi.schema.tables[targetTable];
955
+ if (!meta)
956
+ return null;
957
+ return buildScopedWhere(qi, relationWhereScope(qi, targetTable, meta), subWhere, params);
958
+ }
959
+ /**
960
+ * Resolve a column's Postgres type from an arbitrary table's metadata
961
+ * (relation targets, not just `qi.table`).
962
+ */
963
+ function pgTypeForColumn(_qi, meta, column) {
964
+ return meta.dialectTypes?.[column] ?? meta.pgTypes?.[column] ?? 'text';
965
+ }
966
+ /**
967
+ * The Postgres enum type name for a column, when the schema knows one.
968
+ *
969
+ * Introspection stores each column's `udt_name` in `pgTypes` and every
970
+ * database enum in `schema.enums` (typname → labels); a column whose type
971
+ * matches an enum key needs an explicit `::"EnumName"` cast on its write
972
+ * binds — bulk-insert forms like `UNNEST($1::text[])` otherwise type the
973
+ * value as text and Postgres refuses the implicit text→enum coercion
974
+ * ("column X is of type Y but expression is of type text").
975
+ *
976
+ * Postgres-only by construction: gated on the active dialect being
977
+ * `postgresql` AND on `schema.enums` having entries (only PG introspection
978
+ * produces them — `defineSchema` and the other engines leave it empty), so
979
+ * SQLite/MySQL/MSSQL/PowDB output is byte-identical.
980
+ */
981
+ function enumTypeForColumn(qi, column) {
982
+ if (qi.dialect.name !== 'postgresql')
983
+ return null;
984
+ const enums = qi.schema.enums;
985
+ if (!enums)
986
+ return null;
987
+ // Cross-schema guard (N-5): introspection records pgTypeSchema ONLY when
988
+ // the column's type lives OUTSIDE the introspected schema. A same-named
989
+ // enum in another schema must not get this schema's cast — search_path
990
+ // would resolve `::"status"` to the wrong type. Skipping the cast restores
991
+ // the pre-cast behavior for such columns. Columns without pgTypeSchema
992
+ // (same-schema types, defineSchema/legacy metadata) keep the cast.
993
+ if (qi.crossSchemaTypeColumns.has(column))
994
+ return null;
995
+ const pgType = qi.columnPgTypeMap.get(column) ?? qi.tableMeta.pgTypes?.[column];
996
+ if (!pgType || pgType.startsWith('_'))
997
+ return null;
998
+ return Object.hasOwn(enums, pgType) ? pgType : null;
999
+ }
1000
+ /**
1001
+ * `::"EnumName"` cast suffix for a write-bind placeholder on an enum
1002
+ * column; `''` for every other column, so non-enum SQL stays byte-identical.
1003
+ * The type name is an introspected identifier and is quoted via the dialect.
1004
+ */
1005
+ function enumCastSuffix(qi, column) {
1006
+ const enumType = enumTypeForColumn(qi, column);
1007
+ return enumType ? `::${qi.q(enumType)}` : '';
1008
+ }
1009
+ /**
1010
+ * Equality-fallthrough guard shared by every SQL-build path AND every
1011
+ * cache-hit param-collect path. A plain object literal that matched no known
1012
+ * filter shape on a non-JSON column is almost always a misspelled operator
1013
+ * (`startWith` for `startsWith`); binding it as `col = $1` silently returns
1014
+ * wrong rows. Class instances (Buffer for bytea, Decimal wrappers, ...) are
1015
+ * legitimate bind values and pass through, as do objects on json/jsonb
1016
+ * columns (object equality).
1017
+ */
1018
+ function assertBindableEqualityValue(qi, rawColumn, value, columnPgType, table) {
1019
+ if (!(0, filters_js_1.isUnmatchedPlainObject)(value))
1020
+ return;
1021
+ if (isJsonColumnType(qi, columnPgType))
1022
+ return;
1023
+ const badKeys = Object.keys(value);
1024
+ throw new errors_js_1.ValidationError(badKeys.length === 0
1025
+ ? `[turbine] Empty filter object on "${rawColumn}" for table "${table}". ` +
1026
+ `Provide a value or an operator like { gt: 1 }.`
1027
+ : `[turbine] Unknown operator${badKeys.length > 1 ? 's' : ''} ` +
1028
+ `${badKeys.map((k) => `"${k}"`).join(', ')} on "${rawColumn}" for table "${table}". ` +
1029
+ `Supported operators: ${[...utils_js_1.OPERATOR_KEYS].join(', ')}.`);
1030
+ }
1031
+ /**
1032
+ * Build the user-supplied `where` filter of a relation `with` clause against
1033
+ * the relation's table alias. Supports the same scalar surface as the
1034
+ * top-level WHERE builder — equality, IS NULL, operator objects (incl.
1035
+ * `mode: 'insensitive'`), and OR/AND/NOT combinators. Unknown operator
1036
+ * objects throw via {@link assertBindableEqualityValue}.
1037
+ *
1038
+ * Param push order MUST mirror {@link collectAliasWhereParams} exactly, or
1039
+ * cache hits and pipeline batching will desync.
1040
+ */
1041
+ function buildAliasWhere(qi, targetTable, targetMeta, alias, where, params) {
1042
+ return buildScopedWhere(qi, aliasWhereScope(qi, targetTable, targetMeta, alias), where, params);
1043
+ }
1044
+ /** Mirrors {@link buildAliasWhere} param-push order for the cache-hit collect path. */
1045
+ function collectAliasWhereParams(qi, targetTable, targetMeta, where, params) {
1046
+ // The alias identifier is irrelevant to param collection (it only shapes SQL
1047
+ // text), so reuse the relation scope's host binding for `targetMeta`.
1048
+ collectScopedWhereParams(qi, aliasWhereScope(qi, targetTable, targetMeta, ''), where, params);
1049
+ }
1050
+ /**
1051
+ * Value-invariant, shape-aware fingerprint for a relation `with` clause's
1052
+ * `where` filter. Must distinguish every SQL shape {@link buildAliasWhere}
1053
+ * can emit — equality vs null vs operator sets vs combinators — or two
1054
+ * differently-shaped wheres would share one cached SQL string.
1055
+ */
1056
+ function fingerprintAliasWhere(qi, where, targetTable) {
1057
+ const meta = targetTable ? qi.schema.tables[targetTable] : undefined;
1058
+ const host = meta ? scopedWhereHost(qi, meta) : emptyRelationsHost(qi, targetTable ?? '');
1059
+ return fingerprintScopedWhere(qi, host, where);
1060
+ }
1061
+ /**
1062
+ * Validate a `{ col }` column reference against its table and return the
1063
+ * resolved snake_case column name. Shared by the SQL-build path
1064
+ * ({@link buildOperatorClauses}) and the cache-hit param-collect path
1065
+ * (`collectOperatorParams`) so both always throw identically: a warmed
1066
+ * cache can never skip the check.
1067
+ */
1068
+ function resolveColumnRef(_qi, ref, ctx, mode) {
1069
+ if (mode === 'insensitive') {
1070
+ throw new errors_js_1.ValidationError(`[turbine] mode: 'insensitive' cannot be combined with a column reference ({ col: "${ref.col}" }). ` +
1071
+ `Case-insensitive column-to-column comparison is not supported: use client.sql\`...\` ` +
1072
+ `for lower(a) = lower(b).`);
1073
+ }
1074
+ const col = ctx.meta.columnMap[ref.col] ?? (0, schema_js_1.camelToSnake)(ref.col);
1075
+ if (!ctx.meta.allColumns.includes(col)) {
1076
+ throw new errors_js_1.ValidationError(`[turbine] Unknown field "${ref.col}" referenced by { col } in where on table "${ctx.table}". ` +
1077
+ `Known fields: ${Object.keys(ctx.meta.columnMap).join(', ') || '(none)'}.`);
1078
+ }
1079
+ return col;
1080
+ }
1081
+ /**
1082
+ * Compile a `{ col }` reference to its quoted, prefix-matched SQL identifier.
1083
+ * NO param is bound: the referenced column is part of the SQL text (and of
1084
+ * the where fingerprint, via `fingerprintOperatorShape` in `filters.ts`).
1085
+ */
1086
+ function columnRefSql(qi, ref, ctx, mode) {
1087
+ if (!ctx) {
1088
+ throw new errors_js_1.ValidationError(`[turbine] Column reference { col: "${ref.col}" } is not supported in this filter context.`);
1089
+ }
1090
+ return `${ctx.prefix}${qi.q(resolveColumnRef(qi, ref, ctx, mode))}`;
1091
+ }
1092
+ /**
1093
+ * Build SQL clauses for a single operator object on a column.
1094
+ * Each operator key becomes its own clause, all ANDed together.
1095
+ *
1096
+ * `equals`/`not`/`gt`/`gte`/`lt`/`lte` also accept a {@link ColumnRef}
1097
+ * (`{ col: 'otherField' }`) which compiles to a column-to-column comparison
1098
+ * against `refCtx`: no param bound, so `collectOperatorParams` mirrors by
1099
+ * pushing nothing and the referenced name lives in the fingerprint.
1100
+ */
1101
+ function buildOperatorClauses(qi, column, op, params, refCtx) {
1102
+ const clauses = [];
1103
+ if (op.equals !== undefined) {
1104
+ if (op.equals === null) {
1105
+ clauses.push(`${column} IS NULL`);
1106
+ }
1107
+ else if ((0, filters_js_1.isColumnRef)(op.equals)) {
1108
+ clauses.push(`${column} = ${columnRefSql(qi, op.equals, refCtx, op.mode)}`);
1109
+ }
1110
+ else {
1111
+ (0, filters_js_1.assertBindableEqualsOperand)(op.equals, column);
1112
+ params.push(op.equals);
1113
+ clauses.push(`${column} = ${qi.p(params.length)}`);
1114
+ }
1115
+ }
1116
+ if (op.gt !== undefined) {
1117
+ if ((0, filters_js_1.isColumnRef)(op.gt)) {
1118
+ clauses.push(`${column} > ${columnRefSql(qi, op.gt, refCtx, op.mode)}`);
1119
+ }
1120
+ else {
1121
+ params.push(op.gt);
1122
+ clauses.push(`${column} > ${qi.p(params.length)}`);
1123
+ }
1124
+ }
1125
+ if (op.gte !== undefined) {
1126
+ if ((0, filters_js_1.isColumnRef)(op.gte)) {
1127
+ clauses.push(`${column} >= ${columnRefSql(qi, op.gte, refCtx, op.mode)}`);
1128
+ }
1129
+ else {
1130
+ params.push(op.gte);
1131
+ clauses.push(`${column} >= ${qi.p(params.length)}`);
1132
+ }
1133
+ }
1134
+ if (op.lt !== undefined) {
1135
+ if ((0, filters_js_1.isColumnRef)(op.lt)) {
1136
+ clauses.push(`${column} < ${columnRefSql(qi, op.lt, refCtx, op.mode)}`);
1137
+ }
1138
+ else {
1139
+ params.push(op.lt);
1140
+ clauses.push(`${column} < ${qi.p(params.length)}`);
1141
+ }
1142
+ }
1143
+ if (op.lte !== undefined) {
1144
+ if ((0, filters_js_1.isColumnRef)(op.lte)) {
1145
+ clauses.push(`${column} <= ${columnRefSql(qi, op.lte, refCtx, op.mode)}`);
1146
+ }
1147
+ else {
1148
+ params.push(op.lte);
1149
+ clauses.push(`${column} <= ${qi.p(params.length)}`);
1150
+ }
1151
+ }
1152
+ if (op.not !== undefined) {
1153
+ if (op.not === null) {
1154
+ clauses.push(`${column} IS NOT NULL`);
1155
+ }
1156
+ else if ((0, filters_js_1.isColumnRef)(op.not)) {
1157
+ clauses.push(`${column} != ${columnRefSql(qi, op.not, refCtx, op.mode)}`);
1158
+ }
1159
+ else {
1160
+ params.push(op.not);
1161
+ clauses.push(`${column} != ${qi.p(params.length)}`);
1162
+ }
1163
+ }
1164
+ if (op.in !== undefined) {
1165
+ params.push(qi.inParam(op.in));
1166
+ clauses.push(qi.inClause(column, qi.p(params.length), false));
1167
+ }
1168
+ if (op.notIn !== undefined) {
1169
+ params.push(qi.inParam(op.notIn));
1170
+ clauses.push(qi.inClause(column, qi.p(params.length), true));
1171
+ }
1172
+ const buildLikeClause = (paramRef) => op.mode === 'insensitive' ? qi.dialect.buildInsensitiveLike(column, paramRef) : `${column} LIKE ${paramRef}`;
1173
+ if (op.contains !== undefined) {
1174
+ params.push(`%${(0, utils_js_1.escapeLike)(op.contains)}%`);
1175
+ clauses.push(`${buildLikeClause(qi.p(params.length))} ESCAPE '\\'`);
1176
+ }
1177
+ if (op.startsWith !== undefined) {
1178
+ params.push(`${(0, utils_js_1.escapeLike)(op.startsWith)}%`);
1179
+ clauses.push(`${buildLikeClause(qi.p(params.length))} ESCAPE '\\'`);
1180
+ }
1181
+ if (op.endsWith !== undefined) {
1182
+ params.push(`%${(0, utils_js_1.escapeLike)(op.endsWith)}`);
1183
+ clauses.push(`${buildLikeClause(qi.p(params.length))} ESCAPE '\\'`);
1184
+ }
1185
+ return clauses;
1186
+ }
1187
+ /**
1188
+ * Resolve a {@link VectorMetric} to its pgvector distance operator from a
1189
+ * fixed allow-list, validating the target column is actually a `vector`
1190
+ * column. Throws {@link ValidationError} for an unknown metric or a
1191
+ * non-vector column — a user-supplied string can never become a SQL operator.
1192
+ */
1193
+ function vectorOperator(qi, field, rawColumn, metric) {
1194
+ if (!qi.dialect.supportsVector) {
1195
+ throw new errors_js_1.UnsupportedFeatureError('pgvector distance operations', qi.dialect.name, 'Vector search requires PostgreSQL with the pgvector extension.');
1196
+ }
1197
+ const colType = getColumnPgType(qi, rawColumn);
1198
+ if (colType !== 'vector') {
1199
+ throw new errors_js_1.ValidationError(`[turbine] Column "${field}" on table "${qi.table}" is not a vector column ` +
1200
+ `(actual type: ${colType}); cannot apply a vector distance operation.`);
1201
+ }
1202
+ const op = filters_js_1.VECTOR_METRIC_OPERATORS[metric];
1203
+ if (!op) {
1204
+ throw new errors_js_1.ValidationError(`[turbine] Unknown vector metric "${metric}" for column "${field}". ` +
1205
+ `Valid metrics: ${Object.keys(filters_js_1.VECTOR_METRIC_OPERATORS).join(', ')}.`);
1206
+ }
1207
+ return op;
1208
+ }
1209
+ /**
1210
+ * Validate and bind a query vector as a single `$n::vector` parameter.
1211
+ * Every element must be a finite number (no NaN / Infinity / strings) so a
1212
+ * malformed array can never produce a broken `::vector` literal, and the array
1213
+ * is NEVER string-interpolated into the SQL text. Returns the `$n::vector`
1214
+ * placeholder string.
1215
+ */
1216
+ function pushVectorParam(qi, field, _rawColumn, to, params) {
1217
+ if (!qi.dialect.supportsVector) {
1218
+ throw new errors_js_1.UnsupportedFeatureError('pgvector distance operations', qi.dialect.name, 'Vector search requires PostgreSQL with the pgvector extension.');
1219
+ }
1220
+ if (!Array.isArray(to) || to.length === 0) {
1221
+ throw new errors_js_1.ValidationError(`[turbine] Vector distance on "${field}" requires a non-empty array of numbers for "to".`);
1222
+ }
1223
+ for (const el of to) {
1224
+ if (typeof el !== 'number' || !Number.isFinite(el)) {
1225
+ throw new errors_js_1.ValidationError(`[turbine] Vector "to" for column "${field}" must contain only finite numbers; ` + `got ${JSON.stringify(el)}.`);
1226
+ }
1227
+ }
1228
+ // Bind as a pgvector text literal '[1,2,3]'. Elements are already validated
1229
+ // as finite numbers, so the joined string is safe; it is still passed as a
1230
+ // bound param (never interpolated) and cast with ::vector.
1231
+ params.push(`[${to.join(',')}]`);
1232
+ return `${qi.p(params.length)}::vector`;
1233
+ }
1234
+ /**
1235
+ * Prisma-compat: a plain object on a to-one relation key —
1236
+ * `where: { vendor: { name: { contains: 'x' } } }` — is an implicit `is`
1237
+ * filter. Normalize it to `{ is: obj }` so all downstream handling (SQL,
1238
+ * params, fingerprint) sees one canonical shape. To-many relations still
1239
+ * require an explicit `some`/`every`/`none` (a bare object there is
1240
+ * ambiguous and was never valid in Prisma either).
1241
+ */
1242
+ function normalizeRelationFilter(_qi, relDef, filterObj) {
1243
+ if ((relDef.type === 'belongsTo' || relDef.type === 'hasOne') &&
1244
+ !('some' in filterObj) &&
1245
+ !('every' in filterObj) &&
1246
+ !('none' in filterObj) &&
1247
+ !('is' in filterObj) &&
1248
+ !('isNot' in filterObj)) {
1249
+ return { is: filterObj };
1250
+ }
1251
+ return filterObj;
1252
+ }
1253
+ /**
1254
+ * Case-insensitive json/jsonb column-type check. Postgres reports lowercase
1255
+ * udt_names, but SQLite/MySQL introspection surfaces the DECLARED type
1256
+ * (e.g. `JSON`), so every JSON-feature gate compares through this predicate
1257
+ * — build and collect sides alike, keeping the SQL-cache lockstep.
1258
+ */
1259
+ function isJsonColumnType(_qi, colType) {
1260
+ const t = colType.toLowerCase();
1261
+ return t === 'json' || t === 'jsonb';
1262
+ }
1263
+ function getColumnPgType(qi, column) {
1264
+ return qi.columnPgTypeMap.get(column) ?? 'text';
1265
+ }
1266
+ /**
1267
+ * Get the Postgres base element type for an array column.
1268
+ * E.g. '_text' → 'text', '_int4' → 'integer'
1269
+ */
1270
+ function getArrayElementType(_qi, pgType) {
1271
+ const baseType = pgType.startsWith('_') ? pgType.slice(1) : pgType;
1272
+ const typeMap = {
1273
+ int2: 'smallint',
1274
+ int4: 'integer',
1275
+ int8: 'bigint',
1276
+ float4: 'real',
1277
+ float8: 'double precision',
1278
+ bool: 'boolean',
1279
+ text: 'text',
1280
+ varchar: 'text',
1281
+ uuid: 'uuid',
1282
+ timestamptz: 'timestamptz',
1283
+ timestamp: 'timestamp',
1284
+ jsonb: 'jsonb',
1285
+ json: 'json',
1286
+ };
1287
+ return typeMap[baseType] ?? 'text';
1288
+ }
1289
+ /**
1290
+ * Validate and enumerate the range comparisons (`gt`/`gte`/`lt`/`lte`) on a
1291
+ * JSON filter, in the fixed {@link JSON_RANGE_OPERATORS} order. Shared by
1292
+ * the SQL-build path ({@link buildJsonFilterClauses}) and the cache-hit
1293
+ * param-collect path ({@link collectJsonFilterParams}) so both always agree
1294
+ * on which params are pushed — and both throw identically for invalid
1295
+ * shapes, so a warmed cache can never skip validation.
1296
+ */
1297
+ function jsonRangeEntries(_qi, filter, column) {
1298
+ const entries = [];
1299
+ for (const [op, sqlOp] of Object.entries(filters_js_1.JSON_RANGE_OPERATORS)) {
1300
+ const value = filter[op];
1301
+ if (value === undefined)
1302
+ continue;
1303
+ if (filter.path === undefined) {
1304
+ throw new errors_js_1.ValidationError(`[turbine] JSON range operator '${op}' on ${column} requires a \`path\` ` +
1305
+ `(e.g. { path: ['meta', 'score'], ${op}: ${JSON.stringify(value)} }).`);
1306
+ }
1307
+ if (typeof value !== 'number' && typeof value !== 'string') {
1308
+ throw new errors_js_1.ValidationError(`[turbine] JSON range operator '${op}' on ${column} requires a number or string, ` +
1309
+ `got ${JSON.stringify(value)}.`);
1310
+ }
1311
+ if (typeof value === 'number' && !Number.isFinite(value)) {
1312
+ throw new errors_js_1.ValidationError(`[turbine] JSON range operator '${op}' on ${column} requires a finite number.`);
1313
+ }
1314
+ entries.push({ sqlOp, value });
1315
+ }
1316
+ return entries;
1317
+ }
1318
+ /**
1319
+ * Build SQL clauses for JSONB filter operators on a column.
1320
+ * Supports: path, equals, contains, hasKey, gt, gte, lt, lte.
1321
+ *
1322
+ * The `path` param is bound at most once and its placeholder is shared by
1323
+ * every clause that extracts it (equals + range ops), so the param list
1324
+ * stays byte-identical to {@link collectJsonFilterParams}.
1325
+ */
1326
+ function buildJsonFilterClauses(qi, column, filter, params) {
1327
+ const clauses = [];
1328
+ // Lazily bind the path once; reuse the same $N in every extraction clause.
1329
+ let pathParamIdx = null;
1330
+ const pathExtract = () => {
1331
+ if (pathParamIdx === null) {
1332
+ // Only reached when a path-requiring clause validated filter.path.
1333
+ params.push(jsonPathParam(qi, filter.path, filter.path));
1334
+ pathParamIdx = params.length;
1335
+ }
1336
+ return qi.dialect.buildJsonPathExtract(column, qi.p(pathParamIdx));
1337
+ };
1338
+ if (filter.path !== undefined && filter.equals !== undefined) {
1339
+ // Path access + equals: column #>> $N::text[] = $M
1340
+ const extract = pathExtract();
1341
+ params.push(String(filter.equals));
1342
+ clauses.push(`${extract} = ${qi.p(params.length)}`);
1343
+ }
1344
+ else if (filter.equals !== undefined) {
1345
+ // Containment equality: column @> $N::jsonb
1346
+ params.push(JSON.stringify(filter.equals));
1347
+ clauses.push(qi.dialect.buildJsonContains(column, qi.p(params.length)));
1348
+ }
1349
+ if (filter.contains !== undefined) {
1350
+ // Containment: column @> $N::jsonb
1351
+ params.push(JSON.stringify(filter.contains));
1352
+ clauses.push(qi.dialect.buildJsonContains(column, qi.p(params.length)));
1353
+ }
1354
+ if (filter.hasKey !== undefined) {
1355
+ // Key existence: column ? $N
1356
+ params.push(filter.hasKey);
1357
+ clauses.push(`${column} ? ${qi.p(params.length)}`);
1358
+ }
1359
+ // Range comparisons on the extracted path: numbers compare numerically
1360
+ // (cast through the dialect), strings compare as text.
1361
+ for (const { sqlOp, value } of jsonRangeEntries(qi, filter, column)) {
1362
+ const extract = pathExtract();
1363
+ params.push(value);
1364
+ const lhs = typeof value === 'number' ? castJsonNumeric(qi, extract) : extract;
1365
+ clauses.push(`${lhs} ${sqlOp} ${qi.p(params.length)}`);
1366
+ }
1367
+ return clauses;
1368
+ }
1369
+ /**
1370
+ * Bind value for a JSON path parameter, encoded per dialect. PostgreSQL's
1371
+ * `#>>` takes a `text[]` (the segments as strings — or `nativeForm` when the
1372
+ * caller has a specific native binding, e.g. JsonFilter's raw path array).
1373
+ * Every other engine's JSON function (`json_extract` / `JSON_EXTRACT` /
1374
+ * `JSON_VALUE`) takes a `'$'`-rooted JSONPath STRING: binding the raw array
1375
+ * would arrive as `'["a"]'` (the driver shims JSON.stringify non-primitive
1376
+ * params) and fail at runtime with the engine's bad-JSON-path error. The
1377
+ * encoded path stays a bound parameter — never spliced into SQL text — so
1378
+ * the build/collect param mirrors stay in lockstep and injection-safe.
1379
+ */
1380
+ function jsonPathParam(qi, path, nativeForm) {
1381
+ if (qi.dialect.jsonPathSupport === 'native')
1382
+ return nativeForm ?? path.map(String);
1383
+ return `$${path
1384
+ .map((seg) => typeof seg === 'number' || /^\d+$/.test(String(seg)) ? `[${seg}]` : `."${String(seg).replace(/"/g, '\\"')}"`)
1385
+ .join('')}`;
1386
+ }
1387
+ /**
1388
+ * Cast an extracted JSON path text value to a numeric type for range
1389
+ * comparison. PostgreSQL uses `(expr)::numeric` (exact — the right way to
1390
+ * compare JSON numbers, and `::float` would lose precision on big ints);
1391
+ * other dialects route through {@link Dialect.castAggregate} (SQLite/MySQL/
1392
+ * SQL Server have no `::` operator) as a float cast.
1393
+ */
1394
+ function castJsonNumeric(qi, extract) {
1395
+ if (qi.dialect.name === 'postgresql')
1396
+ return `(${extract})::numeric`;
1397
+ return qi.dialect.castAggregate ? qi.dialect.castAggregate(`(${extract})`, 'float') : `(${extract})::numeric`;
1398
+ }
1399
+ /**
1400
+ * Build SQL clauses for Array filter operators on a column.
1401
+ * Supports: has, hasEvery, hasSome, isEmpty.
1402
+ */
1403
+ function buildArrayFilterClauses(qi, column, filter, params, pgType) {
1404
+ const clauses = [];
1405
+ const elementType = getArrayElementType(qi, pgType);
1406
+ if (filter.has !== undefined) {
1407
+ // value = ANY(column)
1408
+ params.push(filter.has);
1409
+ clauses.push(`${qi.p(params.length)} = ANY(${column})`);
1410
+ }
1411
+ if (filter.hasEvery !== undefined) {
1412
+ // column @> ARRAY[...]::type[]
1413
+ params.push(filter.hasEvery);
1414
+ clauses.push(`${column} @> ${qi.p(params.length)}::${elementType}[]`);
1415
+ }
1416
+ if (filter.hasSome !== undefined) {
1417
+ // column && ARRAY[...]::type[]
1418
+ params.push(filter.hasSome);
1419
+ clauses.push(`${column} && ${qi.p(params.length)}::${elementType}[]`);
1420
+ }
1421
+ if (filter.isEmpty === true) {
1422
+ // Treat NULL and empty arrays as empty for Prisma-compatible ergonomics.
1423
+ clauses.push(`COALESCE(cardinality(${column}), 0) = 0`);
1424
+ }
1425
+ else if (filter.isEmpty === false) {
1426
+ // Require at least one element; excludes both NULL and ARRAY[] values.
1427
+ clauses.push(`cardinality(${column}) > 0`);
1428
+ }
1429
+ return clauses;
1430
+ }
1431
+ /**
1432
+ * Build SQL clauses for a pgvector distance WHERE filter:
1433
+ *
1434
+ * `"embedding" <-> $1::vector < $2`
1435
+ *
1436
+ * The query vector is bound as a `$n::vector` param (never interpolated), the
1437
+ * metric maps to an operator via a fixed allow-list, and each comparison
1438
+ * threshold (`lt`/`lte`/`gt`/`gte`) is its own bound param. Emits one clause
1439
+ * per supplied comparator (all ANDed). Param push order matches
1440
+ * {@link collectVectorFilterParams}.
1441
+ */
1442
+ function buildVectorFilterClauses(qi, field, rawColumn, filter, params) {
1443
+ const dist = filter.distance;
1444
+ const operator = vectorOperator(qi, field, rawColumn, dist.metric);
1445
+ const placeholder = pushVectorParam(qi, field, rawColumn, dist.to, params);
1446
+ const distanceExpr = `${qi.q(rawColumn)} ${operator} ${placeholder}`;
1447
+ const clauses = [];
1448
+ for (const [cmp, sqlOp] of Object.entries(filters_js_1.VECTOR_DISTANCE_COMPARATORS)) {
1449
+ const threshold = dist[cmp];
1450
+ if (threshold === undefined)
1451
+ continue;
1452
+ if (typeof threshold !== 'number' || !Number.isFinite(threshold)) {
1453
+ throw new errors_js_1.ValidationError(`[turbine] Vector distance threshold "${cmp}" on "${field}" must be a finite number; ` +
1454
+ `got ${JSON.stringify(threshold)}.`);
1455
+ }
1456
+ params.push(threshold);
1457
+ clauses.push(`${distanceExpr} ${sqlOp} ${qi.p(params.length)}`);
1458
+ }
1459
+ if (clauses.length === 0) {
1460
+ throw new errors_js_1.ValidationError(`[turbine] Vector distance filter on "${field}" requires at least one comparison (lt / lte / gt / gte).`);
1461
+ }
1462
+ return clauses;
1463
+ }
1464
+ /**
1465
+ * Build SQL clause for full-text search using to_tsvector @@ to_tsquery.
1466
+ * The config name is validated to prevent injection (only alphanumeric + underscore).
1467
+ */
1468
+ function buildTextSearchClause(qi, column, filter, params) {
1469
+ const config = filter.config ?? 'english';
1470
+ if (!(0, filters_js_1.validateTextSearchConfig)(config)) {
1471
+ throw new errors_js_1.ValidationError(`[turbine] Invalid text search config "${config}": only alphanumeric characters and underscores are allowed.`);
1472
+ }
1473
+ params.push(filter.search);
1474
+ return `to_tsvector('${config}', ${column}) @@ to_tsquery('${config}', ${qi.p(params.length)})`;
1475
+ }
1476
+ /**
1477
+ * Get the Postgres array type for a column (used by UNNEST in createMany).
1478
+ * Uses pre-computed Map for O(1) lookup instead of linear scan.
1479
+ */
1480
+ function getColumnArrayType(qi, column) {
1481
+ const arrayType = qi.columnArrayTypeMap.get(column);
1482
+ if (arrayType)
1483
+ return arrayType;
1484
+ // Fallback heuristic for unknown columns, routed through the active dialect
1485
+ // so non-Postgres packages can supply their own bulk-insert cast shape.
1486
+ if (column === 'id' || column.endsWith('_id'))
1487
+ return qi.dialect.arrayType?.('int8') ?? 'text[]';
1488
+ if (column.endsWith('_at'))
1489
+ return qi.dialect.arrayType?.('timestamptz') ?? 'text[]';
1490
+ return qi.dialect.arrayType?.('text') ?? 'text[]';
1491
+ }