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