@cerbos/orm-prisma 4.0.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 +611 -460
- 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 +91 -8
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +34 -2301
- 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 +31 -18
- package/src/adversarial.test.ts +488 -488
- package/src/collections.ts +738 -0
- package/src/comparison.ts +1157 -0
- package/src/corpus.ts +597 -0
- 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 +20 -0
- package/src/generated/prisma-adversarial/client.ts +20 -0
- package/src/generated/prisma-adversarial/commonInputTypes.ts +116 -127
- package/src/generated/prisma-adversarial/internal/class.ts +44 -4
- package/src/generated/prisma-adversarial/internal/prismaNamespace.ts +350 -3
- package/src/generated/prisma-adversarial/internal/prismaNamespaceBrowser.ts +49 -2
- package/src/generated/prisma-adversarial/models/AdversarialBoolListElement.ts +1272 -0
- package/src/generated/prisma-adversarial/models/AdversarialInner.ts +1369 -0
- package/src/generated/prisma-adversarial/models/AdversarialNumberListElement.ts +1316 -0
- package/src/generated/prisma-adversarial/models/AdversarialParent.ts +1474 -0
- package/src/generated/prisma-adversarial/models/AdversarialResource.ts +547 -75
- package/src/generated/prisma-adversarial/models.ts +4 -0
- package/src/generated/prisma-adversarial-mysql/browser.ts +64 -0
- package/src/generated/{prisma → prisma-adversarial-mysql}/client.ts +23 -18
- package/src/generated/{prisma → prisma-adversarial-mysql}/commonInputTypes.ts +167 -92
- 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/index.d.ts +13822 -0
- package/src/generated/prisma-adversarial-pg/browser.ts +64 -0
- package/src/generated/prisma-adversarial-pg/client.ts +86 -0
- package/src/generated/prisma-adversarial-pg/commonInputTypes.ts +345 -0
- package/src/generated/prisma-adversarial-pg/enums.ts +15 -0
- package/src/generated/prisma-adversarial-pg/internal/class.ts +284 -0
- package/src/generated/prisma-adversarial-pg/internal/prismaNamespace.ts +1542 -0
- package/src/generated/{prisma → prisma-adversarial-pg}/internal/prismaNamespaceBrowser.ts +72 -43
- package/src/generated/prisma-adversarial-pg/models/AdversarialBoolListElement.ts +1275 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialCategory.ts +1404 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialInner.ts +1371 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialLabel.ts +1275 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialNumberListElement.ts +1319 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialParent.ts +1476 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialResource.ts +2139 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialSubCategory.ts +1404 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialTag.ts +1310 -0
- package/src/generated/prisma-adversarial-pg/models.ts +20 -0
- package/src/generated/prisma-adversarial-pg-v6/client.d.ts +1 -0
- package/src/generated/prisma-adversarial-pg-v6/default.d.ts +1 -0
- package/src/generated/prisma-adversarial-pg-v6/edge.d.ts +1 -0
- package/src/generated/prisma-adversarial-pg-v6/index.d.ts +14918 -0
- package/src/generated/prisma-adversarial-pg-v6/runtime/index-browser.d.ts +370 -0
- package/src/generated/prisma-adversarial-pg-v6/runtime/library.d.ts +3982 -0
- package/src/generated/prisma-adversarial-pg-v6/wasm.d.ts +1 -0
- package/src/generated/prisma-adversarial-v6/index.d.ts +6515 -304
- package/src/hierarchy.ts +405 -0
- package/src/index.ts +134 -3325
- 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 +21 -0
- 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.pg.v6.ts +13 -0
- package/src/test-setup.adversarial.pg.v7.ts +9 -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 +766 -0
- package/src/types.ts +1310 -0
- package/src/value.ts +16 -0
- package/src/generated/prisma/browser.ts +0 -59
- package/src/generated/prisma/internal/class.ts +0 -274
- package/src/generated/prisma/internal/prismaNamespace.ts +0 -1403
- package/src/generated/prisma/models/Category.ts +0 -1412
- package/src/generated/prisma/models/Label.ts +0 -1276
- package/src/generated/prisma/models/NestedResource.ts +0 -1562
- package/src/generated/prisma/models/NextLevelNestedResource.ts +0 -1337
- package/src/generated/prisma/models/Resource.ts +0 -2196
- package/src/generated/prisma/models/SubCategory.ts +0 -1412
- package/src/generated/prisma/models/Tag.ts +0 -1276
- package/src/generated/prisma/models/User.ts +0 -1503
- package/src/generated/prisma/models.ts +0 -19
- package/src/generated/prisma-v6/index.d.ts +0 -13726
- package/src/index.test.ts +0 -8885
- package/src/test-setup.v6.ts +0 -5
- package/src/test-setup.v7.ts +0 -7
- /package/src/generated/{prisma → prisma-adversarial-mysql}/enums.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/client.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/default.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/edge.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/runtime/index-browser.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/runtime/library.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/wasm.d.ts +0 -0
package/src/translate.ts
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
// The translator's core: one dispatch from a plan operator to its handler, the negation that
|
|
2
|
+
// every handler shares, and the resolution of an operand into a column or a constant.
|
|
3
|
+
|
|
4
|
+
import type { PlanExpressionOperand, Value } from "@cerbos/core";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
buildNegatedCollectionFilter,
|
|
8
|
+
handleCollectionOperator,
|
|
9
|
+
handleHasIntersectionOperator,
|
|
10
|
+
handleLambdaOperator,
|
|
11
|
+
handleMapOperator,
|
|
12
|
+
} from "./collections";
|
|
13
|
+
import {
|
|
14
|
+
containsMapLiteralLookup,
|
|
15
|
+
handleInOperator,
|
|
16
|
+
handleRelationalOperator,
|
|
17
|
+
tryHandleMapLiteralLookup,
|
|
18
|
+
} from "./comparison";
|
|
19
|
+
import { ARITHMETIC_OPERATORS, foldArithmetic } from "./evaluate";
|
|
20
|
+
import { buildFieldFilter } from "./fields";
|
|
21
|
+
import {
|
|
22
|
+
handleAncestorDescendantOperator,
|
|
23
|
+
handleOverlapsOperator,
|
|
24
|
+
} from "./hierarchy";
|
|
25
|
+
import type { PrismaFilter } from "./index";
|
|
26
|
+
import { isResolvedValue, lookupMapping, resolveFieldReference } from "./mapping";
|
|
27
|
+
import type { ResolvedOperand, TranslationContext } from "./mapping";
|
|
28
|
+
import {
|
|
29
|
+
assertDefined,
|
|
30
|
+
assertLogicalOperands,
|
|
31
|
+
isNamedOperand,
|
|
32
|
+
isOperatorOperand,
|
|
33
|
+
isValueOperand,
|
|
34
|
+
} from "./plan";
|
|
35
|
+
import type { OperatorOperand } from "./plan";
|
|
36
|
+
import { handleHasOperator } from "./presence";
|
|
37
|
+
import { negateRequiringHops, referencesRequiredHop } from "./relations";
|
|
38
|
+
import { containsCollectionOperator } from "./rewrite";
|
|
39
|
+
import { handleMatchesOperator } from "./regex";
|
|
40
|
+
import { handleStringOperator } from "./strings";
|
|
41
|
+
import { handleBooleanTernaryOperator, tryHandleTernaryComparison } from "./ternary";
|
|
42
|
+
import { normalizeRfc3339Milliseconds, parseRfc3339Instant } from "./timestamp";
|
|
43
|
+
import { UnsupportedQueryPlanError } from "./errors";
|
|
44
|
+
import { isString, lookup, triple } from "./narrowing";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Builds a Prisma filter from a Cerbos expression.
|
|
48
|
+
*/
|
|
49
|
+
export function buildPrismaFilterFromCerbosExpression(
|
|
50
|
+
expression: PlanExpressionOperand,
|
|
51
|
+
context: TranslationContext
|
|
52
|
+
): PrismaFilter {
|
|
53
|
+
// A bare named operand represents a boolean field reference (e.g. `R.attr.booleanAttr`)
|
|
54
|
+
if (isNamedOperand(expression)) {
|
|
55
|
+
return buildFieldFilter(
|
|
56
|
+
booleanFieldReference(expression.name, context),
|
|
57
|
+
"equals",
|
|
58
|
+
true
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!isOperatorOperand(expression)) {
|
|
63
|
+
throw new UnsupportedQueryPlanError("Invalid Cerbos expression structure");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Every plan operator this adapter translates. Adding an operator is adding a case here;
|
|
67
|
+
// anything else is refused as unsupported.
|
|
68
|
+
const { operator, operands } = expression;
|
|
69
|
+
assertLogicalOperands(operator, operands);
|
|
70
|
+
|
|
71
|
+
switch (operator) {
|
|
72
|
+
case "and":
|
|
73
|
+
return {
|
|
74
|
+
AND: operands.map((operand) =>
|
|
75
|
+
buildPrismaFilterFromCerbosExpression(operand, context)
|
|
76
|
+
),
|
|
77
|
+
};
|
|
78
|
+
case "or":
|
|
79
|
+
return {
|
|
80
|
+
OR: operands.map((operand) =>
|
|
81
|
+
buildPrismaFilterFromCerbosExpression(operand, context)
|
|
82
|
+
),
|
|
83
|
+
};
|
|
84
|
+
case "not": {
|
|
85
|
+
const operand = operands[0];
|
|
86
|
+
|
|
87
|
+
if (!operand) {
|
|
88
|
+
throw new UnsupportedQueryPlanError("not operator requires an operand");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return buildNegatedFilter(operand, context);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case "if":
|
|
95
|
+
return handleBooleanTernaryOperator(operands, context);
|
|
96
|
+
case "eq":
|
|
97
|
+
case "ne":
|
|
98
|
+
case "lt":
|
|
99
|
+
case "le":
|
|
100
|
+
case "gt":
|
|
101
|
+
case "ge":
|
|
102
|
+
return handleRelationalOperator(operator, operands, context);
|
|
103
|
+
case "in":
|
|
104
|
+
return handleInOperator(operands, context);
|
|
105
|
+
case "contains":
|
|
106
|
+
case "startsWith":
|
|
107
|
+
case "endsWith":
|
|
108
|
+
return handleStringOperator(operator, operands, context);
|
|
109
|
+
case "matches":
|
|
110
|
+
return handleMatchesOperator(operands, context);
|
|
111
|
+
case "has":
|
|
112
|
+
return handleHasOperator(operands, context, false);
|
|
113
|
+
case "hasIntersection":
|
|
114
|
+
return handleHasIntersectionOperator(operands, context);
|
|
115
|
+
case "lambda":
|
|
116
|
+
return handleLambdaOperator(operands, context);
|
|
117
|
+
case "exists":
|
|
118
|
+
case "exists_one":
|
|
119
|
+
case "all":
|
|
120
|
+
case "except":
|
|
121
|
+
case "filter":
|
|
122
|
+
return handleCollectionOperator(operator, operands, context);
|
|
123
|
+
case "map":
|
|
124
|
+
return handleMapOperator(operands, context);
|
|
125
|
+
case "overlaps":
|
|
126
|
+
return handleOverlapsOperator(operands, context);
|
|
127
|
+
case "ancestorOf":
|
|
128
|
+
return handleAncestorDescendantOperator(operands, context, "ancestor");
|
|
129
|
+
case "descendentOf":
|
|
130
|
+
return handleAncestorDescendantOperator(operands, context, "descendant");
|
|
131
|
+
default:
|
|
132
|
+
throw new UnsupportedQueryPlanError(`Unsupported operator: ${operator}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Builds the filter for `!expr`. Plain field predicates negate correctly with Prisma's NOT
|
|
138
|
+
* (SQL three-valued logic keeps NULL rows excluded under both polarities), but relation
|
|
139
|
+
* subqueries collapse UNKNOWN to false at the EXISTS boundary, so `NOT(some(P))` would
|
|
140
|
+
* wrongly include rows whose elements make P UNKNOWN (a NULL element column is a missing
|
|
141
|
+
* attribute — a CEL error — on the check side, which Cerbos treats as deny). Negation is
|
|
142
|
+
* therefore pushed down to the collection operators, which encode CEL's exact error
|
|
143
|
+
* semantics:
|
|
144
|
+
*
|
|
145
|
+
* - exists(c,P) is TRUE with a true witness, FALSE if every element is definitively false,
|
|
146
|
+
* and an ERROR otherwise — so `!exists` admits only rows with no P-match AND no
|
|
147
|
+
* UNKNOWN-element (the nullable-field guard).
|
|
148
|
+
* - all(c,P) is FALSE only with a definitive false witness (a false witness absorbs error
|
|
149
|
+
* elements) — so `!all` is `some(NOT P)`, which SQL already evaluates definitively.
|
|
150
|
+
*/
|
|
151
|
+
/**
|
|
152
|
+
* A field read where CEL needs a boolean. A field declared another type is a no-overload error,
|
|
153
|
+
* which no Prisma filter keeps out under both polarities, so it is refused.
|
|
154
|
+
*/
|
|
155
|
+
function booleanFieldReference(name: string, context: TranslationContext) {
|
|
156
|
+
if (lookupMapping(context.mapper, name)?.relation?.type === "many") {
|
|
157
|
+
throw new UnsupportedQueryPlanError("A list in boolean position is a CEL type error");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const fieldRef = resolveFieldReference(name, context);
|
|
161
|
+
|
|
162
|
+
if (fieldRef.valueType !== undefined && fieldRef.valueType !== "boolean") {
|
|
163
|
+
throw new UnsupportedQueryPlanError(
|
|
164
|
+
`A ${fieldRef.valueType} field in boolean position is a CEL type error`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return fieldRef;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function buildNegatedFilter(
|
|
172
|
+
operand: PlanExpressionOperand,
|
|
173
|
+
context: TranslationContext
|
|
174
|
+
): PrismaFilter {
|
|
175
|
+
if (isOperatorOperand(operand)) {
|
|
176
|
+
if (operand.operator === "not") {
|
|
177
|
+
return buildPrismaFilterFromCerbosExpression(
|
|
178
|
+
assertDefined(operand.operands[0], "not requires an operand"),
|
|
179
|
+
context
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (operand.operator === "has") {
|
|
184
|
+
return handleHasOperator(operand.operands, context, true);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const ternary = tryHandleTernaryComparison(
|
|
188
|
+
operand.operator,
|
|
189
|
+
operand.operands,
|
|
190
|
+
context,
|
|
191
|
+
true
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
if (ternary !== null) return ternary;
|
|
195
|
+
|
|
196
|
+
const lookup = tryHandleMapLiteralLookup(
|
|
197
|
+
operand.operator,
|
|
198
|
+
operand.operands,
|
|
199
|
+
context,
|
|
200
|
+
true
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (lookup !== null) return lookup;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (isNamedOperand(operand)) {
|
|
207
|
+
const { relations, ...fieldRef } = booleanFieldReference(operand.name, context);
|
|
208
|
+
|
|
209
|
+
if (!relations || relations.length === 0) {
|
|
210
|
+
return buildFieldFilter(fieldRef, "equals", false);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return negateRequiringHops(
|
|
214
|
+
operand,
|
|
215
|
+
buildPrismaFilterFromCerbosExpression(operand, context),
|
|
216
|
+
context
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// `and`/`or` must be pushed through with De Morgan whenever a branch can be UNKNOWN, so
|
|
221
|
+
// CEL's error absorption survives: `!(A && B)` with A erroring and B false is TRUE in CEL,
|
|
222
|
+
// and `OR[!A, !B]` reproduces that where a single outer NOT over the conjunction — with the
|
|
223
|
+
// hop requirement ANDed outside it — would deny. A to-one hop is the second source of UNKNOWN
|
|
224
|
+
// besides the collection macros, so it opens the same push-down, and each leaf requires only
|
|
225
|
+
// the hops it reads itself. (A nested `not` was already unwrapped above.) A lookup into a map literal
|
|
226
|
+
// is the third: a missing key is an error, which the comparison's own negation leaves out.
|
|
227
|
+
if (
|
|
228
|
+
isOperatorOperand(operand) &&
|
|
229
|
+
(containsCollectionOperator(operand) ||
|
|
230
|
+
containsMapLiteralLookup(operand) ||
|
|
231
|
+
referencesRequiredHop(operand, context))
|
|
232
|
+
) {
|
|
233
|
+
switch (operand.operator) {
|
|
234
|
+
case "and":
|
|
235
|
+
return {
|
|
236
|
+
OR: operand.operands.map((o) => buildNegatedFilter(o, context)),
|
|
237
|
+
};
|
|
238
|
+
case "or":
|
|
239
|
+
return {
|
|
240
|
+
AND: operand.operands.map((o) => buildNegatedFilter(o, context)),
|
|
241
|
+
};
|
|
242
|
+
// `!(c ? a : b)` is `c ? !a : !b`, errors included: a condition that errors errors either
|
|
243
|
+
// way. A NOT over the guarded-branch disjunction instead selects the rows where the
|
|
244
|
+
// condition is UNKNOWN, which neither guarded branch matched.
|
|
245
|
+
case "if":
|
|
246
|
+
if (operand.operands.length === 3) {
|
|
247
|
+
const [condition, thenBranch, elseBranch] = triple(operand.operands);
|
|
248
|
+
|
|
249
|
+
return handleBooleanTernaryOperator(
|
|
250
|
+
[
|
|
251
|
+
condition,
|
|
252
|
+
{ operator: "not", operands: [thenBranch] },
|
|
253
|
+
{ operator: "not", operands: [elseBranch] },
|
|
254
|
+
],
|
|
255
|
+
context
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
break;
|
|
260
|
+
case "exists":
|
|
261
|
+
case "all":
|
|
262
|
+
case "except":
|
|
263
|
+
case "exists_one":
|
|
264
|
+
case "filter":
|
|
265
|
+
return buildNegatedCollectionFilter(
|
|
266
|
+
operand.operator,
|
|
267
|
+
operand.operands,
|
|
268
|
+
context
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (containsMapLiteralLookup(operand)) {
|
|
273
|
+
// A missing key is an error under both polarities, which a NOT over the positive filter
|
|
274
|
+
// would turn into a match; only and/or and the comparison itself push the negation down.
|
|
275
|
+
throw new UnsupportedQueryPlanError(
|
|
276
|
+
`Cannot negate ${operand.operator} over an index into a map literal: a missing key is an ` +
|
|
277
|
+
"error under both polarities, and a NOT over its filter would select it"
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return negateRequiringHops(
|
|
283
|
+
operand,
|
|
284
|
+
buildPrismaFilterFromCerbosExpression(operand, context),
|
|
285
|
+
context
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Resolves a plan operand into a column reference or a constant. A nested expression that is
|
|
291
|
+
* neither a timestamp nor foldable arithmetic resolves to the filter it translates to.
|
|
292
|
+
*
|
|
293
|
+
* A timestamp literal between two milliseconds is refused unless `allowSubMillisecond` is set, by
|
|
294
|
+
* a caller that compares it against a column and applies roundSubMillisecond.
|
|
295
|
+
*/
|
|
296
|
+
export function resolveOperand(
|
|
297
|
+
operand: PlanExpressionOperand,
|
|
298
|
+
context: TranslationContext,
|
|
299
|
+
allowSubMillisecond = false
|
|
300
|
+
): ResolvedOperand {
|
|
301
|
+
if (isNamedOperand(operand)) {
|
|
302
|
+
return resolveFieldReference(operand.name, context);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (isValueOperand(operand)) {
|
|
306
|
+
return { value: operand.value };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (isOperatorOperand(operand)) {
|
|
310
|
+
if (operand.operator === "timestamp") {
|
|
311
|
+
return resolveTimestampOperand(operand, context, allowSubMillisecond);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const identity = resolveIdentityCast(operand, context);
|
|
315
|
+
|
|
316
|
+
if (identity !== undefined) return identity;
|
|
317
|
+
const folded = tryFoldValueExpression(operand, context);
|
|
318
|
+
|
|
319
|
+
if (folded !== null) return { value: folded };
|
|
320
|
+
|
|
321
|
+
return { value: buildPrismaFilterFromCerbosExpression(operand, context) };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
throw new UnsupportedQueryPlanError("Operand must have name, value, or be an expression");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* `string(column)` over a string column and `double(column)` over a number column convert nothing:
|
|
329
|
+
* the column itself, except that the cast of a null VALUE is an error. The reference is resolved
|
|
330
|
+
* as one whose NULL is missing, so no comparison against it can select a NULL row.
|
|
331
|
+
*/
|
|
332
|
+
function resolveIdentityCast(
|
|
333
|
+
expression: OperatorOperand,
|
|
334
|
+
context: TranslationContext
|
|
335
|
+
): ResolvedOperand | undefined {
|
|
336
|
+
const target = lookup(IDENTITY_CASTS, expression.operator);
|
|
337
|
+
const [operand] = expression.operands;
|
|
338
|
+
|
|
339
|
+
if (
|
|
340
|
+
target === undefined ||
|
|
341
|
+
expression.operands.length !== 1 ||
|
|
342
|
+
operand === undefined ||
|
|
343
|
+
!isNamedOperand(operand)
|
|
344
|
+
) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const fieldRef = resolveFieldReference(operand.name, context);
|
|
349
|
+
|
|
350
|
+
if (fieldRef.valueType !== target) return undefined;
|
|
351
|
+
|
|
352
|
+
return { ...fieldRef, nullAttributeRepresentation: "omitted" };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const IDENTITY_CASTS = { string: "string", double: "number" } satisfies Record<string, string>;
|
|
356
|
+
|
|
357
|
+
function resolveTimestampOperand(
|
|
358
|
+
expression: OperatorOperand,
|
|
359
|
+
context: TranslationContext,
|
|
360
|
+
allowSubMillisecond: boolean
|
|
361
|
+
): ResolvedOperand {
|
|
362
|
+
if (expression.operands.length !== 1) {
|
|
363
|
+
throw new UnsupportedQueryPlanError("timestamp() requires exactly one operand");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const operand = assertDefined(
|
|
367
|
+
expression.operands[0],
|
|
368
|
+
"timestamp() requires an operand"
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
if (isNamedOperand(operand)) {
|
|
372
|
+
const fieldRef = resolveFieldReference(operand.name, context);
|
|
373
|
+
|
|
374
|
+
if (fieldRef.valueType !== "dateTime") {
|
|
375
|
+
throw new UnsupportedQueryPlanError(
|
|
376
|
+
`timestamp() field ${operand.name} must be mapped with valueType: \"dateTime\"`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return { ...fieldRef, timestampWrapped: true };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!isValueOperand(operand) || !isString(operand.value)) {
|
|
384
|
+
throw new UnsupportedQueryPlanError("timestamp() requires a field reference or RFC 3339 string");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (!allowSubMillisecond) {
|
|
388
|
+
return { value: normalizeRfc3339Milliseconds(operand.value) };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const { iso, subMillisecond } = parseRfc3339Instant(operand.value);
|
|
392
|
+
|
|
393
|
+
return subMillisecond ? { value: iso, subMillisecond } : { value: iso };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** The value of arithmetic over two constant operands, or null when it cannot be folded. */
|
|
397
|
+
export function tryFoldValueExpression(
|
|
398
|
+
expr: OperatorOperand,
|
|
399
|
+
context: TranslationContext
|
|
400
|
+
): Value | null {
|
|
401
|
+
if (!ARITHMETIC_OPERATORS.has(expr.operator)) return null;
|
|
402
|
+
const leftOp = expr.operands[0];
|
|
403
|
+
const rightOp = expr.operands[1];
|
|
404
|
+
|
|
405
|
+
if (!leftOp || !rightOp) return null;
|
|
406
|
+
|
|
407
|
+
const left = resolveOperand(leftOp, context);
|
|
408
|
+
|
|
409
|
+
if (!isResolvedValue(left)) return null;
|
|
410
|
+
const right = resolveOperand(rightOp, context);
|
|
411
|
+
|
|
412
|
+
if (!isResolvedValue(right)) return null;
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
return foldArithmetic(expr.operator, left.value, right.value);
|
|
416
|
+
} catch {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
}
|