@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
package/src/rewrite.ts
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
// Plan-to-plan rewrites that run before translation. None of them knows about Prisma: each takes
|
|
2
|
+
// an expression tree and returns an equivalent one that the translator can lower more directly.
|
|
3
|
+
|
|
4
|
+
import type { PlanExpressionOperand, Value } from "@cerbos/core";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
evaluateConstantComparison,
|
|
8
|
+
foldArithmetic,
|
|
9
|
+
} from "./evaluate";
|
|
10
|
+
import {
|
|
11
|
+
COMPARISON_OPERATORS,
|
|
12
|
+
assertDefined,
|
|
13
|
+
assertLogicalOperands,
|
|
14
|
+
isNamedOperand,
|
|
15
|
+
isOperatorOperand,
|
|
16
|
+
isValueOperand,
|
|
17
|
+
} from "./plan";
|
|
18
|
+
import type { NamedOperand, OperatorOperand } from "./plan";
|
|
19
|
+
import { UnsupportedQueryPlanError } from "./errors";
|
|
20
|
+
import { isBoolean, isObject, isString } from "./narrowing";
|
|
21
|
+
|
|
22
|
+
/** The boolean collection macros: each takes a collection and a lambda. */
|
|
23
|
+
export const COLLECTION_OPERATORS = new Set([
|
|
24
|
+
"exists",
|
|
25
|
+
"exists_one",
|
|
26
|
+
"all",
|
|
27
|
+
"except",
|
|
28
|
+
"filter",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
// Operators whose second operand is a lambda that binds an iteration variable.
|
|
32
|
+
const LAMBDA_BINDING_OPERATORS = new Set([...COLLECTION_OPERATORS, "map"]);
|
|
33
|
+
|
|
34
|
+
export function containsCollectionOperator(expr: PlanExpressionOperand): boolean {
|
|
35
|
+
if (!isOperatorOperand(expr)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (COLLECTION_OPERATORS.has(expr.operator)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return expr.operands.some(containsCollectionOperator);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Substitute a lambda iteration variable with a concrete collection element
|
|
48
|
+
* inside a lambda body. A bare reference to the variable becomes the element
|
|
49
|
+
* itself; a `variable.path.to.field` reference drills into the element. A
|
|
50
|
+
* nested collection macro whose lambda rebinds the same variable name shadows
|
|
51
|
+
* the outer variable, so substitution only descends into its collection
|
|
52
|
+
* operand.
|
|
53
|
+
*/
|
|
54
|
+
export function substituteLambdaVariable(
|
|
55
|
+
operand: PlanExpressionOperand,
|
|
56
|
+
variableName: string,
|
|
57
|
+
element: Value
|
|
58
|
+
): PlanExpressionOperand {
|
|
59
|
+
if (isNamedOperand(operand)) {
|
|
60
|
+
if (operand.name === variableName) {
|
|
61
|
+
return { value: element };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (operand.name.startsWith(`${variableName}.`)) {
|
|
65
|
+
let current: Value = element;
|
|
66
|
+
|
|
67
|
+
for (const segment of operand.name
|
|
68
|
+
.slice(variableName.length + 1)
|
|
69
|
+
.split(".")) {
|
|
70
|
+
if (
|
|
71
|
+
!isObject(current) ||
|
|
72
|
+
Array.isArray(current) ||
|
|
73
|
+
!(segment in current) ||
|
|
74
|
+
current[segment] === undefined
|
|
75
|
+
) {
|
|
76
|
+
throw new UnsupportedQueryPlanError(
|
|
77
|
+
`Cannot resolve "${operand.name}": collection element has no field "${segment}"`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
current = current[segment];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { value: current };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return operand;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!isOperatorOperand(operand)) {
|
|
91
|
+
return operand;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const [nestedCollection, nestedLambda] = operand.operands;
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
LAMBDA_BINDING_OPERATORS.has(operand.operator) &&
|
|
98
|
+
operand.operands.length === 2 &&
|
|
99
|
+
nestedCollection !== undefined &&
|
|
100
|
+
nestedLambda !== undefined &&
|
|
101
|
+
isOperatorOperand(nestedLambda) &&
|
|
102
|
+
nestedLambda.operator === "lambda"
|
|
103
|
+
) {
|
|
104
|
+
const nestedVariable = nestedLambda.operands[1];
|
|
105
|
+
|
|
106
|
+
if (
|
|
107
|
+
nestedVariable !== undefined &&
|
|
108
|
+
isNamedOperand(nestedVariable) &&
|
|
109
|
+
nestedVariable.name === variableName
|
|
110
|
+
) {
|
|
111
|
+
// The nested lambda shadows our variable: substitute only in the collection operand.
|
|
112
|
+
return {
|
|
113
|
+
operator: operand.operator,
|
|
114
|
+
operands: [
|
|
115
|
+
substituteLambdaVariable(nestedCollection, variableName, element),
|
|
116
|
+
nestedLambda,
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
operator: operand.operator,
|
|
124
|
+
operands: operand.operands.map((o) =>
|
|
125
|
+
substituteLambdaVariable(o, variableName, element)
|
|
126
|
+
),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function mentionsVariable(
|
|
131
|
+
expr: PlanExpressionOperand,
|
|
132
|
+
variableName: string
|
|
133
|
+
): boolean {
|
|
134
|
+
if (isNamedOperand(expr)) {
|
|
135
|
+
return (
|
|
136
|
+
expr.name === variableName || expr.name.startsWith(variableName + ".")
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (isOperatorOperand(expr)) {
|
|
141
|
+
return expr.operands.some((operand) =>
|
|
142
|
+
mentionsVariable(operand, variableName)
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isOuterScopeName(name: string, enclosingVariables: string[]): boolean {
|
|
150
|
+
return !enclosingVariables.some(
|
|
151
|
+
(variable) => name === variable || name.startsWith(variable + ".")
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Replaces every occurrence of the bare named operand `name` with a boolean constant. */
|
|
156
|
+
function substituteNamedOperand(
|
|
157
|
+
expr: PlanExpressionOperand,
|
|
158
|
+
name: string,
|
|
159
|
+
value: boolean
|
|
160
|
+
): PlanExpressionOperand {
|
|
161
|
+
if (isNamedOperand(expr)) {
|
|
162
|
+
return expr.name === name ? { value } : expr;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (isOperatorOperand(expr)) {
|
|
166
|
+
return {
|
|
167
|
+
operator: expr.operator,
|
|
168
|
+
operands: expr.operands.map((operand) =>
|
|
169
|
+
substituteNamedOperand(operand, name, value)
|
|
170
|
+
),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return expr;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const BOOLEAN_POSITION_OPERATORS = new Set(["and", "or", "not", "if"]);
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Finds a bare boolean reference to an OUTER column (root or enclosing lambda scope) in a
|
|
181
|
+
* boolean position of a lambda body: a direct operand of and/or/not, a ternary condition, or
|
|
182
|
+
* the body itself.
|
|
183
|
+
*/
|
|
184
|
+
function findOuterBooleanReference(
|
|
185
|
+
expr: PlanExpressionOperand,
|
|
186
|
+
enclosingVariables: string[]
|
|
187
|
+
): string | undefined {
|
|
188
|
+
if (isNamedOperand(expr)) {
|
|
189
|
+
return isOuterScopeName(expr.name, enclosingVariables)
|
|
190
|
+
? expr.name
|
|
191
|
+
: undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!isOperatorOperand(expr)) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const booleanPositions = BOOLEAN_POSITION_OPERATORS.has(expr.operator)
|
|
199
|
+
? expr.operands
|
|
200
|
+
: // Comparisons wrapping a ternary keep the outer reference in the ternary condition.
|
|
201
|
+
expr.operands.filter(
|
|
202
|
+
(operand) =>
|
|
203
|
+
isOperatorOperand(operand) &&
|
|
204
|
+
BOOLEAN_POSITION_OPERATORS.has(operand.operator)
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
for (const operand of booleanPositions) {
|
|
208
|
+
const found = findOuterBooleanReference(operand, enclosingVariables);
|
|
209
|
+
|
|
210
|
+
if (found !== undefined) {
|
|
211
|
+
return found;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The parts of `macro(collection, lambda(body, variable))` when `expr` is a boolean collection
|
|
220
|
+
* macro over a single-variable lambda, or undefined for any other shape.
|
|
221
|
+
*/
|
|
222
|
+
function parseLambdaMacro(expr: OperatorOperand):
|
|
223
|
+
| {
|
|
224
|
+
collection: PlanExpressionOperand;
|
|
225
|
+
lambda: OperatorOperand;
|
|
226
|
+
variable: NamedOperand;
|
|
227
|
+
}
|
|
228
|
+
| undefined {
|
|
229
|
+
const [collection, lambda] = expr.operands;
|
|
230
|
+
|
|
231
|
+
if (
|
|
232
|
+
!COLLECTION_OPERATORS.has(expr.operator) ||
|
|
233
|
+
expr.operands.length !== 2 ||
|
|
234
|
+
collection === undefined ||
|
|
235
|
+
lambda === undefined ||
|
|
236
|
+
!isOperatorOperand(lambda) ||
|
|
237
|
+
lambda.operator !== "lambda" ||
|
|
238
|
+
lambda.operands.length !== 2
|
|
239
|
+
) {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const variable = lambda.operands[1];
|
|
244
|
+
|
|
245
|
+
if (variable === undefined || !isNamedOperand(variable)) {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { collection, lambda, variable };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Rewrites lambda bodies that reference columns of an ENCLOSING scope (the root row, or an
|
|
254
|
+
* outer lambda's element) so that every filter lands on the model it belongs to. Without this,
|
|
255
|
+
* an outer column referenced inside `tags.exists(t, ...)` would be emitted as a field of the
|
|
256
|
+
* tag model. Two sound transforms, applied bottom-up:
|
|
257
|
+
*
|
|
258
|
+
* - Conjunct hoisting (exists only): `exists(c, P(c) && Q)` with row-constant Q becomes
|
|
259
|
+
* `exists(c, P(c)) && Q`. Valid in three-valued logic (AND distributes over the per-element
|
|
260
|
+
* OR), including the empty-collection case (both sides are FALSE).
|
|
261
|
+
* - Case split (any collection op): a bare outer boolean reference Q in a boolean position
|
|
262
|
+
* becomes `(Q && op[Q:=true]) || (!Q && op[Q:=false]) || (Q && !Q && exists(c, true))`. The
|
|
263
|
+
* contradiction arm keeps the whole expression UNKNOWN (not FALSE) when Q is NULL-derived and
|
|
264
|
+
* the collection has elements, mirroring the guarded-ternary encoding.
|
|
265
|
+
*/
|
|
266
|
+
export function hoistOuterScopeReferences(
|
|
267
|
+
expr: PlanExpressionOperand,
|
|
268
|
+
enclosingVariables: string[]
|
|
269
|
+
): PlanExpressionOperand {
|
|
270
|
+
if (!isOperatorOperand(expr)) {
|
|
271
|
+
return expr;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const macro = parseLambdaMacro(expr);
|
|
275
|
+
|
|
276
|
+
if (macro === undefined) {
|
|
277
|
+
return {
|
|
278
|
+
operator: expr.operator,
|
|
279
|
+
operands: expr.operands.map((operand) =>
|
|
280
|
+
hoistOuterScopeReferences(operand, enclosingVariables)
|
|
281
|
+
),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const { collection, lambda, variable } = macro;
|
|
286
|
+
|
|
287
|
+
const scopeVariables = [...enclosingVariables, variable.name];
|
|
288
|
+
|
|
289
|
+
const body = hoistOuterScopeReferences(
|
|
290
|
+
assertDefined(lambda.operands[0], "Lambda requires a condition"),
|
|
291
|
+
scopeVariables
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const rebuild = (newBody: PlanExpressionOperand): OperatorOperand => ({
|
|
295
|
+
operator: expr.operator,
|
|
296
|
+
operands: [collection, { operator: "lambda", operands: [newBody, variable] }],
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Conjunct hoisting out of exists().
|
|
300
|
+
if (
|
|
301
|
+
expr.operator === "exists" &&
|
|
302
|
+
isOperatorOperand(body) &&
|
|
303
|
+
body.operator === "and"
|
|
304
|
+
) {
|
|
305
|
+
const elementConjuncts = body.operands.filter((operand) =>
|
|
306
|
+
mentionsVariable(operand, variable.name)
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const outerConjuncts = body.operands.filter(
|
|
310
|
+
(operand) => !mentionsVariable(operand, variable.name)
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
if (outerConjuncts.length > 0 && elementConjuncts.length > 0) {
|
|
314
|
+
const innerBody =
|
|
315
|
+
elementConjuncts.length === 1
|
|
316
|
+
? elementConjuncts[0]!
|
|
317
|
+
: { operator: "and", operands: elementConjuncts };
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
operator: "and",
|
|
321
|
+
operands: [rebuild(innerBody), ...outerConjuncts],
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Case split on a bare outer boolean reference.
|
|
327
|
+
const outerRef = findOuterBooleanReference(body, scopeVariables);
|
|
328
|
+
|
|
329
|
+
if (outerRef !== undefined) {
|
|
330
|
+
const q: PlanExpressionOperand = { name: outerRef };
|
|
331
|
+
const notQ: PlanExpressionOperand = { operator: "not", operands: [q] };
|
|
332
|
+
|
|
333
|
+
const arms: PlanExpressionOperand[] = [
|
|
334
|
+
{
|
|
335
|
+
operator: "and",
|
|
336
|
+
operands: [q, rebuild(substituteNamedOperand(body, outerRef, true))],
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
operator: "and",
|
|
340
|
+
operands: [notQ, rebuild(substituteNamedOperand(body, outerRef, false))],
|
|
341
|
+
},
|
|
342
|
+
// Contradiction arm: UNKNOWN when the reference is NULL-derived, never TRUE. CEL never
|
|
343
|
+
// evaluates the body over an empty collection, so there the arm is FALSE, not UNKNOWN.
|
|
344
|
+
{
|
|
345
|
+
operator: "and",
|
|
346
|
+
operands: [
|
|
347
|
+
q,
|
|
348
|
+
notQ,
|
|
349
|
+
{
|
|
350
|
+
operator: "exists",
|
|
351
|
+
operands: [collection, { operator: "lambda", operands: [{ value: true }, variable] }],
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
},
|
|
355
|
+
];
|
|
356
|
+
|
|
357
|
+
// all() over an empty collection never evaluates its body, so CEL holds it TRUE even when
|
|
358
|
+
// the reference is missing; the arms above are all UNKNOWN on that row. `all(c, false)` is
|
|
359
|
+
// TRUE exactly when the collection is empty, and definite, so it restores that row without
|
|
360
|
+
// touching any other (#488).
|
|
361
|
+
if (expr.operator === "all") {
|
|
362
|
+
arms.push(rebuild({ value: false }));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return { operator: "or", operands: arms };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return rebuild(body);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* `and`/`or` over already-folded operands: a dominating constant decides the whole expression,
|
|
373
|
+
* and the other constants drop out.
|
|
374
|
+
*/
|
|
375
|
+
function foldLogical(
|
|
376
|
+
operator: "and" | "or",
|
|
377
|
+
operands: PlanExpressionOperand[]
|
|
378
|
+
): PlanExpressionOperand {
|
|
379
|
+
const dominant = operator === "or";
|
|
380
|
+
|
|
381
|
+
if (operands.some((o) => isValueOperand(o) && o.value === dominant)) {
|
|
382
|
+
return { value: dominant };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const remaining = operands.filter((o) => !isValueOperand(o));
|
|
386
|
+
|
|
387
|
+
if (remaining.length === 0) return { value: !dominant };
|
|
388
|
+
|
|
389
|
+
if (remaining.length === 1) return remaining[0]!;
|
|
390
|
+
|
|
391
|
+
return { operator, operands: remaining };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Bottom-up constant folding over the plan AST. Only shapes the case-split transform can
|
|
396
|
+
* produce need folding (constant ternary conditions, comparisons between two constants,
|
|
397
|
+
* logical operators with constant operands, collection macros with constant bodies); real
|
|
398
|
+
* planner output arrives pre-folded.
|
|
399
|
+
*/
|
|
400
|
+
export function constantFoldExpression(
|
|
401
|
+
expr: PlanExpressionOperand
|
|
402
|
+
): PlanExpressionOperand {
|
|
403
|
+
if (!isOperatorOperand(expr)) {
|
|
404
|
+
return expr;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
assertLogicalOperands(expr.operator, expr.operands);
|
|
408
|
+
const operands = expr.operands.map(constantFoldExpression);
|
|
409
|
+
const folded: OperatorOperand = { operator: expr.operator, operands };
|
|
410
|
+
const [first, second] = operands;
|
|
411
|
+
|
|
412
|
+
const bothConstant =
|
|
413
|
+
first !== undefined &&
|
|
414
|
+
second !== undefined &&
|
|
415
|
+
isValueOperand(first) &&
|
|
416
|
+
isValueOperand(second);
|
|
417
|
+
|
|
418
|
+
switch (expr.operator) {
|
|
419
|
+
case "if": {
|
|
420
|
+
const [condition, thenBranch, elseBranch] = operands;
|
|
421
|
+
|
|
422
|
+
if (condition !== undefined && isValueOperand(condition)) {
|
|
423
|
+
if (!isBoolean(condition.value)) {
|
|
424
|
+
throw new UnsupportedQueryPlanError("if (ternary) condition must be a boolean expression");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return assertDefined(
|
|
428
|
+
condition.value ? thenBranch : elseBranch,
|
|
429
|
+
"if (ternary) requires branch operands"
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return folded;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
case "and":
|
|
437
|
+
case "or":
|
|
438
|
+
return foldLogical(expr.operator, operands);
|
|
439
|
+
case "not":
|
|
440
|
+
if (first !== undefined && isValueOperand(first)) {
|
|
441
|
+
return { value: first.value !== true };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return folded;
|
|
445
|
+
case "add":
|
|
446
|
+
case "sub":
|
|
447
|
+
case "mult":
|
|
448
|
+
case "div":
|
|
449
|
+
return bothConstant
|
|
450
|
+
? { value: foldArithmetic(expr.operator, first.value, second.value) }
|
|
451
|
+
: folded;
|
|
452
|
+
case "contains":
|
|
453
|
+
case "startsWith":
|
|
454
|
+
case "endsWith": {
|
|
455
|
+
if (
|
|
456
|
+
bothConstant &&
|
|
457
|
+
isString(first.value) &&
|
|
458
|
+
isString(second.value)
|
|
459
|
+
) {
|
|
460
|
+
const receiver = first.value;
|
|
461
|
+
const needle = second.value;
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
value:
|
|
465
|
+
expr.operator === "contains"
|
|
466
|
+
? receiver.includes(needle)
|
|
467
|
+
: expr.operator === "startsWith"
|
|
468
|
+
? receiver.startsWith(needle)
|
|
469
|
+
: receiver.endsWith(needle),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return folded;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
case "exists":
|
|
477
|
+
case "all": {
|
|
478
|
+
const lambda = second;
|
|
479
|
+
|
|
480
|
+
if (
|
|
481
|
+
first !== undefined &&
|
|
482
|
+
lambda !== undefined &&
|
|
483
|
+
isOperatorOperand(lambda) &&
|
|
484
|
+
lambda.operator === "lambda" &&
|
|
485
|
+
lambda.operands[0] !== undefined &&
|
|
486
|
+
isValueOperand(lambda.operands[0])
|
|
487
|
+
) {
|
|
488
|
+
const bodyValue = lambda.operands[0].value === true;
|
|
489
|
+
|
|
490
|
+
if (expr.operator === "exists" && !bodyValue) return { value: false };
|
|
491
|
+
|
|
492
|
+
if (expr.operator === "all" && bodyValue) return { value: true };
|
|
493
|
+
|
|
494
|
+
const sizeExpr: PlanExpressionOperand = {
|
|
495
|
+
operator: "size",
|
|
496
|
+
operands: [first],
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
// exists(c, true) is "collection is non-empty"; all(c, false) is "collection is empty".
|
|
500
|
+
return expr.operator === "exists"
|
|
501
|
+
? { operator: "gt", operands: [sizeExpr, { value: 0 }] }
|
|
502
|
+
: { operator: "eq", operands: [sizeExpr, { value: 0 }] };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return folded;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
default:
|
|
509
|
+
if (
|
|
510
|
+
COMPARISON_OPERATORS.has(expr.operator) &&
|
|
511
|
+
bothConstant
|
|
512
|
+
) {
|
|
513
|
+
return {
|
|
514
|
+
value: evaluateConstantComparison(
|
|
515
|
+
expr.operator,
|
|
516
|
+
first.value,
|
|
517
|
+
second.value
|
|
518
|
+
),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return folded;
|
|
523
|
+
}
|
|
524
|
+
}
|
package/src/solve.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Solving `f(column) CMP v` for a monotone IEEE-754 function f exactly, over the doubles.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The doubles `x` for which a comparison holds, as an interval of finite doubles: `lo`/`hi` are
|
|
5
|
+
* inclusive bounds, undefined when that side is unbounded. `empty` when no double satisfies it.
|
|
6
|
+
*/
|
|
7
|
+
export type DoubleInterval =
|
|
8
|
+
| { empty: true }
|
|
9
|
+
| { empty: false; lo: number | undefined; hi: number | undefined };
|
|
10
|
+
|
|
11
|
+
const view = new DataView(new ArrayBuffer(8));
|
|
12
|
+
const SIGN = 1n << 63n;
|
|
13
|
+
|
|
14
|
+
/** An integer key that orders finite doubles as the doubles themselves (+0 and -0 share one). */
|
|
15
|
+
function toKey(x: number): bigint {
|
|
16
|
+
view.setFloat64(0, x);
|
|
17
|
+
const bits = view.getBigUint64(0);
|
|
18
|
+
|
|
19
|
+
return bits & SIGN ? -(bits & ~SIGN) : bits;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function fromKey(key: bigint): number {
|
|
23
|
+
view.setBigUint64(0, key < 0n ? -key | SIGN : key);
|
|
24
|
+
|
|
25
|
+
return view.getFloat64(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const MIN_KEY = toKey(-Number.MAX_VALUE);
|
|
29
|
+
const MAX_KEY = toKey(Number.MAX_VALUE);
|
|
30
|
+
|
|
31
|
+
/** The least key in [MIN_KEY, MAX_KEY] whose double satisfies `holds`, or undefined. */
|
|
32
|
+
function leastKey(holds: (x: number) => boolean): bigint | undefined {
|
|
33
|
+
if (!holds(fromKey(MAX_KEY))) return undefined;
|
|
34
|
+
let low = MIN_KEY;
|
|
35
|
+
let high = MAX_KEY;
|
|
36
|
+
|
|
37
|
+
while (low < high) {
|
|
38
|
+
const mid = low + (high - low) / 2n;
|
|
39
|
+
|
|
40
|
+
if (holds(fromKey(mid))) high = mid;
|
|
41
|
+
else low = mid + 1n;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return low;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The doubles `x` with `f(x) CMP v`, for `f` non-decreasing over the finite doubles — IEEE-754
|
|
49
|
+
* addition and multiplication by a positive constant are, whatever rounding does to them, which
|
|
50
|
+
* is what makes this exact where subtracting the constant from `v` is not.
|
|
51
|
+
*/
|
|
52
|
+
export function solveMonotone(
|
|
53
|
+
f: (x: number) => number,
|
|
54
|
+
operator: "eq" | "lt" | "le" | "gt" | "ge",
|
|
55
|
+
v: number
|
|
56
|
+
): DoubleInterval {
|
|
57
|
+
// {x : f(x) >= v} and {x : f(x) > v} are both upward-closed.
|
|
58
|
+
const atLeast = leastKey((x) => f(x) >= v);
|
|
59
|
+
const above = leastKey((x) => f(x) > v);
|
|
60
|
+
|
|
61
|
+
const interval = (lo: bigint | undefined, hi: bigint | undefined): DoubleInterval => {
|
|
62
|
+
if (lo !== undefined && hi !== undefined && lo > hi) return { empty: true };
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
empty: false,
|
|
66
|
+
lo: lo === undefined || lo === MIN_KEY ? undefined : fromKey(lo),
|
|
67
|
+
hi: hi === undefined || hi === MAX_KEY ? undefined : fromKey(hi),
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const below = (key: bigint | undefined) => (key === undefined ? MAX_KEY : key - 1n);
|
|
72
|
+
|
|
73
|
+
switch (operator) {
|
|
74
|
+
case "ge":
|
|
75
|
+
return atLeast === undefined ? { empty: true } : interval(atLeast, undefined);
|
|
76
|
+
case "gt":
|
|
77
|
+
return above === undefined ? { empty: true } : interval(above, undefined);
|
|
78
|
+
case "le":
|
|
79
|
+
return above === MIN_KEY ? { empty: true } : interval(undefined, below(above));
|
|
80
|
+
case "lt":
|
|
81
|
+
return atLeast === MIN_KEY ? { empty: true } : interval(undefined, below(atLeast));
|
|
82
|
+
case "eq":
|
|
83
|
+
return atLeast === undefined ? { empty: true } : interval(atLeast, below(above));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The interval of `-x` for `x` in `interval`. */
|
|
88
|
+
export function negateInterval(interval: DoubleInterval): DoubleInterval {
|
|
89
|
+
if (interval.empty) return interval;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
empty: false,
|
|
93
|
+
lo: interval.hi === undefined ? undefined : -interval.hi,
|
|
94
|
+
hi: interval.lo === undefined ? undefined : -interval.lo,
|
|
95
|
+
};
|
|
96
|
+
}
|