@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.
Files changed (174) hide show
  1. package/README.md +598 -603
  2. package/lib/collections.d.ts +30 -0
  3. package/lib/collections.d.ts.map +1 -0
  4. package/lib/collections.js +421 -0
  5. package/lib/collections.js.map +1 -0
  6. package/lib/comparison.d.ts +24 -0
  7. package/lib/comparison.d.ts.map +1 -0
  8. package/lib/comparison.js +731 -0
  9. package/lib/comparison.js.map +1 -0
  10. package/lib/errors.d.ts +13 -0
  11. package/lib/errors.d.ts.map +1 -0
  12. package/lib/errors.js +17 -0
  13. package/lib/errors.js.map +1 -0
  14. package/lib/evaluate.d.ts +5 -0
  15. package/lib/evaluate.d.ts.map +1 -0
  16. package/lib/evaluate.js +118 -0
  17. package/lib/evaluate.js.map +1 -0
  18. package/lib/fields.d.ts +34 -0
  19. package/lib/fields.d.ts.map +1 -0
  20. package/lib/fields.js +253 -0
  21. package/lib/fields.js.map +1 -0
  22. package/lib/functions.d.ts +5 -0
  23. package/lib/functions.d.ts.map +1 -0
  24. package/lib/functions.js +408 -0
  25. package/lib/functions.js.map +1 -0
  26. package/lib/hierarchy.d.ts +6 -0
  27. package/lib/hierarchy.d.ts.map +1 -0
  28. package/lib/hierarchy.js +266 -0
  29. package/lib/hierarchy.js.map +1 -0
  30. package/lib/index.d.ts +23 -13
  31. package/lib/index.d.ts.map +1 -1
  32. package/lib/index.js +34 -2555
  33. package/lib/index.js.map +1 -1
  34. package/lib/literals.d.ts +20 -0
  35. package/lib/literals.d.ts.map +1 -0
  36. package/lib/literals.js +290 -0
  37. package/lib/literals.js.map +1 -0
  38. package/lib/mapping.d.ts +95 -0
  39. package/lib/mapping.d.ts.map +1 -0
  40. package/lib/mapping.js +316 -0
  41. package/lib/mapping.js.map +1 -0
  42. package/lib/narrowing.d.ts +12 -0
  43. package/lib/narrowing.d.ts.map +1 -0
  44. package/lib/narrowing.js +32 -0
  45. package/lib/narrowing.js.map +1 -0
  46. package/lib/plan.d.ts +49 -0
  47. package/lib/plan.d.ts.map +1 -0
  48. package/lib/plan.js +86 -0
  49. package/lib/plan.js.map +1 -0
  50. package/lib/presence.d.ts +25 -0
  51. package/lib/presence.d.ts.map +1 -0
  52. package/lib/presence.js +82 -0
  53. package/lib/presence.js.map +1 -0
  54. package/lib/regex.d.ts +33 -0
  55. package/lib/regex.d.ts.map +1 -0
  56. package/lib/regex.js +480 -0
  57. package/lib/regex.js.map +1 -0
  58. package/lib/relations.d.ts +58 -0
  59. package/lib/relations.d.ts.map +1 -0
  60. package/lib/relations.js +175 -0
  61. package/lib/relations.js.map +1 -0
  62. package/lib/rewrite.d.ts +36 -0
  63. package/lib/rewrite.d.ts.map +1 -0
  64. package/lib/rewrite.js +366 -0
  65. package/lib/rewrite.js.map +1 -0
  66. package/lib/solve.d.ts +20 -0
  67. package/lib/solve.d.ts.map +1 -0
  68. package/lib/solve.js +77 -0
  69. package/lib/solve.js.map +1 -0
  70. package/lib/strings.d.ts +12 -0
  71. package/lib/strings.d.ts.map +1 -0
  72. package/lib/strings.js +110 -0
  73. package/lib/strings.js.map +1 -0
  74. package/lib/ternary.d.ts +12 -0
  75. package/lib/ternary.d.ts.map +1 -0
  76. package/lib/ternary.js +192 -0
  77. package/lib/ternary.js.map +1 -0
  78. package/lib/timestamp.d.ts +45 -0
  79. package/lib/timestamp.d.ts.map +1 -0
  80. package/lib/timestamp.js +225 -0
  81. package/lib/timestamp.js.map +1 -0
  82. package/lib/translate.d.ts +20 -0
  83. package/lib/translate.d.ts.map +1 -0
  84. package/lib/translate.js +287 -0
  85. package/lib/translate.js.map +1 -0
  86. package/lib/types.d.ts +29 -0
  87. package/lib/types.d.ts.map +1 -0
  88. package/lib/types.js +980 -0
  89. package/lib/types.js.map +1 -0
  90. package/lib/value.d.ts +8 -0
  91. package/lib/value.d.ts.map +1 -0
  92. package/lib/value.js +7 -0
  93. package/lib/value.js.map +1 -0
  94. package/package.json +27 -17
  95. package/src/adversarial.test.ts +376 -1071
  96. package/src/collections.ts +738 -0
  97. package/src/comparison.ts +1157 -0
  98. package/src/corpus.ts +433 -250
  99. package/src/errors.ts +12 -0
  100. package/src/evaluate.ts +170 -0
  101. package/src/fields.ts +373 -0
  102. package/src/functions.ts +548 -0
  103. package/src/generated/prisma-adversarial/browser.ts +10 -0
  104. package/src/generated/prisma-adversarial/client.ts +10 -0
  105. package/src/generated/prisma-adversarial/commonInputTypes.ts +116 -127
  106. package/src/generated/prisma-adversarial/internal/class.ts +24 -4
  107. package/src/generated/prisma-adversarial/internal/prismaNamespace.ts +174 -3
  108. package/src/generated/prisma-adversarial/internal/prismaNamespaceBrowser.ts +23 -2
  109. package/src/generated/prisma-adversarial/models/AdversarialBoolListElement.ts +1272 -0
  110. package/src/generated/prisma-adversarial/models/AdversarialInner.ts +54 -54
  111. package/src/generated/prisma-adversarial/models/AdversarialNumberListElement.ts +1316 -0
  112. package/src/generated/prisma-adversarial/models/AdversarialParent.ts +66 -66
  113. package/src/generated/prisma-adversarial/models/AdversarialResource.ts +435 -87
  114. package/src/generated/prisma-adversarial/models.ts +2 -0
  115. package/src/generated/prisma-adversarial-mysql/browser.ts +64 -0
  116. package/src/generated/prisma-adversarial-mysql/client.ts +86 -0
  117. package/src/generated/prisma-adversarial-mysql/commonInputTypes.ts +349 -0
  118. package/src/generated/prisma-adversarial-mysql/enums.ts +15 -0
  119. package/src/generated/prisma-adversarial-mysql/internal/class.ts +284 -0
  120. package/src/generated/prisma-adversarial-mysql/internal/prismaNamespace.ts +1518 -0
  121. package/src/generated/prisma-adversarial-mysql/internal/prismaNamespaceBrowser.ts +274 -0
  122. package/src/generated/prisma-adversarial-mysql/models/AdversarialBoolListElement.ts +1157 -0
  123. package/src/generated/prisma-adversarial-mysql/models/AdversarialCategory.ts +1286 -0
  124. package/src/generated/prisma-adversarial-mysql/models/AdversarialInner.ts +1247 -0
  125. package/src/generated/prisma-adversarial-mysql/models/AdversarialLabel.ts +1157 -0
  126. package/src/generated/prisma-adversarial-mysql/models/AdversarialNumberListElement.ts +1201 -0
  127. package/src/generated/prisma-adversarial-mysql/models/AdversarialParent.ts +1352 -0
  128. package/src/generated/prisma-adversarial-mysql/models/AdversarialResource.ts +2021 -0
  129. package/src/generated/prisma-adversarial-mysql/models/AdversarialSubCategory.ts +1286 -0
  130. package/src/generated/prisma-adversarial-mysql/models/AdversarialTag.ts +1190 -0
  131. package/src/generated/prisma-adversarial-mysql/models.ts +20 -0
  132. package/src/generated/prisma-adversarial-mysql-v6/client.d.ts +1 -0
  133. package/src/generated/prisma-adversarial-mysql-v6/default.d.ts +1 -0
  134. package/src/generated/prisma-adversarial-mysql-v6/edge.d.ts +1 -0
  135. package/src/generated/prisma-adversarial-mysql-v6/index.d.ts +13822 -0
  136. package/src/generated/prisma-adversarial-mysql-v6/runtime/index-browser.d.ts +370 -0
  137. package/src/generated/prisma-adversarial-mysql-v6/runtime/library.d.ts +3982 -0
  138. package/src/generated/prisma-adversarial-mysql-v6/wasm.d.ts +1 -0
  139. package/src/generated/prisma-adversarial-pg/browser.ts +10 -0
  140. package/src/generated/prisma-adversarial-pg/client.ts +10 -0
  141. package/src/generated/prisma-adversarial-pg/commonInputTypes.ts +118 -129
  142. package/src/generated/prisma-adversarial-pg/internal/class.ts +24 -4
  143. package/src/generated/prisma-adversarial-pg/internal/prismaNamespace.ts +174 -3
  144. package/src/generated/prisma-adversarial-pg/internal/prismaNamespaceBrowser.ts +23 -2
  145. package/src/generated/prisma-adversarial-pg/models/AdversarialBoolListElement.ts +1275 -0
  146. package/src/generated/prisma-adversarial-pg/models/AdversarialInner.ts +54 -54
  147. package/src/generated/prisma-adversarial-pg/models/AdversarialNumberListElement.ts +1319 -0
  148. package/src/generated/prisma-adversarial-pg/models/AdversarialParent.ts +66 -66
  149. package/src/generated/prisma-adversarial-pg/models/AdversarialResource.ts +435 -87
  150. package/src/generated/prisma-adversarial-pg/models.ts +2 -0
  151. package/src/generated/prisma-adversarial-pg-v6/index.d.ts +3476 -388
  152. package/src/generated/prisma-adversarial-v6/index.d.ts +3465 -383
  153. package/src/hierarchy.ts +405 -0
  154. package/src/index.ts +66 -3646
  155. package/src/literals.ts +363 -0
  156. package/src/mapping.ts +474 -0
  157. package/src/narrowing.ts +33 -0
  158. package/src/plan.ts +125 -0
  159. package/src/presence.ts +114 -0
  160. package/src/regex.ts +596 -0
  161. package/src/relations.ts +226 -0
  162. package/src/rewrite.ts +524 -0
  163. package/src/solve.ts +96 -0
  164. package/src/strings.ts +152 -0
  165. package/src/ternary.ts +359 -0
  166. package/src/test-setup.adversarial-url.ts +6 -3
  167. package/src/test-setup.adversarial.mysql.v6.ts +13 -0
  168. package/src/test-setup.adversarial.mysql.v7.ts +12 -0
  169. package/src/test-setup.adversarial.v7.ts +1 -0
  170. package/src/timestamp.ts +308 -0
  171. package/src/translate.ts +419 -0
  172. package/src/translator.test.ts +497 -1317
  173. package/src/types.ts +1310 -0
  174. 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
+ }