@cerbos/orm-prisma 4.1.0 → 5.0.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 +598 -603
- package/lib/collections.d.ts +30 -0
- package/lib/collections.d.ts.map +1 -0
- package/lib/collections.js +421 -0
- package/lib/collections.js.map +1 -0
- package/lib/comparison.d.ts +24 -0
- package/lib/comparison.d.ts.map +1 -0
- package/lib/comparison.js +731 -0
- package/lib/comparison.js.map +1 -0
- package/lib/errors.d.ts +13 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +17 -0
- package/lib/errors.js.map +1 -0
- package/lib/evaluate.d.ts +5 -0
- package/lib/evaluate.d.ts.map +1 -0
- package/lib/evaluate.js +118 -0
- package/lib/evaluate.js.map +1 -0
- package/lib/fields.d.ts +34 -0
- package/lib/fields.d.ts.map +1 -0
- package/lib/fields.js +253 -0
- package/lib/fields.js.map +1 -0
- package/lib/functions.d.ts +5 -0
- package/lib/functions.d.ts.map +1 -0
- package/lib/functions.js +408 -0
- package/lib/functions.js.map +1 -0
- package/lib/hierarchy.d.ts +6 -0
- package/lib/hierarchy.d.ts.map +1 -0
- package/lib/hierarchy.js +266 -0
- package/lib/hierarchy.js.map +1 -0
- package/lib/index.d.ts +23 -13
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +34 -2555
- package/lib/index.js.map +1 -1
- package/lib/literals.d.ts +20 -0
- package/lib/literals.d.ts.map +1 -0
- package/lib/literals.js +290 -0
- package/lib/literals.js.map +1 -0
- package/lib/mapping.d.ts +95 -0
- package/lib/mapping.d.ts.map +1 -0
- package/lib/mapping.js +316 -0
- package/lib/mapping.js.map +1 -0
- package/lib/narrowing.d.ts +12 -0
- package/lib/narrowing.d.ts.map +1 -0
- package/lib/narrowing.js +32 -0
- package/lib/narrowing.js.map +1 -0
- package/lib/plan.d.ts +49 -0
- package/lib/plan.d.ts.map +1 -0
- package/lib/plan.js +86 -0
- package/lib/plan.js.map +1 -0
- package/lib/presence.d.ts +25 -0
- package/lib/presence.d.ts.map +1 -0
- package/lib/presence.js +82 -0
- package/lib/presence.js.map +1 -0
- package/lib/regex.d.ts +33 -0
- package/lib/regex.d.ts.map +1 -0
- package/lib/regex.js +480 -0
- package/lib/regex.js.map +1 -0
- package/lib/relations.d.ts +58 -0
- package/lib/relations.d.ts.map +1 -0
- package/lib/relations.js +175 -0
- package/lib/relations.js.map +1 -0
- package/lib/rewrite.d.ts +36 -0
- package/lib/rewrite.d.ts.map +1 -0
- package/lib/rewrite.js +366 -0
- package/lib/rewrite.js.map +1 -0
- package/lib/solve.d.ts +20 -0
- package/lib/solve.d.ts.map +1 -0
- package/lib/solve.js +77 -0
- package/lib/solve.js.map +1 -0
- package/lib/strings.d.ts +12 -0
- package/lib/strings.d.ts.map +1 -0
- package/lib/strings.js +110 -0
- package/lib/strings.js.map +1 -0
- package/lib/ternary.d.ts +12 -0
- package/lib/ternary.d.ts.map +1 -0
- package/lib/ternary.js +192 -0
- package/lib/ternary.js.map +1 -0
- package/lib/timestamp.d.ts +45 -0
- package/lib/timestamp.d.ts.map +1 -0
- package/lib/timestamp.js +225 -0
- package/lib/timestamp.js.map +1 -0
- package/lib/translate.d.ts +20 -0
- package/lib/translate.d.ts.map +1 -0
- package/lib/translate.js +287 -0
- package/lib/translate.js.map +1 -0
- package/lib/types.d.ts +29 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +980 -0
- package/lib/types.js.map +1 -0
- package/lib/value.d.ts +8 -0
- package/lib/value.d.ts.map +1 -0
- package/lib/value.js +7 -0
- package/lib/value.js.map +1 -0
- package/package.json +27 -17
- package/src/adversarial.test.ts +376 -1071
- package/src/collections.ts +738 -0
- package/src/comparison.ts +1157 -0
- package/src/corpus.ts +433 -250
- package/src/errors.ts +12 -0
- package/src/evaluate.ts +170 -0
- package/src/fields.ts +373 -0
- package/src/functions.ts +548 -0
- package/src/generated/prisma-adversarial/browser.ts +10 -0
- package/src/generated/prisma-adversarial/client.ts +10 -0
- package/src/generated/prisma-adversarial/commonInputTypes.ts +116 -127
- package/src/generated/prisma-adversarial/internal/class.ts +24 -4
- package/src/generated/prisma-adversarial/internal/prismaNamespace.ts +174 -3
- package/src/generated/prisma-adversarial/internal/prismaNamespaceBrowser.ts +23 -2
- package/src/generated/prisma-adversarial/models/AdversarialBoolListElement.ts +1272 -0
- package/src/generated/prisma-adversarial/models/AdversarialInner.ts +54 -54
- package/src/generated/prisma-adversarial/models/AdversarialNumberListElement.ts +1316 -0
- package/src/generated/prisma-adversarial/models/AdversarialParent.ts +66 -66
- package/src/generated/prisma-adversarial/models/AdversarialResource.ts +435 -87
- package/src/generated/prisma-adversarial/models.ts +2 -0
- package/src/generated/prisma-adversarial-mysql/browser.ts +64 -0
- package/src/generated/prisma-adversarial-mysql/client.ts +86 -0
- package/src/generated/prisma-adversarial-mysql/commonInputTypes.ts +349 -0
- package/src/generated/prisma-adversarial-mysql/enums.ts +15 -0
- package/src/generated/prisma-adversarial-mysql/internal/class.ts +284 -0
- package/src/generated/prisma-adversarial-mysql/internal/prismaNamespace.ts +1518 -0
- package/src/generated/prisma-adversarial-mysql/internal/prismaNamespaceBrowser.ts +274 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialBoolListElement.ts +1157 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialCategory.ts +1286 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialInner.ts +1247 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialLabel.ts +1157 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialNumberListElement.ts +1201 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialParent.ts +1352 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialResource.ts +2021 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialSubCategory.ts +1286 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialTag.ts +1190 -0
- package/src/generated/prisma-adversarial-mysql/models.ts +20 -0
- package/src/generated/prisma-adversarial-mysql-v6/client.d.ts +1 -0
- package/src/generated/prisma-adversarial-mysql-v6/default.d.ts +1 -0
- package/src/generated/prisma-adversarial-mysql-v6/edge.d.ts +1 -0
- package/src/generated/prisma-adversarial-mysql-v6/index.d.ts +13822 -0
- package/src/generated/prisma-adversarial-mysql-v6/runtime/index-browser.d.ts +370 -0
- package/src/generated/prisma-adversarial-mysql-v6/runtime/library.d.ts +3982 -0
- package/src/generated/prisma-adversarial-mysql-v6/wasm.d.ts +1 -0
- package/src/generated/prisma-adversarial-pg/browser.ts +10 -0
- package/src/generated/prisma-adversarial-pg/client.ts +10 -0
- package/src/generated/prisma-adversarial-pg/commonInputTypes.ts +118 -129
- package/src/generated/prisma-adversarial-pg/internal/class.ts +24 -4
- package/src/generated/prisma-adversarial-pg/internal/prismaNamespace.ts +174 -3
- package/src/generated/prisma-adversarial-pg/internal/prismaNamespaceBrowser.ts +23 -2
- package/src/generated/prisma-adversarial-pg/models/AdversarialBoolListElement.ts +1275 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialInner.ts +54 -54
- package/src/generated/prisma-adversarial-pg/models/AdversarialNumberListElement.ts +1319 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialParent.ts +66 -66
- package/src/generated/prisma-adversarial-pg/models/AdversarialResource.ts +435 -87
- package/src/generated/prisma-adversarial-pg/models.ts +2 -0
- package/src/generated/prisma-adversarial-pg-v6/index.d.ts +3476 -388
- package/src/generated/prisma-adversarial-v6/index.d.ts +3465 -383
- package/src/hierarchy.ts +405 -0
- package/src/index.ts +66 -3646
- package/src/literals.ts +363 -0
- package/src/mapping.ts +474 -0
- package/src/narrowing.ts +33 -0
- package/src/plan.ts +125 -0
- package/src/presence.ts +114 -0
- package/src/regex.ts +596 -0
- package/src/relations.ts +226 -0
- package/src/rewrite.ts +524 -0
- package/src/solve.ts +96 -0
- package/src/strings.ts +152 -0
- package/src/ternary.ts +359 -0
- package/src/test-setup.adversarial-url.ts +6 -3
- package/src/test-setup.adversarial.mysql.v6.ts +13 -0
- package/src/test-setup.adversarial.mysql.v7.ts +12 -0
- package/src/test-setup.adversarial.v7.ts +1 -0
- package/src/timestamp.ts +308 -0
- package/src/translate.ts +419 -0
- package/src/translator.test.ts +497 -1317
- package/src/types.ts +1310 -0
- package/src/value.ts +16 -0
|
@@ -0,0 +1,1157 @@
|
|
|
1
|
+
// Comparisons (`eq`, `ne`, `lt`, `le`, `gt`, `ge`) and membership (`in`): against a constant,
|
|
2
|
+
// between two columns, over size(), and over linear arithmetic solved for the column.
|
|
3
|
+
|
|
4
|
+
import type { PlanExpressionOperand, Value } from "@cerbos/core";
|
|
5
|
+
|
|
6
|
+
import { ARITHMETIC_OPERATORS, evaluateConstantComparison, foldArithmetic } from "./evaluate";
|
|
7
|
+
import {
|
|
8
|
+
assertStringField,
|
|
9
|
+
buildComparisonFilter,
|
|
10
|
+
buildFieldFilter,
|
|
11
|
+
buildMembershipFilter,
|
|
12
|
+
rejectConstantFalse,
|
|
13
|
+
} from "./fields";
|
|
14
|
+
import { literalValue } from "./literals";
|
|
15
|
+
import type { PrismaFilter } from "./index";
|
|
16
|
+
import {
|
|
17
|
+
currentScope,
|
|
18
|
+
getLeafField,
|
|
19
|
+
isExplicitNullReference,
|
|
20
|
+
isResolvedFieldReference,
|
|
21
|
+
isResolvedValue,
|
|
22
|
+
namesToOneRelation,
|
|
23
|
+
recordErroringNullElement,
|
|
24
|
+
resolveFieldReference,
|
|
25
|
+
} from "./mapping";
|
|
26
|
+
import type {
|
|
27
|
+
ResolvedFieldReference,
|
|
28
|
+
ResolvedValue,
|
|
29
|
+
TranslationContext,
|
|
30
|
+
} from "./mapping";
|
|
31
|
+
import {
|
|
32
|
+
CERBOS_TO_PRISMA_OPERATOR,
|
|
33
|
+
assertDefined,
|
|
34
|
+
isNamedOperand,
|
|
35
|
+
isOperatorOperand,
|
|
36
|
+
isValueOperand,
|
|
37
|
+
mirrorOperator,
|
|
38
|
+
normalizeBinaryOperands,
|
|
39
|
+
} from "./plan";
|
|
40
|
+
import type { OperatorOperand } from "./plan";
|
|
41
|
+
import { relationFilter, wrapInRelations } from "./relations";
|
|
42
|
+
import { negateInterval, solveMonotone } from "./solve";
|
|
43
|
+
import type { DoubleInterval } from "./solve";
|
|
44
|
+
import { tryHandleTernaryComparison } from "./ternary";
|
|
45
|
+
import { roundSubMillisecond } from "./timestamp";
|
|
46
|
+
import {
|
|
47
|
+
buildPrismaFilterFromCerbosExpression,
|
|
48
|
+
resolveOperand,
|
|
49
|
+
tryFoldValueExpression,
|
|
50
|
+
} from "./translate";
|
|
51
|
+
import { presence } from "./types";
|
|
52
|
+
import { UnsupportedQueryPlanError } from "./errors";
|
|
53
|
+
import { isNumber, isObject, isString, lookup, pair, typeofValue } from "./narrowing";
|
|
54
|
+
import { isPrismaFilter } from "./value";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Helper function to process relational operators (eq, ne, lt, etc.)
|
|
58
|
+
*/
|
|
59
|
+
export function handleRelationalOperator(
|
|
60
|
+
operator: string,
|
|
61
|
+
operands: PlanExpressionOperand[],
|
|
62
|
+
context: TranslationContext
|
|
63
|
+
): PrismaFilter {
|
|
64
|
+
const ternaryFilter = tryHandleTernaryComparison(operator, operands, context);
|
|
65
|
+
|
|
66
|
+
if (ternaryFilter) {
|
|
67
|
+
return ternaryFilter;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const lookupFilter =
|
|
71
|
+
tryHandleMapLiteralLookup(operator, operands, context) ??
|
|
72
|
+
tryHandleUpperAsciiComparison(operator, operands, context);
|
|
73
|
+
|
|
74
|
+
if (lookupFilter) {
|
|
75
|
+
return lookupFilter;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
({ operator, operands } = normalizeBinaryOperands(operator, operands));
|
|
79
|
+
|
|
80
|
+
if (!lookup(CERBOS_TO_PRISMA_OPERATOR, operator)) {
|
|
81
|
+
throw new UnsupportedQueryPlanError(`Unsupported operator: ${operator}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const leftOperand = operands.find(
|
|
85
|
+
(o) => isNamedOperand(o) || isOperatorOperand(o)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (!leftOperand) throw new UnsupportedQueryPlanError("No valid left operand found");
|
|
89
|
+
|
|
90
|
+
const rightOperand = operands.find((o) => o !== leftOperand);
|
|
91
|
+
|
|
92
|
+
if (!rightOperand) throw new UnsupportedQueryPlanError("No valid right operand found");
|
|
93
|
+
|
|
94
|
+
if (isOperatorOperand(leftOperand) && leftOperand.operator === "size") {
|
|
95
|
+
return handleSizeComparison(operator, leftOperand, rightOperand, context);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const arithOperand = [leftOperand, rightOperand].find(
|
|
99
|
+
(o): o is OperatorOperand =>
|
|
100
|
+
isOperatorOperand(o) && ARITHMETIC_OPERATORS.has(o.operator)
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (arithOperand) {
|
|
104
|
+
const arithIsLeft = arithOperand === leftOperand;
|
|
105
|
+
|
|
106
|
+
return handleArithmeticComparison(
|
|
107
|
+
operator,
|
|
108
|
+
arithOperand,
|
|
109
|
+
arithIsLeft ? rightOperand : leftOperand,
|
|
110
|
+
arithIsLeft,
|
|
111
|
+
context
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
[leftOperand, rightOperand].some(
|
|
117
|
+
(o) => isOperatorOperand(o) && o.operator === "map"
|
|
118
|
+
)
|
|
119
|
+
) {
|
|
120
|
+
throw new UnsupportedQueryPlanError(
|
|
121
|
+
`Direct comparison of map(...) to a value is not supported (operator: ${operator}). ` +
|
|
122
|
+
`Wrap the map() expression in hasIntersection(map(...), [...]) instead.`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
assertNoRawTemporalAgainstTimestamp(leftOperand, rightOperand, context);
|
|
127
|
+
|
|
128
|
+
// Only a comparison between a column and one constant can absorb a sub-millisecond instant.
|
|
129
|
+
const oneColumn = isColumnOperand(leftOperand) !== isColumnOperand(rightOperand);
|
|
130
|
+
const left = resolveOperand(leftOperand, context, oneColumn);
|
|
131
|
+
const right = resolveOperand(rightOperand, context, oneColumn);
|
|
132
|
+
|
|
133
|
+
if (isResolvedValue(left)) {
|
|
134
|
+
if (isResolvedFieldReference(right)) {
|
|
135
|
+
return buildValueComparisonFilter(
|
|
136
|
+
context,
|
|
137
|
+
right,
|
|
138
|
+
mirrorOperator(operator),
|
|
139
|
+
left
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return evaluateConstantComparison(operator, left.value, right.value)
|
|
144
|
+
? {}
|
|
145
|
+
: rejectConstantFalse();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (isResolvedFieldReference(right)) {
|
|
149
|
+
if (
|
|
150
|
+
left.valueType === "dateTime" &&
|
|
151
|
+
right.valueType === "dateTime" &&
|
|
152
|
+
(!left.timestampWrapped || !right.timestampWrapped)
|
|
153
|
+
) {
|
|
154
|
+
throw new UnsupportedQueryPlanError(
|
|
155
|
+
"Raw temporal column comparison loses RFC-3339 string spelling; wrap both operands in timestamp()"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (
|
|
160
|
+
left.valueType !== undefined &&
|
|
161
|
+
right.valueType !== undefined &&
|
|
162
|
+
left.valueType !== right.valueType
|
|
163
|
+
) {
|
|
164
|
+
throw new UnsupportedQueryPlanError("Cannot compare fields with different mapped value types");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return buildFieldToFieldFilter(
|
|
168
|
+
context,
|
|
169
|
+
operator,
|
|
170
|
+
leftOperand,
|
|
171
|
+
left,
|
|
172
|
+
rightOperand,
|
|
173
|
+
right
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return buildValueComparisonFilter(context, left, operator, right);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** `{"k": v, ...}[x]`: an index into a map literal. */
|
|
181
|
+
function isMapLiteralLookup(operand: PlanExpressionOperand): operand is OperatorOperand {
|
|
182
|
+
if (!isOperatorOperand(operand) || operand.operator !== "index" || operand.operands.length !== 2) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const [map] = operand.operands;
|
|
187
|
+
|
|
188
|
+
return isOperatorOperand(map!) && map.operator === "struct";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Whether a map-literal lookup appears anywhere in `operand`. */
|
|
192
|
+
export function containsMapLiteralLookup(operand: PlanExpressionOperand): boolean {
|
|
193
|
+
if (!isOperatorOperand(operand)) return false;
|
|
194
|
+
|
|
195
|
+
return operand.operands.some(containsMapLiteralLookup) || isMapLiteralLookup(operand);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* `{"k1": v1, "k2": v2}[x] == v` (and `!=`, either side first), with `x` a string column: the keys
|
|
200
|
+
* whose value equals `v` are exactly the values of `x` that make it true, and every other key the
|
|
201
|
+
* values that make it false. Any `x` that is not a key is an error, true under neither polarity,
|
|
202
|
+
* so the comparison and its negation are two membership tests, never one test and its NOT:
|
|
203
|
+
* `negated` asks for the negation's filter, and buildNegatedFilter pushes every `not` above a
|
|
204
|
+
* lookup down to it. Inside a lambda body a macro negates its body with a plain NOT, so the
|
|
205
|
+
* lookup is refused there.
|
|
206
|
+
*/
|
|
207
|
+
export function tryHandleMapLiteralLookup(
|
|
208
|
+
operator: string,
|
|
209
|
+
operands: PlanExpressionOperand[],
|
|
210
|
+
context: TranslationContext,
|
|
211
|
+
negated = false
|
|
212
|
+
): PrismaFilter | null {
|
|
213
|
+
if ((operator !== "eq" && operator !== "ne") || operands.length !== 2) return null;
|
|
214
|
+
const lookupIndex = operands.findIndex(isMapLiteralLookup);
|
|
215
|
+
|
|
216
|
+
if (lookupIndex === -1) return null;
|
|
217
|
+
const lookup = operands[lookupIndex];
|
|
218
|
+
|
|
219
|
+
if (lookup === undefined || !isOperatorOperand(lookup)) return null;
|
|
220
|
+
const other = operands[1 - lookupIndex]!;
|
|
221
|
+
const [mapOperand, key] = pair(lookup.operands);
|
|
222
|
+
const map = literalValue(mapOperand);
|
|
223
|
+
|
|
224
|
+
if (
|
|
225
|
+
map === undefined ||
|
|
226
|
+
!isObject(map) ||
|
|
227
|
+
Array.isArray(map) ||
|
|
228
|
+
!isValueOperand(other) ||
|
|
229
|
+
!isNamedOperand(key)
|
|
230
|
+
) {
|
|
231
|
+
throw new UnsupportedQueryPlanError(
|
|
232
|
+
"An index into a map literal is translated only as a literal map indexed by a column " +
|
|
233
|
+
"and compared with a literal: a Prisma filter has no map lookup"
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (context.scopes.length > 0) {
|
|
238
|
+
throw new UnsupportedQueryPlanError(
|
|
239
|
+
"An index into a map literal inside a lambda body is not supported: a missing key is an " +
|
|
240
|
+
"error under both polarities, and the macro negates its body with a plain NOT"
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const fieldRef = resolveFieldReference(key.name, context);
|
|
245
|
+
|
|
246
|
+
if (fieldRef.valueType !== "string") {
|
|
247
|
+
throw new UnsupportedQueryPlanError(
|
|
248
|
+
`An index into a map literal requires a string key column, and ${key.name} is not mapped ` +
|
|
249
|
+
'with valueType: "string"'
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const keys = Object.keys(map);
|
|
254
|
+
const matching = keys.filter((k) => evaluateConstantComparison("eq", map[k]!, other.value));
|
|
255
|
+
const holds = (operator === "eq") !== negated;
|
|
256
|
+
|
|
257
|
+
return buildFieldFilter(
|
|
258
|
+
fieldRef,
|
|
259
|
+
"in",
|
|
260
|
+
holds ? matching : keys.filter((k) => !matching.includes(k))
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Past this many spellings, `upperAscii(x) == "LIT"` is refused rather than enumerated. */
|
|
265
|
+
const MAX_UPPER_ASCII_SPELLINGS = 1024;
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* `x.upperAscii() == "LIT"` (and `!=`, either side first), with `x` a string column: upperAscii
|
|
269
|
+
* maps exactly the ASCII letters a-z to A-Z and leaves every other code point alone, so the
|
|
270
|
+
* strings it maps to "LIT" are its spellings with each ASCII capital in either case — none at all
|
|
271
|
+
* when the literal holds a lowercase ASCII letter. An IN over those spellings compares the bytes
|
|
272
|
+
* exactly, where a store's UPPER would also fold non-ASCII letters. A NULL column stays UNKNOWN,
|
|
273
|
+
* as upperAscii of a missing (or null) value is an error.
|
|
274
|
+
*/
|
|
275
|
+
function tryHandleUpperAsciiComparison(
|
|
276
|
+
operator: string,
|
|
277
|
+
operands: PlanExpressionOperand[],
|
|
278
|
+
context: TranslationContext
|
|
279
|
+
): PrismaFilter | null {
|
|
280
|
+
if ((operator !== "eq" && operator !== "ne") || operands.length !== 2) return null;
|
|
281
|
+
|
|
282
|
+
const callIndex = operands.findIndex(
|
|
283
|
+
(operand) => isOperatorOperand(operand) && operand.operator === "upperAscii"
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
if (callIndex === -1) return null;
|
|
287
|
+
const call = operands[callIndex];
|
|
288
|
+
|
|
289
|
+
if (call === undefined || !isOperatorOperand(call)) return null;
|
|
290
|
+
const other = operands[1 - callIndex]!;
|
|
291
|
+
const [subject] = call.operands;
|
|
292
|
+
|
|
293
|
+
if (
|
|
294
|
+
call.operands.length !== 1 ||
|
|
295
|
+
subject === undefined ||
|
|
296
|
+
!isNamedOperand(subject) ||
|
|
297
|
+
!isValueOperand(other) ||
|
|
298
|
+
!isString(other.value)
|
|
299
|
+
) {
|
|
300
|
+
throw new UnsupportedQueryPlanError(
|
|
301
|
+
"upperAscii() is translated only as a column compared with a string literal: a Prisma " +
|
|
302
|
+
"filter has no ASCII-only case mapping"
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const fieldRef = resolveFieldReference(subject.name, context);
|
|
307
|
+
assertStringField(fieldRef, "upperAscii");
|
|
308
|
+
recordErroringNullElement(context, subject, fieldRef);
|
|
309
|
+
let spellings = [""];
|
|
310
|
+
|
|
311
|
+
for (const character of other.value) {
|
|
312
|
+
if (character >= "a" && character <= "z") {
|
|
313
|
+
spellings = [];
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const cases =
|
|
318
|
+
character >= "A" && character <= "Z" ? [character, character.toLowerCase()] : [character];
|
|
319
|
+
|
|
320
|
+
spellings = spellings.flatMap((prefix) => cases.map((c) => prefix + c));
|
|
321
|
+
|
|
322
|
+
if (spellings.length > MAX_UPPER_ASCII_SPELLINGS) {
|
|
323
|
+
throw new UnsupportedQueryPlanError(
|
|
324
|
+
`Cannot translate upperAscii() == ${JSON.stringify(other.value)}: its spellings exceed ` +
|
|
325
|
+
`the ${MAX_UPPER_ASCII_SPELLINGS}-entry limit`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const filter = buildFieldFilter(fieldRef, "in", spellings);
|
|
331
|
+
|
|
332
|
+
return operator === "eq" ? filter : { NOT: filter };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Refuses a bare `dateTime` column compared with a `timestamp(...)` operand. The attribute the
|
|
337
|
+
* application sends for that column is its RFC 3339 STRING (a Cerbos attribute has no timestamp
|
|
338
|
+
* type), and CEL has no overload ordering a string against a timestamp: `check()` raises an error,
|
|
339
|
+
* which decides the row under neither polarity the way any column filter would. Wrapping the
|
|
340
|
+
* attribute in timestamp() is what the policy means.
|
|
341
|
+
*/
|
|
342
|
+
function assertNoRawTemporalAgainstTimestamp(
|
|
343
|
+
first: PlanExpressionOperand,
|
|
344
|
+
second: PlanExpressionOperand,
|
|
345
|
+
context: TranslationContext
|
|
346
|
+
): void {
|
|
347
|
+
for (const [column, other] of [
|
|
348
|
+
[first, second],
|
|
349
|
+
[second, first],
|
|
350
|
+
] as const) {
|
|
351
|
+
if (
|
|
352
|
+
isNamedOperand(column) &&
|
|
353
|
+
isOperatorOperand(other) &&
|
|
354
|
+
other.operator === "timestamp" &&
|
|
355
|
+
resolveFieldReference(column.name, context).valueType === "dateTime"
|
|
356
|
+
) {
|
|
357
|
+
throw new UnsupportedQueryPlanError(
|
|
358
|
+
`Cannot compare ${column.name} with a timestamp: the attribute holds an RFC 3339 ` +
|
|
359
|
+
"string, and CEL has no overload comparing a string with a timestamp, so check() " +
|
|
360
|
+
"raises an error that no column filter reproduces under both polarities; wrap the " +
|
|
361
|
+
"attribute in timestamp()"
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** A column reference, bare or wrapped in timestamp(). */
|
|
368
|
+
function isColumnOperand(operand: PlanExpressionOperand): boolean {
|
|
369
|
+
if (isNamedOperand(operand)) return true;
|
|
370
|
+
|
|
371
|
+
return (
|
|
372
|
+
isOperatorOperand(operand) &&
|
|
373
|
+
operand.operator === "timestamp" &&
|
|
374
|
+
operand.operands.length === 1 &&
|
|
375
|
+
isNamedOperand(operand.operands[0]!)
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** buildComparisonFilter, for a resolved constant that may be a sub-millisecond timestamp. */
|
|
380
|
+
function buildValueComparisonFilter(
|
|
381
|
+
context: TranslationContext,
|
|
382
|
+
fieldRef: ResolvedFieldReference,
|
|
383
|
+
operator: string,
|
|
384
|
+
constant: ResolvedValue
|
|
385
|
+
): PrismaFilter {
|
|
386
|
+
if (!constant.subMillisecond) {
|
|
387
|
+
return buildComparisonFilter(context, fieldRef, operator, constant.value);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const rounded = roundSubMillisecond(operator);
|
|
391
|
+
|
|
392
|
+
if (rounded !== null) {
|
|
393
|
+
return buildComparisonFilter(context, fieldRef, rounded, constant.value);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// No whole millisecond equals the instant. Spelled as a contradiction on the column, so a NULL
|
|
397
|
+
// stays UNKNOWN under both polarities — timestamp() of a missing attribute is an error.
|
|
398
|
+
const never: PrismaFilter = {
|
|
399
|
+
AND: [
|
|
400
|
+
buildComparisonFilter(context, fieldRef, "lt", constant.value),
|
|
401
|
+
buildComparisonFilter(context, fieldRef, "ge", constant.value),
|
|
402
|
+
],
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
return operator === "eq" ? never : { NOT: never };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* A size is a non-negative integer, so every `size(x) CMP n` — fractional, negative or huge `n`
|
|
410
|
+
* included — is one of: at least `k`, exactly `k`, or the negation of either. `at least 0` is
|
|
411
|
+
* true whenever `x` can be evaluated at all.
|
|
412
|
+
*/
|
|
413
|
+
type CountPredicate = {
|
|
414
|
+
kind: "atLeast" | "exactly";
|
|
415
|
+
count: number;
|
|
416
|
+
negated: boolean;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
function countPredicate(operator: string, n: number): CountPredicate {
|
|
420
|
+
const atLeast = (count: number, negated = false): CountPredicate => ({
|
|
421
|
+
kind: "atLeast",
|
|
422
|
+
count: Math.max(count, 0),
|
|
423
|
+
negated,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
switch (operator) {
|
|
427
|
+
case "gt":
|
|
428
|
+
return atLeast(Math.floor(n) + 1);
|
|
429
|
+
case "ge":
|
|
430
|
+
return atLeast(Math.ceil(n));
|
|
431
|
+
case "lt":
|
|
432
|
+
return atLeast(Math.ceil(n), true);
|
|
433
|
+
case "le":
|
|
434
|
+
return atLeast(Math.floor(n) + 1, true);
|
|
435
|
+
case "eq":
|
|
436
|
+
case "ne": {
|
|
437
|
+
const negated = operator === "ne";
|
|
438
|
+
|
|
439
|
+
// A fractional size never equals anything; a negative one never exists.
|
|
440
|
+
return Number.isInteger(n) && n >= 0
|
|
441
|
+
? { kind: "exactly", count: n, negated }
|
|
442
|
+
: atLeast(0, !negated);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
default:
|
|
446
|
+
throw new UnsupportedQueryPlanError(`Unsupported operator: ${operator}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* No store holds a string of 2^32 characters: PostgreSQL caps a value at 1 GB, SQLite at
|
|
452
|
+
* 2^31 - 1 bytes and MySQL's LONGTEXT at 2^32 - 1 bytes. A length threshold at or past it is
|
|
453
|
+
* decided without a pattern.
|
|
454
|
+
*/
|
|
455
|
+
const STRING_LENGTH_CEILING = 2 ** 32;
|
|
456
|
+
|
|
457
|
+
/** Past this, a length threshold short of the ceiling is refused rather than spelled out. */
|
|
458
|
+
const MAX_LENGTH_PATTERN = 1024;
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* `size(column) CMP n` over a string column, as `LIKE` patterns of `_`: Prisma does not escape
|
|
462
|
+
* the wildcards in a `startsWith` needle, so `startsWith: "_____"` is `LIKE '_____%'`, true exactly
|
|
463
|
+
* when the value holds at least five characters. Each store's `_` matches one character — a code
|
|
464
|
+
* point in a UTF-8 database — which is what CEL's size() counts. `startsWith: ""` is `LIKE '%'`,
|
|
465
|
+
* true for every non-NULL value. Every filter built here is therefore UNKNOWN on a NULL column
|
|
466
|
+
* under both polarities, as size() of a missing attribute is an error in CEL.
|
|
467
|
+
*/
|
|
468
|
+
function buildStringLengthFilter(
|
|
469
|
+
fieldRef: ResolvedFieldReference,
|
|
470
|
+
predicate: CountPredicate
|
|
471
|
+
): PrismaFilter {
|
|
472
|
+
const atLeast = (count: number): PrismaFilter => {
|
|
473
|
+
if (count >= STRING_LENGTH_CEILING) {
|
|
474
|
+
return { NOT: buildFieldFilter(fieldRef, "startsWith", "") };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (count > MAX_LENGTH_PATTERN) {
|
|
478
|
+
throw new UnsupportedQueryPlanError(
|
|
479
|
+
`Cannot translate a string length threshold of ${count}: the LIKE pattern it needs ` +
|
|
480
|
+
`exceeds the ${MAX_LENGTH_PATTERN}-character limit`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return buildFieldFilter(fieldRef, "startsWith", "_".repeat(count));
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const filter =
|
|
488
|
+
predicate.kind === "atLeast"
|
|
489
|
+
? atLeast(predicate.count)
|
|
490
|
+
: { AND: [atLeast(predicate.count), { NOT: atLeast(predicate.count + 1) }] };
|
|
491
|
+
|
|
492
|
+
return predicate.negated ? { NOT: filter } : filter;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* `size(collection) CMP n`. Over a relation, only the thresholds that mean "is empty", "is
|
|
497
|
+
* non-empty" or "the chain reaching it exists" — the only counts a relation filter can express.
|
|
498
|
+
* Over a string column, any threshold (see buildStringLengthFilter).
|
|
499
|
+
*/
|
|
500
|
+
function handleSizeComparison(
|
|
501
|
+
operator: string,
|
|
502
|
+
sizeOperand: OperatorOperand,
|
|
503
|
+
valueOperand: PlanExpressionOperand,
|
|
504
|
+
context: TranslationContext
|
|
505
|
+
): PrismaFilter {
|
|
506
|
+
const collectionOperand = sizeOperand.operands[0];
|
|
507
|
+
|
|
508
|
+
if (collectionOperand && isOperatorOperand(collectionOperand)) {
|
|
509
|
+
throw new UnsupportedQueryPlanError(
|
|
510
|
+
`size() of ${collectionOperand.operator}(...) counts the elements of a derived list, and a ` +
|
|
511
|
+
"Prisma relation filter can only test whether some, every or no related row matches, " +
|
|
512
|
+
"never count them"
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (!collectionOperand || !isNamedOperand(collectionOperand)) {
|
|
517
|
+
throw new UnsupportedQueryPlanError("size operator requires a named collection operand");
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (!isValueOperand(valueOperand)) {
|
|
521
|
+
throw new UnsupportedQueryPlanError("size comparison requires a numeric value operand");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const count = valueOperand.value;
|
|
525
|
+
|
|
526
|
+
if (!isNumber(count) || !Number.isFinite(count)) {
|
|
527
|
+
throw new UnsupportedQueryPlanError("size comparison requires a numeric value");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const predicate = countPredicate(operator, count);
|
|
531
|
+
|
|
532
|
+
const fieldRef = resolveFieldReference(collectionOperand.name, context);
|
|
533
|
+
const { relations } = fieldRef;
|
|
534
|
+
|
|
535
|
+
// A string column reached through to-one hops is measured on the hop's row, never counted as
|
|
536
|
+
// one: `size(parent.aString) >= 1` read as "has a parent" returned a parent whose aString is
|
|
537
|
+
// empty. The length's own negation sits inside the hops, so a missing hop is unmatched under
|
|
538
|
+
// both polarities, the missing-path error CEL denies.
|
|
539
|
+
if (fieldRef.valueType === "string") {
|
|
540
|
+
return wrapInRelations(
|
|
541
|
+
relations,
|
|
542
|
+
buildStringLengthFilter({ ...fieldRef, relations: [] }, predicate)
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (!relations || relations.length === 0) {
|
|
547
|
+
throw new UnsupportedQueryPlanError("size operator requires a relation mapping");
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// The count applies to the deepest relation; every relation before it is a hop to reach it.
|
|
551
|
+
const hops = relations.slice(0, -1);
|
|
552
|
+
const deepest = relations[relations.length - 1]!;
|
|
553
|
+
const { kind, count: k, negated } = predicate;
|
|
554
|
+
|
|
555
|
+
// `size >= 0` holds whenever the collection can be reached. Only a chain can fail to reach it;
|
|
556
|
+
// a direct relation would be an unconditional `{}`, which no negation could then express.
|
|
557
|
+
if (kind === "atLeast" && k === 0 && !negated && hops.length > 0) {
|
|
558
|
+
return wrapInRelations(hops, {});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const isNonEmpty =
|
|
562
|
+
(kind === "atLeast" && k === 1 && !negated) ||
|
|
563
|
+
(kind === "exactly" && k === 0 && negated);
|
|
564
|
+
|
|
565
|
+
const isEmpty =
|
|
566
|
+
(kind === "atLeast" && k === 1 && negated) ||
|
|
567
|
+
(kind === "exactly" && k === 0 && !negated);
|
|
568
|
+
|
|
569
|
+
if (!isNonEmpty && !isEmpty) {
|
|
570
|
+
throw new UnsupportedQueryPlanError(
|
|
571
|
+
`Unsupported size comparison: size(...) ${operator} ${count}`
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return wrapInRelations(
|
|
576
|
+
hops,
|
|
577
|
+
relationFilter(deepest, isNonEmpty ? "some" : "none", {})
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Builds a column-vs-column comparison as a Prisma field reference. Prisma only supports
|
|
583
|
+
* references between fields of the SAME model, so the container is the root model for
|
|
584
|
+
* root-level comparisons and the relation's model inside a lambda; mixed-scope comparisons
|
|
585
|
+
* (an element column against an outer column) are cross-model and must fail loudly.
|
|
586
|
+
*/
|
|
587
|
+
function buildFieldToFieldFilter(
|
|
588
|
+
context: TranslationContext,
|
|
589
|
+
operator: string,
|
|
590
|
+
leftOperand: PlanExpressionOperand,
|
|
591
|
+
left: ResolvedFieldReference,
|
|
592
|
+
rightOperand: PlanExpressionOperand,
|
|
593
|
+
right: ResolvedFieldReference
|
|
594
|
+
): PrismaFilter {
|
|
595
|
+
const prismaOperator = assertDefined(
|
|
596
|
+
lookup(CERBOS_TO_PRISMA_OPERATOR, operator),
|
|
597
|
+
`Unsupported operator: ${operator}`
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
if (!isNamedOperand(leftOperand) || !isNamedOperand(rightOperand)) {
|
|
601
|
+
throw new UnsupportedQueryPlanError("Field-to-field comparison requires two named operands");
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const container = fieldReferenceContainer(
|
|
605
|
+
context,
|
|
606
|
+
leftOperand.name,
|
|
607
|
+
left,
|
|
608
|
+
rightOperand.name,
|
|
609
|
+
right
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
const leftField = getLeafField(left.path);
|
|
613
|
+
const rightField = getLeafField(right.path);
|
|
614
|
+
const fieldReference = { _ref: rightField, _container: container };
|
|
615
|
+
|
|
616
|
+
// Two explicit nulls are EQUAL in CEL, and a null against a value is definitely NOT equal.
|
|
617
|
+
// A bare field reference gives UNKNOWN for both, which drops the row under either polarity.
|
|
618
|
+
const leftExplicit = isExplicitNullReference(left);
|
|
619
|
+
const rightExplicit = isExplicitNullReference(right);
|
|
620
|
+
|
|
621
|
+
if (operator === "eq" || operator === "ne") {
|
|
622
|
+
// Mixing the two conventions across one comparison has no faithful rendering. The declared
|
|
623
|
+
// side needs a definite answer for its NULL (CEL holds a null VALUE); the undeclared side
|
|
624
|
+
// needs UNKNOWN for its NULL (a missing attribute, which CEL denies under both polarities). A
|
|
625
|
+
// definite predicate returns rows the PDP refuses; a plain one drops rows the PDP allows.
|
|
626
|
+
// Refuse it rather than pick a direction — declare both attributes, or neither.
|
|
627
|
+
// Mixing the two conventions across one comparison: the explicit side's NULL is a null VALUE,
|
|
628
|
+
// which `!=` answers TRUE against any present value, while the other side's NULL is a missing
|
|
629
|
+
// attribute, an error under both polarities. `other = other` is TRUE for a present value and
|
|
630
|
+
// UNKNOWN for a NULL one, so it carries exactly that error into the explicit-null arm:
|
|
631
|
+
//
|
|
632
|
+
// explicit NULL, other present -> TRUE explicit NULL, other NULL -> UNKNOWN
|
|
633
|
+
// explicit present, other NULL -> UNKNOWN both present -> the plain comparison
|
|
634
|
+
//
|
|
635
|
+
// and `==` is its negation.
|
|
636
|
+
if (leftExplicit !== rightExplicit) {
|
|
637
|
+
const [explicitField, otherField] = leftExplicit
|
|
638
|
+
? [leftField, rightField]
|
|
639
|
+
: [rightField, leftField];
|
|
640
|
+
|
|
641
|
+
const otherReference = { _ref: otherField, _container: container };
|
|
642
|
+
|
|
643
|
+
const inequality: PrismaFilter = {
|
|
644
|
+
OR: [
|
|
645
|
+
{
|
|
646
|
+
AND: [
|
|
647
|
+
{ [explicitField]: null },
|
|
648
|
+
{ [otherField]: { equals: otherReference } },
|
|
649
|
+
],
|
|
650
|
+
},
|
|
651
|
+
{ NOT: { [explicitField]: { equals: otherReference } } },
|
|
652
|
+
],
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
return operator === "ne" ? inequality : { NOT: inequality };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (leftExplicit) {
|
|
659
|
+
const equality: PrismaFilter = {
|
|
660
|
+
OR: [
|
|
661
|
+
{ AND: [{ [leftField]: null }, { [rightField]: null }] },
|
|
662
|
+
{
|
|
663
|
+
AND: [
|
|
664
|
+
{ [leftField]: { not: null } },
|
|
665
|
+
{ [rightField]: { not: null } },
|
|
666
|
+
{ [leftField]: { equals: fieldReference } },
|
|
667
|
+
],
|
|
668
|
+
},
|
|
669
|
+
],
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
return operator === "eq" ? equality : { NOT: equality };
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// `ne` is the one operator whose Prisma spelling cannot carry a field reference: `not` nests a
|
|
677
|
+
// NestedStringFilter, which has no `_ref` form, so `{ col: { not: { _ref } } }` is rejected by
|
|
678
|
+
// the client rather than executed. Hoisting the negation to the top-level `NOT` combinator says
|
|
679
|
+
// the same thing with a shape that does accept one — the same rewrite the explicit-null branch
|
|
680
|
+
// above already performs. Three-valued logic is unchanged by the hoist: a NULL on either side
|
|
681
|
+
// makes the inner equality UNKNOWN, and `NOT UNKNOWN` is still UNKNOWN, so the row stays out
|
|
682
|
+
// under both polarities, which is what a missing attribute does on the check side.
|
|
683
|
+
if (operator === "ne") {
|
|
684
|
+
return { NOT: { [leftField]: { equals: fieldReference } } };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return { [leftField]: { [prismaOperator]: fieldReference } };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* The Prisma model both columns of a field-to-field comparison belong to: the relation model
|
|
692
|
+
* inside a lambda when both are element columns, otherwise the root model.
|
|
693
|
+
*/
|
|
694
|
+
function fieldReferenceContainer(
|
|
695
|
+
context: TranslationContext,
|
|
696
|
+
leftName: string,
|
|
697
|
+
left: ResolvedFieldReference,
|
|
698
|
+
rightName: string,
|
|
699
|
+
right: ResolvedFieldReference
|
|
700
|
+
): string {
|
|
701
|
+
const scope = currentScope(context);
|
|
702
|
+
|
|
703
|
+
if (scope) {
|
|
704
|
+
const prefix = scope.variableName + ".";
|
|
705
|
+
const leftInScope = leftName.startsWith(prefix);
|
|
706
|
+
const rightInScope = rightName.startsWith(prefix);
|
|
707
|
+
|
|
708
|
+
if (leftInScope !== rightInScope) {
|
|
709
|
+
throw new UnsupportedQueryPlanError(
|
|
710
|
+
`Cannot compare a collection element column with an outer column (${leftName} vs ${rightName}): ` +
|
|
711
|
+
"Prisma field references only work between fields of the same model"
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (leftInScope) {
|
|
716
|
+
if (!scope.relationModel) {
|
|
717
|
+
throw new Error(
|
|
718
|
+
"Field-to-field comparison inside a collection requires `relation.model` in the mapper"
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return scope.relationModel;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (
|
|
727
|
+
(left.relations && left.relations.length > 0) ||
|
|
728
|
+
(right.relations && right.relations.length > 0)
|
|
729
|
+
) {
|
|
730
|
+
throw new UnsupportedQueryPlanError(
|
|
731
|
+
"Cannot compare columns across relations: Prisma field references only work between fields of the same model"
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (!context.rootModel) {
|
|
736
|
+
throw new Error(
|
|
737
|
+
"Field-to-field comparison requires the `model` option (the Prisma model name) to build a field reference"
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return context.rootModel;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Translates `column ⊕ constant CMP value` (and its mirrored forms) by solving for the
|
|
746
|
+
* column: Prisma where-filters cannot express column arithmetic, but linear arithmetic with
|
|
747
|
+
* one constant side always rewrites to a plain comparison. Multiplying/dividing by a
|
|
748
|
+
* negative constant mirrors directional operators. The rewrite happens in IEEE double space,
|
|
749
|
+
* matching CEL: Cerbos attribute numbers are doubles, so this preserves check-time semantics
|
|
750
|
+
* (e.g. `n * 0.1 == 0.3` solves to an unrepresentable fraction and matches no integer row,
|
|
751
|
+
* exactly as CEL evaluates it).
|
|
752
|
+
*/
|
|
753
|
+
function handleArithmeticComparison(
|
|
754
|
+
operator: string,
|
|
755
|
+
arithExpr: OperatorOperand,
|
|
756
|
+
otherOperand: PlanExpressionOperand,
|
|
757
|
+
arithIsLeft: boolean,
|
|
758
|
+
context: TranslationContext
|
|
759
|
+
): PrismaFilter {
|
|
760
|
+
const arithOp = arithExpr.operator;
|
|
761
|
+
|
|
762
|
+
const arithLeftOp = assertDefined(
|
|
763
|
+
arithExpr.operands[0],
|
|
764
|
+
`${arithOp} operator requires a left operand`
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
const arithRightOp = assertDefined(
|
|
768
|
+
arithExpr.operands[1],
|
|
769
|
+
`${arithOp} operator requires a right operand`
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
if (
|
|
773
|
+
isOperatorOperand(otherOperand) &&
|
|
774
|
+
ARITHMETIC_OPERATORS.has(otherOperand.operator) &&
|
|
775
|
+
tryFoldValueExpression(otherOperand, context) === null
|
|
776
|
+
) {
|
|
777
|
+
throw new UnsupportedQueryPlanError(
|
|
778
|
+
"Arithmetic on both sides of a comparison is not supported: the expression cannot be solved to a plain column filter"
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const arithLeft = resolveOperand(arithLeftOp, context);
|
|
783
|
+
const arithRight = resolveOperand(arithRightOp, context);
|
|
784
|
+
const other = resolveOperand(otherOperand, context);
|
|
785
|
+
|
|
786
|
+
// Fully constant arithmetic: fold and compare the other side against the result. The
|
|
787
|
+
// arithmetic side keeps its source position, so when the folded constant was written
|
|
788
|
+
// FIRST (`1 + 2 < f`), directional operators mirror to keep the field on the left.
|
|
789
|
+
if (isResolvedValue(arithLeft) && isResolvedValue(arithRight)) {
|
|
790
|
+
const folded = foldArithmetic(arithOp, arithLeft.value, arithRight.value);
|
|
791
|
+
|
|
792
|
+
if (!isResolvedFieldReference(other)) {
|
|
793
|
+
throw new UnsupportedQueryPlanError(
|
|
794
|
+
`${arithOp} with two values requires a field reference on the other side`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return buildComparisonFilter(
|
|
799
|
+
context,
|
|
800
|
+
other,
|
|
801
|
+
arithIsLeft ? mirrorOperator(operator) : operator,
|
|
802
|
+
folded
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (!isResolvedValue(other)) {
|
|
807
|
+
throw new UnsupportedQueryPlanError(
|
|
808
|
+
`${arithOp} operator with field references requires a value on the other side of the comparison`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
let fieldRef: ResolvedFieldReference;
|
|
813
|
+
let constant: Value;
|
|
814
|
+
let fieldIsLeft: boolean;
|
|
815
|
+
|
|
816
|
+
if (isResolvedFieldReference(arithLeft) && isResolvedValue(arithRight)) {
|
|
817
|
+
fieldRef = arithLeft;
|
|
818
|
+
constant = arithRight.value;
|
|
819
|
+
fieldIsLeft = true;
|
|
820
|
+
} else if (
|
|
821
|
+
isResolvedValue(arithLeft) &&
|
|
822
|
+
isResolvedFieldReference(arithRight)
|
|
823
|
+
) {
|
|
824
|
+
fieldRef = arithRight;
|
|
825
|
+
constant = arithLeft.value;
|
|
826
|
+
fieldIsLeft = false;
|
|
827
|
+
} else {
|
|
828
|
+
throw new UnsupportedQueryPlanError(
|
|
829
|
+
`${arithOp} operator requires exactly one field reference and one value, or two values`
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// The comparison operator was normalized arithmetic-side-first by the caller; if the
|
|
834
|
+
// arithmetic was written on the RIGHT of the comparison, direction was already mirrored.
|
|
835
|
+
let effectiveOperator = arithIsLeft ? operator : mirrorOperator(operator);
|
|
836
|
+
|
|
837
|
+
const declared = fieldRef.valueType;
|
|
838
|
+
const declaredString = declared === "string";
|
|
839
|
+
|
|
840
|
+
if (
|
|
841
|
+
(isString(constant) && declared !== undefined && !declaredString) ||
|
|
842
|
+
(isNumber(constant) &&
|
|
843
|
+
(declaredString || declared === "boolean" || declared === "dateTime"))
|
|
844
|
+
) {
|
|
845
|
+
throw new UnsupportedQueryPlanError(
|
|
846
|
+
`${arithOp} over a ${declared} field and a ${typeofValue(constant)} is a CEL no-overload error`
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// String concatenation solving (eq/ne only).
|
|
851
|
+
if (arithOp === "add" && isString(constant)) {
|
|
852
|
+
if (effectiveOperator !== "eq" && effectiveOperator !== "ne") {
|
|
853
|
+
throw new UnsupportedQueryPlanError(
|
|
854
|
+
`Operator ${effectiveOperator} is not supported with string concatenation`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const solvedValue = solveAdd(other.value, constant, fieldIsLeft);
|
|
859
|
+
|
|
860
|
+
if (solvedValue === null) {
|
|
861
|
+
// A contradictory / exhaustive pair remains SQL UNKNOWN for a missing field.
|
|
862
|
+
// An empty IN or an empty filter loses that error when an outer NOT is applied.
|
|
863
|
+
const equality = buildFieldFilter(fieldRef, "equals", other.value);
|
|
864
|
+
const inequality = buildFieldFilter(fieldRef, "not", other.value);
|
|
865
|
+
|
|
866
|
+
return effectiveOperator === "eq"
|
|
867
|
+
? { AND: [equality, inequality] }
|
|
868
|
+
: { OR: [equality, inequality] };
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// null + "x" is a CEL error, so the solved comparison stays UNKNOWN for a NULL column even
|
|
872
|
+
// where the field is sent as an explicit null.
|
|
873
|
+
const erroring = { ...fieldRef, nullAttributeRepresentation: "omitted" as const };
|
|
874
|
+
|
|
875
|
+
return buildComparisonFilter(context, erroring, effectiveOperator, solvedValue);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (!isNumber(constant) || !isNumber(other.value)) {
|
|
879
|
+
throw new UnsupportedQueryPlanError(`${arithOp} comparison requires numeric operands`);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (arithOp === "mult" && constant === 0) {
|
|
883
|
+
throw new UnsupportedQueryPlanError(
|
|
884
|
+
"Multiplication by a constant zero must be folded by the Cerbos planner"
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (arithOp === "div" && !fieldIsLeft) {
|
|
889
|
+
throw new UnsupportedQueryPlanError(
|
|
890
|
+
"Division by a column is not supported: the comparison cannot be solved to a plain column filter"
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (arithOp === "div" && constant === 0) {
|
|
895
|
+
throw new UnsupportedQueryPlanError("Division by a constant zero is not supported");
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (!ARITHMETIC_OPERATORS.has(arithOp)) {
|
|
899
|
+
throw new UnsupportedQueryPlanError(`Unsupported operator: ${arithOp}`);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// The column side as a non-decreasing function of `y`, where `y` is the column or, when the
|
|
903
|
+
// arithmetic reverses the order (`c - x`, a negative multiplier or divisor), its negation —
|
|
904
|
+
// negating a double is exact, so `c - x` is `c + y` and `x * c` is `y * -c`.
|
|
905
|
+
const c = constant;
|
|
906
|
+
|
|
907
|
+
const decreasing =
|
|
908
|
+
(arithOp === "sub" && !fieldIsLeft) || ((arithOp === "mult" || arithOp === "div") && c < 0);
|
|
909
|
+
|
|
910
|
+
const f = (y: number): number => {
|
|
911
|
+
switch (arithOp) {
|
|
912
|
+
case "add":
|
|
913
|
+
return y + c;
|
|
914
|
+
case "sub":
|
|
915
|
+
return fieldIsLeft ? y - c : c + y;
|
|
916
|
+
case "mult":
|
|
917
|
+
return decreasing ? y * -c : y * c;
|
|
918
|
+
default:
|
|
919
|
+
return decreasing ? y / -c : y / c;
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
const solvedOperator = effectiveOperator === "ne" ? "eq" : effectiveOperator;
|
|
924
|
+
|
|
925
|
+
if (
|
|
926
|
+
solvedOperator !== "eq" &&
|
|
927
|
+
solvedOperator !== "lt" &&
|
|
928
|
+
solvedOperator !== "le" &&
|
|
929
|
+
solvedOperator !== "gt" &&
|
|
930
|
+
solvedOperator !== "ge"
|
|
931
|
+
) {
|
|
932
|
+
throw new UnsupportedQueryPlanError(`Operator ${effectiveOperator} is not supported with arithmetic`);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const solved = solveMonotone(f, solvedOperator, other.value);
|
|
936
|
+
|
|
937
|
+
const interval = decreasing ? negateInterval(solved) : solved;
|
|
938
|
+
const filter = buildIntervalFilter(context, fieldRef, interval);
|
|
939
|
+
|
|
940
|
+
return effectiveOperator === "ne" ? { NOT: filter } : filter;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* `lo <= column <= hi`. An empty interval is a contradiction and an unbounded one a tautology,
|
|
945
|
+
* both spelled on the column, so a NULL keeps them UNKNOWN as the arithmetic's error requires.
|
|
946
|
+
*/
|
|
947
|
+
function buildIntervalFilter(
|
|
948
|
+
context: TranslationContext,
|
|
949
|
+
fieldRef: ResolvedFieldReference,
|
|
950
|
+
interval: DoubleInterval
|
|
951
|
+
): PrismaFilter {
|
|
952
|
+
if (interval.empty) {
|
|
953
|
+
return {
|
|
954
|
+
AND: [
|
|
955
|
+
buildComparisonFilter(context, fieldRef, "gt", 0),
|
|
956
|
+
buildComparisonFilter(context, fieldRef, "le", 0),
|
|
957
|
+
],
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const bounds: PrismaFilter[] = [];
|
|
962
|
+
|
|
963
|
+
if (interval.lo !== undefined) {
|
|
964
|
+
bounds.push(buildComparisonFilter(context, fieldRef, "ge", interval.lo));
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (interval.hi !== undefined) {
|
|
968
|
+
bounds.push(buildComparisonFilter(context, fieldRef, "le", interval.hi));
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (bounds.length === 0) {
|
|
972
|
+
return {
|
|
973
|
+
OR: [
|
|
974
|
+
buildComparisonFilter(context, fieldRef, "gt", 0),
|
|
975
|
+
buildComparisonFilter(context, fieldRef, "le", 0),
|
|
976
|
+
],
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Each bound may itself be a bracket (see fractionalBracket); one flat AND reads better.
|
|
981
|
+
const flat = bounds.flatMap((bound) => {
|
|
982
|
+
const conjuncts = bound["AND"];
|
|
983
|
+
|
|
984
|
+
return Object.keys(bound).length === 1 && Array.isArray(conjuncts) ? conjuncts.filter(isPrismaFilter) : [bound];
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
return flat.length === 1 ? flat[0]! : { AND: flat };
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* The column value that makes `column + addConstant` (or `addConstant + column`) equal
|
|
992
|
+
* `comparisonValue`, or null when no string does.
|
|
993
|
+
*/
|
|
994
|
+
function solveAdd(
|
|
995
|
+
comparisonValue: Value,
|
|
996
|
+
addConstant: Value,
|
|
997
|
+
fieldIsLeft: boolean
|
|
998
|
+
): Value | null {
|
|
999
|
+
if (isString(comparisonValue) && isString(addConstant)) {
|
|
1000
|
+
if (fieldIsLeft) {
|
|
1001
|
+
if (!comparisonValue.endsWith(addConstant)) return null;
|
|
1002
|
+
|
|
1003
|
+
return comparisonValue.slice(
|
|
1004
|
+
0,
|
|
1005
|
+
comparisonValue.length - addConstant.length
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (!comparisonValue.startsWith(addConstant)) return null;
|
|
1010
|
+
|
|
1011
|
+
return comparisonValue.slice(addConstant.length);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
if (isNumber(comparisonValue) && isNumber(addConstant)) {
|
|
1015
|
+
return comparisonValue - addConstant;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
throw new UnsupportedQueryPlanError("Type mismatch in add comparison");
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Helper function to handle "in" operator
|
|
1023
|
+
*/
|
|
1024
|
+
export function handleInOperator(
|
|
1025
|
+
operands: PlanExpressionOperand[],
|
|
1026
|
+
context: TranslationContext
|
|
1027
|
+
): PrismaFilter {
|
|
1028
|
+
if (operands.length !== 2) {
|
|
1029
|
+
throw new UnsupportedQueryPlanError("in requires exactly two operands");
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const ternaryFilter = tryHandleTernaryComparison("in", operands, context);
|
|
1033
|
+
|
|
1034
|
+
if (ternaryFilter) {
|
|
1035
|
+
return ternaryFilter;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const member = assertDefined(operands[0], "in requires a member operand");
|
|
1039
|
+
|
|
1040
|
+
const collection = assertDefined(
|
|
1041
|
+
operands[1],
|
|
1042
|
+
"in requires a collection operand"
|
|
1043
|
+
);
|
|
1044
|
+
|
|
1045
|
+
if (isNamedOperand(member) && isValueOperand(collection)) {
|
|
1046
|
+
if (!Array.isArray(collection.value)) throw scalarMembership();
|
|
1047
|
+
|
|
1048
|
+
return buildMembershipFilter(context, resolveFieldReference(member.name, context), collection.value);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
if (isValueOperand(member) && isNamedOperand(collection)) {
|
|
1052
|
+
if (namesToOneRelation(context.mapper, collection.name)) {
|
|
1053
|
+
throw new UnsupportedQueryPlanError(
|
|
1054
|
+
`in over ${collection.name}: it is a to-one relation, which CEL reads as a map, and ` +
|
|
1055
|
+
"`in` over a map tests its keys (the attribute names present on the related row); " +
|
|
1056
|
+
"a Prisma relation filter tests rows, not attribute names"
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
const fieldRef = resolveFieldReference(collection.name, context);
|
|
1061
|
+
|
|
1062
|
+
if (!fieldRef.relations?.length && fieldRef.valueType !== undefined) throw scalarMembership();
|
|
1063
|
+
|
|
1064
|
+
return buildMembershipFilter(context, fieldRef, [member.value]);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (isOperatorOperand(collection) && collection.operator === "list") {
|
|
1068
|
+
return handleListMembership(member, collection.operands, context);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (isOperatorOperand(collection) && collection.operator === "add") {
|
|
1072
|
+
throw new UnsupportedQueryPlanError(
|
|
1073
|
+
`Membership in a list built by ${collection.operator}(...) from columns: each element is ` +
|
|
1074
|
+
"compared with the member, and a Prisma field reference compares two plain columns of " +
|
|
1075
|
+
"one model, never a column with an expression over another column or with the " +
|
|
1076
|
+
"elements of a related list"
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
if (isNamedOperand(member) && isNamedOperand(collection)) {
|
|
1081
|
+
throw new UnsupportedQueryPlanError(
|
|
1082
|
+
"Membership between two database attributes is not supported: Prisma cannot compare a related element column with an outer scalar column"
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
throw new UnsupportedQueryPlanError(
|
|
1087
|
+
"in requires a field member and literal list, or a literal member and field collection"
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function scalarMembership(): UnsupportedQueryPlanError {
|
|
1092
|
+
return new UnsupportedQueryPlanError(
|
|
1093
|
+
"in over a scalar is a CEL error, which a Prisma filter cannot express"
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* `x in [a, b, ...]` over a list the planner could not fold because an element is a column. CEL
|
|
1099
|
+
* builds the list first, which raises an error as soon as one element is a missing attribute, and
|
|
1100
|
+
* then tests `x` against each element for equality, so the membership is the OR of the comparisons
|
|
1101
|
+
* `x == e`, each translated as that comparison would be. With several elements, an element column
|
|
1102
|
+
* the caller omits when NULL gets its presence test ANDed on, TRUE when the value is there and
|
|
1103
|
+
* UNKNOWN when the column is NULL, so a missing element leaves the whole membership UNKNOWN even
|
|
1104
|
+
* where another element matches. One element needs no test: its equality is already UNKNOWN on a
|
|
1105
|
+
* NULL column. An element that is neither a literal nor a column has no presence test, so the
|
|
1106
|
+
* membership is refused rather than let its error be absorbed by a matching sibling.
|
|
1107
|
+
*/
|
|
1108
|
+
function handleListMembership(
|
|
1109
|
+
member: PlanExpressionOperand,
|
|
1110
|
+
elements: PlanExpressionOperand[],
|
|
1111
|
+
context: TranslationContext
|
|
1112
|
+
): PrismaFilter {
|
|
1113
|
+
if (elements.length === 0) {
|
|
1114
|
+
throw new UnsupportedQueryPlanError("Membership in an empty list must be folded by the Cerbos planner");
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const equalities = elements.map((element) =>
|
|
1118
|
+
buildPrismaFilterFromCerbosExpression(
|
|
1119
|
+
{ operator: "eq", operands: [member, element] },
|
|
1120
|
+
context
|
|
1121
|
+
)
|
|
1122
|
+
);
|
|
1123
|
+
|
|
1124
|
+
const filter = equalities.length === 1 ? equalities[0]! : { OR: equalities };
|
|
1125
|
+
|
|
1126
|
+
if (elements.length === 1) return filter;
|
|
1127
|
+
|
|
1128
|
+
const presences: PrismaFilter[] = [];
|
|
1129
|
+
|
|
1130
|
+
for (const element of elements) {
|
|
1131
|
+
if (isValueOperand(element)) continue;
|
|
1132
|
+
|
|
1133
|
+
if (!isNamedOperand(element)) {
|
|
1134
|
+
throw new UnsupportedQueryPlanError(
|
|
1135
|
+
"Membership in a list holding an expression beside other elements: building the list " +
|
|
1136
|
+
"raises the expression's error even where another element matches, and no Prisma " +
|
|
1137
|
+
"filter carries that error for an expression"
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const fieldRef = resolveFieldReference(element.name, context);
|
|
1142
|
+
|
|
1143
|
+
if (isExplicitNullReference(fieldRef)) continue;
|
|
1144
|
+
const test = presence(element, fieldRef);
|
|
1145
|
+
|
|
1146
|
+
if (test === undefined) {
|
|
1147
|
+
throw new UnsupportedQueryPlanError(
|
|
1148
|
+
`Membership in a list holding ${element.name} beside other elements requires the ` +
|
|
1149
|
+
"column's valueType in the mapper, to spell the presence test a missing element needs"
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
presences.push(buildPrismaFilterFromCerbosExpression(test, context));
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
return presences.length === 0 ? filter : { AND: [...presences, filter] };
|
|
1157
|
+
}
|