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