@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/types.ts ADDED
@@ -0,0 +1,1310 @@
1
+ // A plan-to-plan rewrite that settles comparisons whose outcome the mapped column types already
2
+ // decide: CEL's heterogeneous equality, and the no-overload errors a mistyped operand raises.
3
+ // Runs before constant folding, so every constant it produces folds away like any other.
4
+
5
+ import type { PlanExpressionOperand, Value } from "@cerbos/core";
6
+
7
+ import { ARITHMETIC_OPERATORS } from "./evaluate";
8
+ import { literalValue } from "./literals";
9
+ import { enterLambdaScope, lookupMapping, resolveFieldReference } from "./mapping";
10
+ import type { ResolvedFieldReference, TranslationContext } from "./mapping";
11
+ import type { MapperConfig } from "./index";
12
+ import { isInvalidPattern } from "./regex";
13
+ import { constantFoldExpression } from "./rewrite";
14
+ import { rewriteTemporalComparison } from "./timestamp";
15
+ import { COMPARISON_OPERATORS, isNamedOperand, isOperatorOperand, isValueOperand } from "./plan";
16
+ import type { NamedOperand, OperatorOperand } from "./plan";
17
+ import { isBoolean, isNumber, isString, pair, triple, typeofValue } from "./narrowing";
18
+
19
+ /**
20
+ * The CEL type an operand is known to have. Numbers are one family: CEL's heterogeneous equality
21
+ * compares an int, a uint and a double numerically, so `1 == 1.0` is true across them.
22
+ */
23
+ type CelType = "string" | "number" | "boolean" | "null" | "list" | "map";
24
+
25
+ /**
26
+ * The values one leaf can take across the rows. `error` is a CEL evaluation error, which Cerbos
27
+ * denies: a no-overload call, or a missing attribute (an omitted NULL column, an absent to-one
28
+ * parent) reached before the comparison could decide.
29
+ */
30
+ type Outcomes = {
31
+ true?: boolean;
32
+ false?: boolean;
33
+ error?: boolean;
34
+ /**
35
+ * When the only error is a missing column: each such column's presence test, TRUE when the
36
+ * value is there and UNKNOWN when it is NULL (see presence).
37
+ */
38
+ missing?: PlanExpressionOperand[];
39
+ /**
40
+ * When the only error is a missing attribute reached through a to-one relation: a test TRUE
41
+ * exactly when the relation exists and the column is not NULL, and FALSE or UNKNOWN otherwise.
42
+ * Unlike `missing`, a relation filter cannot promise UNKNOWN for the error rows (Prisma matches a
43
+ * to-one relation through a join or subquery, which can collapse UNKNOWN to FALSE), so the test
44
+ * is only used where the leaf's polarity is fixed and FALSE-or-UNKNOWN is the value that
45
+ * polarity needs (see settleTypeMismatches).
46
+ */
47
+ reached?: PlanExpressionOperand;
48
+ };
49
+
50
+ const ORDERING_OPERATORS = new Set(["lt", "le", "gt", "ge"]);
51
+ const STRING_OPERATORS = new Set(["contains", "startsWith", "endsWith", "matches"]);
52
+ const HIERARCHY_OPERATORS = new Set(["overlaps", "ancestorOf", "descendentOf"]);
53
+ const LIST_OPERATORS = new Set(["filter", "map", "except"]);
54
+
55
+ /**
56
+ * Replaces every leaf whose outcome the mapped value types settle with the constant that decides
57
+ * the same rows, then leaves the rest of the plan to the translator.
58
+ *
59
+ * CEL's `&&`, `||` and `!` are Kleene's strong three-valued logic with an error in the role of
60
+ * UNKNOWN, and `exists`/`all` are its disjunction and conjunction over the elements. Kleene logic
61
+ * is regular — a formula that is TRUE with an UNKNOWN operand is TRUE whatever that operand is —
62
+ * so under an even number of negations a leaf that is only ever FALSE or an error decides every
63
+ * row exactly as a constant FALSE does, and under an odd number a leaf that is only ever TRUE or
64
+ * an error decides every row exactly as a constant TRUE does. A leaf that can be both TRUE and
65
+ * FALSE, or that sits where its polarity is not fixed (a ternary condition, an operand of `==`,
66
+ * the body of `exists_one`), is left for the translator to lower or refuse.
67
+ */
68
+ export function settleTypeMismatches(
69
+ expr: PlanExpressionOperand,
70
+ context: TranslationContext,
71
+ positive = true
72
+ ): PlanExpressionOperand {
73
+ if (!isOperatorOperand(expr)) return expr;
74
+
75
+ switch (expr.operator) {
76
+ case "and":
77
+ case "or":
78
+ return {
79
+ operator: expr.operator,
80
+ operands: expr.operands.map((operand) =>
81
+ settleTypeMismatches(operand, context, positive)
82
+ ),
83
+ };
84
+ case "not":
85
+ return {
86
+ operator: "not",
87
+ operands: expr.operands.map((operand) =>
88
+ settleTypeMismatches(operand, context, !positive)
89
+ ),
90
+ };
91
+ case "exists":
92
+ case "all":
93
+ return settleLambdaBody(expr, context, positive);
94
+ }
95
+
96
+ const cast =
97
+ unwrapBooleanComparison(expr) ??
98
+ distributeTernary(expr, context) ??
99
+ rewriteCastComparison(expr, context, positive) ??
100
+ rewriteConcatenation(expr, context) ??
101
+ rewriteTemporalComparison(expr, positive) ??
102
+ splitDivision(expr, context);
103
+
104
+ if (cast !== undefined) return settleTypeMismatches(cast, context, positive);
105
+
106
+ const outcomes = leafOutcomes(expr, context);
107
+
108
+ if (outcomes === undefined) return rewriteLiteralLists(expr, context);
109
+
110
+ // Definite once every column is present, and an error otherwise: the presence tests carry the
111
+ // error as SQL UNKNOWN, which decides the row as CEL's error does under either polarity.
112
+ if (outcomes.missing !== undefined && outcomes.true !== outcomes.false) {
113
+ const present: PlanExpressionOperand =
114
+ outcomes.missing.length === 1
115
+ ? outcomes.missing[0]!
116
+ : { operator: "and", operands: outcomes.missing };
117
+
118
+ if (outcomes.missing.length === 0) return { value: outcomes.true === true };
119
+
120
+ return outcomes.true ? present : { operator: "not", operands: [present] };
121
+ }
122
+
123
+ if (positive && !outcomes.true) return { value: false };
124
+
125
+ if (!positive && !outcomes.false) return { value: true };
126
+
127
+ // What is left is a leaf that is TRUE (positive) or FALSE (negative) once the relation and its
128
+ // column are present, and an error otherwise. The error rows must not satisfy the leaf's
129
+ // polarity: `reached` is FALSE or UNKNOWN on them, so it stands for the TRUE leaf under an even
130
+ // number of negations, and its negation for the FALSE leaf under an odd number, where that
131
+ // negation meets the enclosing one and is TRUE only for a present value (Kleene logic is
132
+ // regular, so an error leaf decides no more rows than the value its polarity can least use).
133
+ if (outcomes.reached !== undefined) {
134
+ return positive ? outcomes.reached : { operator: "not", operands: [outcomes.reached] };
135
+ }
136
+
137
+ if (!outcomes.error && !(outcomes.true && outcomes.false)) {
138
+ return { value: outcomes.true === true };
139
+ }
140
+
141
+ return expr;
142
+ }
143
+
144
+ /** `exists(c, P)` and `all(c, P)` are monotone in P, so the body keeps the macro's polarity. */
145
+ function settleLambdaBody(
146
+ expr: OperatorOperand,
147
+ context: TranslationContext,
148
+ positive: boolean
149
+ ): PlanExpressionOperand {
150
+ const [collection, lambda] = expr.operands;
151
+
152
+ if (
153
+ collection === undefined ||
154
+ !isNamedOperand(collection) ||
155
+ lambda === undefined ||
156
+ !isOperatorOperand(lambda) ||
157
+ lambda.operator !== "lambda" ||
158
+ // A two-variable comprehension binds a second variable; rebuilding the lambda below would
159
+ // drop it. It is left whole for the translator to refuse.
160
+ lambda.operands.length !== 2
161
+ ) {
162
+ return expr;
163
+ }
164
+
165
+ const [body, variable] = lambda.operands;
166
+
167
+ if (body === undefined || variable === undefined || !isNamedOperand(variable)) {
168
+ return expr;
169
+ }
170
+
171
+ const scoped = scopedContext(context, collection.name, variable.name);
172
+
173
+ if (scoped === undefined) return expr;
174
+
175
+ return {
176
+ operator: expr.operator,
177
+ operands: [
178
+ collection,
179
+ { operator: "lambda", operands: [settleTypeMismatches(body, scoped, positive), variable] },
180
+ ],
181
+ };
182
+ }
183
+
184
+ /** The context a lambda body over `collectionName` resolves its element references in. */
185
+ function scopedContext(
186
+ context: TranslationContext,
187
+ collectionName: string,
188
+ variableName: string
189
+ ): TranslationContext | undefined {
190
+ const { relations } = resolveFieldReference(collectionName, context);
191
+
192
+ if (!relations || relations.length === 0) return undefined;
193
+
194
+ return enterLambdaScope(context, collectionName, {
195
+ variableName,
196
+ relationModel: relations[relations.length - 1]!.model,
197
+ nullableFields: new Set(),
198
+ unknownFilters: [],
199
+ });
200
+ }
201
+
202
+ // -- casts ---------------------------------------------------------------------------------------
203
+
204
+ /**
205
+ * `cmp(c ? a : b, x)` as `(c && cmp(a, x)) || (!c && cmp(b, x))`, when that lets a branch settle.
206
+ * A ternary evaluates only the branch it selects, so the comparison distributes over it; the
207
+ * expansion is exact only when `c` itself cannot be an error, so `c` must be a boolean column that
208
+ * is never missing. Any other ternary is left to the translator's own lowering.
209
+ */
210
+ function distributeTernary(
211
+ expr: OperatorOperand,
212
+ context: TranslationContext
213
+ ): PlanExpressionOperand | undefined {
214
+ if (!COMPARISON_OPERATORS.has(expr.operator) || expr.operands.length !== 2) return undefined;
215
+
216
+ const index = expr.operands.findIndex(
217
+ (operand) => isOperatorOperand(operand) && operand.operator === "if" && operand.operands.length === 3
218
+ );
219
+
220
+ if (index === -1) return undefined;
221
+
222
+ const ternary = expr.operands[index];
223
+
224
+ if (ternary === undefined || !isOperatorOperand(ternary)) return undefined;
225
+
226
+ const [condition, whenTrue, whenFalse] = triple(ternary.operands);
227
+
228
+ if (!isNamedOperand(condition)) return undefined;
229
+ const fieldRef = resolveFieldReference(condition.name, context);
230
+
231
+ if (scalarType(fieldRef) !== "boolean" || fieldRef.relations?.length) return undefined;
232
+ // A missing condition is an error that selects neither branch; its presence test keeps it one.
233
+ const conditionPresent = mayBeMissing(fieldRef) ? presence(condition, fieldRef) : undefined;
234
+
235
+ // A relation reached in a branch would be required around the whole expansion under an
236
+ // enclosing negation (see negateRequiringHops), where CEL never evaluates the unselected branch.
237
+ const reachesRelation = (operand: PlanExpressionOperand): boolean =>
238
+ isNamedOperand(operand)
239
+ ? (resolveFieldReference(operand.name, context).relations?.length ?? 0) > 0
240
+ : isOperatorOperand(operand) && operand.operands.some(reachesRelation);
241
+
242
+ if (expr.operands.some(reachesRelation)) return undefined;
243
+
244
+ const branch = (value: PlanExpressionOperand): PlanExpressionOperand =>
245
+ constantFoldExpression({
246
+ operator: expr.operator,
247
+ operands: expr.operands.map((operand, i) => (i === index ? value : operand)),
248
+ });
249
+
250
+ const [onTrue, onFalse] = [branch(whenTrue), branch(whenFalse)];
251
+
252
+ const settles = (leaf: PlanExpressionOperand) =>
253
+ isValueOperand(leaf) || (isOperatorOperand(leaf) && leafOutcomes(leaf, context) !== undefined);
254
+
255
+ if (!settles(onTrue) && !settles(onFalse)) return undefined;
256
+
257
+ return {
258
+ operator: "or",
259
+ operands: [
260
+ { operator: "and", operands: [condition, onTrue] },
261
+ { operator: "and", operands: [{ operator: "not", operands: [condition] }, onFalse] },
262
+ ...(conditionPresent === undefined
263
+ ? []
264
+ : [{ operator: "not", operands: [conditionPresent] }]),
265
+ ],
266
+ };
267
+ }
268
+
269
+ /** Operators whose value is a boolean (or an error), never anything else. */
270
+ const BOOLEAN_OPERATORS = new Set([
271
+ "and",
272
+ "or",
273
+ "not",
274
+ "eq",
275
+ "ne",
276
+ "lt",
277
+ "le",
278
+ "gt",
279
+ "ge",
280
+ "in",
281
+ "contains",
282
+ "startsWith",
283
+ "endsWith",
284
+ "matches",
285
+ "exists",
286
+ "all",
287
+ "exists_one",
288
+ "hasIntersection",
289
+ ]);
290
+
291
+ /**
292
+ * `b == true`, `b != false` (and the mirrors) over a boolean-valued `b` are `b`; `b == false` and
293
+ * `b != true` are `!b`. An error in `b` is an error either way.
294
+ */
295
+ function unwrapBooleanComparison(expr: OperatorOperand): PlanExpressionOperand | undefined {
296
+ if ((expr.operator !== "eq" && expr.operator !== "ne") || expr.operands.length !== 2) {
297
+ return undefined;
298
+ }
299
+
300
+ const [first, second] = pair(expr.operands);
301
+ const [inner, literal] = isValueOperand(first) ? [second, first] : [first, second];
302
+
303
+ if (
304
+ !isValueOperand(literal) ||
305
+ !isBoolean(literal.value) ||
306
+ !isOperatorOperand(inner) ||
307
+ !BOOLEAN_OPERATORS.has(inner.operator)
308
+ ) {
309
+ return undefined;
310
+ }
311
+
312
+ const same = (expr.operator === "eq") === literal.value;
313
+
314
+ return same ? inner : { operator: "not", operands: [inner] };
315
+ }
316
+
317
+ /**
318
+ * `string(column) == "lit"` and `int(column) == k` (and their `!=` forms), solved for the column
319
+ * the cast wraps: a Prisma filter has no cast, but these conversions are invertible.
320
+ *
321
+ * - `string(b)` of a boolean is `"true"` or `"false"`, for a column or any boolean-valued
322
+ * expression (see renderedBooleanComparison).
323
+ * - `string(d)` of a number is cel-go's shortest `%g` rendering, so exactly one double prints as a
324
+ * given literal — or none does, and the comparison is settled like any type mismatch.
325
+ * - `int(d)` truncates toward zero, and raises an error outside (-2^63, 2^63). A threshold is a
326
+ * half-open interval that would need that bound spelled out, which an `Int` column rejects, so
327
+ * only `==` outside a `!` and `!=` under one are translated: the interval `==` solves to already
328
+ * excludes every value that overflows, and the error then decides the row as the constant does.
329
+ *
330
+ * A column the caller sends as an explicit null is left alone: the cast of a null VALUE is an
331
+ * error, where the comparison it would be rewritten to is definite.
332
+ */
333
+ function rewriteCastComparison(
334
+ expr: OperatorOperand,
335
+ context: TranslationContext,
336
+ positive: boolean
337
+ ): PlanExpressionOperand | undefined {
338
+ const { operator } = expr;
339
+
340
+ if ((operator !== "eq" && operator !== "ne") || expr.operands.length !== 2) return undefined;
341
+ const [first, second] = pair(expr.operands);
342
+ const [cast, literal] = isOperatorOperand(first) ? [first, second] : [second, first];
343
+
344
+ if (
345
+ !isOperatorOperand(cast) ||
346
+ cast.operands.length !== 1 ||
347
+ !isValueOperand(literal) ||
348
+ literal.value === null
349
+ ) {
350
+ return undefined;
351
+ }
352
+
353
+ const column = cast.operands[0]!;
354
+ const value = literal.value;
355
+
356
+ if (cast.operator === "string" && isString(value) && !isNamedOperand(column)) {
357
+ if (isBooleanValued(column, context)) {
358
+ return renderedBooleanComparison(column, value, operator);
359
+ }
360
+
361
+ // string(int(x)) renders the int in plain decimal, so the comparison is int(x) == k for the
362
+ // one k that renders as the literal, solved below on the next pass. A literal no int renders
363
+ // as, or one past 2^53, is left alone: the int's overflow error has no bound an Int column
364
+ // accepts.
365
+ const rendered = isOperatorOperand(column) && column.operator === "int" ? Number(value) : NaN;
366
+
367
+ return /^(0|-?[1-9][0-9]*)$/.test(value) && Number.isSafeInteger(rendered)
368
+ ? { operator, operands: [column, { value: rendered }] }
369
+ : undefined;
370
+ }
371
+
372
+ if (!isNamedOperand(column)) return undefined;
373
+ const fieldRef = resolveFieldReference(column.name, context);
374
+
375
+ if (fieldRef.nullAttributeRepresentation === "explicit") return undefined;
376
+ const type = scalarType(fieldRef);
377
+
378
+ // A column reached through a to-one relation is solved for only as a boolean rendering: the
379
+ // comparison it becomes is one the translator already guards through the hop.
380
+ if (fieldRef.relations?.length && (cast.operator !== "string" || type !== "boolean")) {
381
+ return undefined;
382
+ }
383
+
384
+ const compare = (op: string, v: Value): PlanExpressionOperand => ({
385
+ operator: op,
386
+ operands: [column, { value: v }],
387
+ });
388
+
389
+ const equality = (v: Value): PlanExpressionOperand => compare(operator, v);
390
+
391
+ // No value of the column prints as the literal: the comparison is settled by its type alone.
392
+ const unmatched: PlanExpressionOperand = {
393
+ operator,
394
+ operands: [column, { operator: "struct", operands: [] }],
395
+ };
396
+
397
+ if (cast.operator === "string" && isString(value)) {
398
+ if (type === "boolean") {
399
+ return value === "true" || value === "false" ? equality(value === "true") : unmatched;
400
+ }
401
+
402
+ if (type === "number") {
403
+ const parsed = Number(value);
404
+
405
+ return value.trim() !== "" && formatCelDouble(parsed) === value
406
+ ? equality(parsed)
407
+ : unmatched;
408
+ }
409
+
410
+ return undefined;
411
+ }
412
+
413
+ if (cast.operator === "int" && type === "number" && isNumber(value)) {
414
+ if ((operator === "eq") !== positive) return undefined;
415
+
416
+ if (!Number.isInteger(value)) return unmatched;
417
+
418
+ const range: PlanExpressionOperand = {
419
+ operator: "and",
420
+ operands:
421
+ value > 0
422
+ ? [compare("ge", value), compare("lt", value + 1)]
423
+ : value < 0
424
+ ? [compare("gt", value - 1), compare("le", value)]
425
+ : [compare("gt", -1), compare("lt", 1)],
426
+ };
427
+
428
+ return operator === "eq" ? range : { operator: "not", operands: [range] };
429
+ }
430
+
431
+ return undefined;
432
+ }
433
+
434
+ /**
435
+ * `string(b) == "lit"` (and `!=`) over a boolean-valued expression `b`: CEL renders a boolean as
436
+ * `"true"` or `"false"`, so the comparison is `b`, `!b`, or for any other literal false wherever
437
+ * `b` evaluates, and an error wherever `b` is one — which `b && !b` is, exactly.
438
+ */
439
+ function renderedBooleanComparison(
440
+ expression: PlanExpressionOperand,
441
+ literal: string,
442
+ operator: string
443
+ ): PlanExpressionOperand {
444
+ const negated: PlanExpressionOperand = { operator: "not", operands: [expression] };
445
+
446
+ const holds: PlanExpressionOperand =
447
+ literal === "true"
448
+ ? expression
449
+ : literal === "false"
450
+ ? negated
451
+ : { operator: "and", operands: [expression, negated] };
452
+
453
+ return operator === "eq" ? holds : { operator: "not", operands: [holds] };
454
+ }
455
+
456
+ /**
457
+ * Whether `expr` is a boolean, or an error, on every row: a boolean operator, a boolean literal,
458
+ * a boolean column, or a ternary whose arms both are.
459
+ */
460
+ function isBooleanValued(expr: PlanExpressionOperand, context: TranslationContext): boolean {
461
+ if (isValueOperand(expr)) return isBoolean(expr.value);
462
+
463
+ if (isNamedOperand(expr)) {
464
+ return scalarType(resolveFieldReference(expr.name, context)) === "boolean";
465
+ }
466
+
467
+ if (BOOLEAN_OPERATORS.has(expr.operator)) return true;
468
+
469
+ return (
470
+ expr.operator === "if" &&
471
+ expr.operands.length === 3 &&
472
+ expr.operands.slice(1).every((arm) => isBooleanValued(arm, context))
473
+ );
474
+ }
475
+
476
+ /**
477
+ * A comparison whose only column appears as `x / x` or `x / ±0`, split on the sign of `x`. CEL
478
+ * numbers are doubles, so neither division raises an error: `x / x` is 1, or NaN at zero, and
479
+ * `x / ±0` is an infinity signed by both operands, or NaN at zero. Each arm substitutes that
480
+ * constant, leaving a comparison between constants that folds; the arms' guards are plain
481
+ * comparisons on `x`, UNKNOWN on a NULL column, as the missing attribute's error requires.
482
+ */
483
+ function splitDivision(
484
+ expr: OperatorOperand,
485
+ context: TranslationContext
486
+ ): PlanExpressionOperand | undefined {
487
+ if (!COMPARISON_OPERATORS.has(expr.operator)) return undefined;
488
+ const column = divisionColumn(expr);
489
+
490
+ if (column === undefined) return undefined;
491
+ const fieldRef = resolveFieldReference(column.name, context);
492
+
493
+ if (
494
+ fieldRef.valueType !== "number" ||
495
+ (fieldRef.relations?.length ?? 0) > 0 ||
496
+ fieldRef.nullAttributeRepresentation === "explicit"
497
+ ) {
498
+ return undefined;
499
+ }
500
+
501
+ const sign = (op: string): PlanExpressionOperand => ({
502
+ operator: op,
503
+ operands: [column, { value: 0 }],
504
+ });
505
+
506
+ const arms: PlanExpressionOperand[] = [];
507
+
508
+ for (const [guard, x] of [
509
+ ["gt", 1],
510
+ ["lt", -1],
511
+ ["eq", 0],
512
+ ] as const) {
513
+ const substituted = substituteDivision(expr, column.name, x);
514
+
515
+ if (substituted === undefined) return undefined;
516
+ arms.push({ operator: "and", operands: [sign(guard), substituted] });
517
+ }
518
+
519
+ return { operator: "or", operands: arms };
520
+ }
521
+
522
+ /** The one column of a comparison when every reference to it sits in `x / x` or `x / ±0`. */
523
+ function divisionColumn(expr: PlanExpressionOperand): NamedOperand | undefined {
524
+ let column: NamedOperand | undefined;
525
+ let divided = false;
526
+
527
+ const visit = (node: PlanExpressionOperand): boolean => {
528
+ if (isNamedOperand(node)) {
529
+ if (column !== undefined && column.name !== node.name) return false;
530
+ column = node;
531
+
532
+ return false;
533
+ }
534
+
535
+ if (!isOperatorOperand(node)) return true;
536
+
537
+ if (node.operator === "lambda") return false;
538
+
539
+ const division = columnDivision(node);
540
+
541
+ if (division !== undefined) {
542
+ const { numerator } = division;
543
+
544
+ if (column !== undefined && column.name !== numerator.name) return false;
545
+ column = numerator;
546
+ divided = true;
547
+
548
+ return true;
549
+ }
550
+
551
+ return node.operands.every(visit);
552
+ };
553
+
554
+ return visit(expr) && divided ? column : undefined;
555
+ }
556
+
557
+ /** `divisor` is the constant zero, signed, or undefined where the column divides itself. */
558
+ type ColumnDivision = { numerator: NamedOperand; divisor: number | undefined };
559
+
560
+ /** `x / x` or `x / 0` over a column: the division whose quotient the column's value decides. */
561
+ function columnDivision(node: OperatorOperand): ColumnDivision | undefined {
562
+ if (node.operator !== "div" || node.operands.length !== 2) return undefined;
563
+ const [numerator, denominator] = pair(node.operands);
564
+
565
+ if (!isNamedOperand(numerator)) return undefined;
566
+
567
+ if (isNamedOperand(denominator)) {
568
+ return denominator.name === numerator.name ? { numerator, divisor: undefined } : undefined;
569
+ }
570
+
571
+ return isValueOperand(denominator) && isNumber(denominator.value) && denominator.value === 0
572
+ ? { numerator, divisor: denominator.value }
573
+ : undefined;
574
+ }
575
+
576
+ /**
577
+ * `expr` with every division of the column replaced by its value when the column has the sign of
578
+ * `x`, or undefined when the column is still referenced outside one.
579
+ */
580
+ function substituteDivision(
581
+ expr: PlanExpressionOperand,
582
+ name: string,
583
+ x: number
584
+ ): PlanExpressionOperand | undefined {
585
+ if (isNamedOperand(expr)) return undefined;
586
+
587
+ if (!isOperatorOperand(expr)) return expr;
588
+
589
+ const division = columnDivision(expr);
590
+
591
+ if (division !== undefined) return { value: x / (division.divisor ?? x) };
592
+
593
+ const operands: PlanExpressionOperand[] = [];
594
+
595
+ for (const operand of expr.operands) {
596
+ const substituted = substituteDivision(operand, name, x);
597
+
598
+ if (substituted === undefined) return undefined;
599
+ operands.push(substituted);
600
+ }
601
+
602
+ return { operator: expr.operator, operands };
603
+ }
604
+
605
+ /** Past this many code points, a literal is not split across a concatenation of columns. */
606
+ const MAX_CONCATENATION_SPLITS = 256;
607
+
608
+ /** Past this many ways to split the literal, the concatenation is left to the translator. */
609
+ const MAX_CONCATENATION_ARMS = 256;
610
+
611
+ /**
612
+ * `a + "sep" + b == "lit"` over string columns and string literals, with at least two columns:
613
+ * one arm per way of splitting the literal into consecutive pieces, each column taking one piece
614
+ * (`a == prefix && b == suffix`) and each literal term matching its own text in place. Each arm
615
+ * also ORs in `!startsWith(column, "")` for every column — FALSE for a present value, UNKNOWN for a
616
+ * NULL one — so a missing operand leaves the whole comparison UNKNOWN under both polarities, as
617
+ * the error `+` raises on it does in CEL.
618
+ */
619
+ function rewriteConcatenation(
620
+ expr: OperatorOperand,
621
+ context: TranslationContext
622
+ ): PlanExpressionOperand | undefined {
623
+ const { operator } = expr;
624
+
625
+ if ((operator !== "eq" && operator !== "ne") || expr.operands.length !== 2) return undefined;
626
+ const [first, second] = pair(expr.operands);
627
+ const [sum, literal] = isOperatorOperand(first) ? [first, second] : [second, first];
628
+
629
+ if (
630
+ !isOperatorOperand(sum) ||
631
+ sum.operator !== "add" ||
632
+ !isValueOperand(literal) ||
633
+ !isString(literal.value)
634
+ ) {
635
+ return undefined;
636
+ }
637
+
638
+ const terms = concatenationTerms(sum);
639
+
640
+ if (terms === undefined) return undefined;
641
+ const columns = terms.filter(isNamedOperand);
642
+
643
+ if (
644
+ columns.length < 2 ||
645
+ columns.some((column) => {
646
+ const fieldRef = resolveFieldReference(column.name, context);
647
+
648
+ return fieldRef.valueType !== "string" || (fieldRef.relations?.length ?? 0) > 0;
649
+ })
650
+ ) {
651
+ return undefined;
652
+ }
653
+
654
+ const codePoints = Array.from(literal.value);
655
+
656
+ if (codePoints.length > MAX_CONCATENATION_SPLITS) return undefined;
657
+
658
+ const arms: PlanExpressionOperand[] = [];
659
+
660
+ const split = (index: number, offset: number, pieces: PlanExpressionOperand[]): boolean => {
661
+ if (index === terms.length) {
662
+ if (offset !== codePoints.length) return true;
663
+ arms.push(pieces.length === 1 ? pieces[0]! : { operator: "and", operands: pieces });
664
+
665
+ return arms.length <= MAX_CONCATENATION_ARMS;
666
+ }
667
+
668
+ const term = terms[index]!;
669
+
670
+ if ("value" in term) {
671
+ const text = Array.from(term.value);
672
+ const matches = text.every((point, i) => codePoints[offset + i] === point);
673
+
674
+ return !matches || split(index + 1, offset + text.length, pieces);
675
+ }
676
+
677
+ const last = index === terms.length - 1;
678
+
679
+ for (let end = last ? codePoints.length : offset; end <= codePoints.length; end++) {
680
+ const piece = codePoints.slice(offset, end).join("");
681
+ const equality = { operator: "eq", operands: [term, { value: piece }] };
682
+
683
+ if (!split(index + 1, end, [...pieces, equality])) return false;
684
+ }
685
+
686
+ return true;
687
+ };
688
+
689
+ if (!split(0, 0, [])) return undefined;
690
+
691
+ for (const column of columns) {
692
+ arms.push({
693
+ operator: "not",
694
+ operands: [{ operator: "startsWith", operands: [column, { value: "" }] }],
695
+ });
696
+ }
697
+
698
+ const equality: PlanExpressionOperand = { operator: "or", operands: arms };
699
+
700
+ return operator === "eq" ? equality : { operator: "not", operands: [equality] };
701
+ }
702
+
703
+ /** The terms of a nested `add` of string columns and string literals, left to right. */
704
+ function concatenationTerms(
705
+ expr: PlanExpressionOperand
706
+ ): (NamedOperand | { value: string })[] | undefined {
707
+ if (isNamedOperand(expr)) return [expr];
708
+
709
+ if (isValueOperand(expr)) {
710
+ return isString(expr.value) ? [{ value: expr.value }] : undefined;
711
+ }
712
+
713
+ if (!isOperatorOperand(expr) || expr.operator !== "add" || expr.operands.length !== 2) {
714
+ return undefined;
715
+ }
716
+
717
+ const left = concatenationTerms(expr.operands[0]!);
718
+ const right = concatenationTerms(expr.operands[1]!);
719
+
720
+ return left && right && [...left, ...right];
721
+ }
722
+
723
+ /**
724
+ * CEL's string() of a double: cel-go's `fmt.Sprintf("%g", d)`, the shortest digits that
725
+ * round-trip, in exponent form below 1e-4 or from 1e+06.
726
+ */
727
+ export function formatCelDouble(x: number): string {
728
+ if (Number.isNaN(x)) return "NaN";
729
+
730
+ if (x === Infinity) return "+Inf";
731
+
732
+ if (x === -Infinity) return "-Inf";
733
+
734
+ if (x === 0) return Object.is(x, -0) ? "-0" : "0";
735
+ const sign = x < 0 ? "-" : "";
736
+ const exponential = Math.abs(x).toExponential();
737
+ const exponentAt = exponential.indexOf("e");
738
+ const mantissa = exponential.slice(0, exponentAt);
739
+ const exponentText = exponential.slice(exponentAt + 1);
740
+ const digits = mantissa.replace(".", "");
741
+ const exponent = Number(exponentText);
742
+
743
+ if (exponent < -4 || exponent >= 6) {
744
+ const fraction = digits.length > 1 ? `.${digits.slice(1)}` : "";
745
+ const magnitude = String(Math.abs(exponent)).padStart(2, "0");
746
+
747
+ return `${sign}${digits[0]}${fraction}e${exponent < 0 ? "-" : "+"}${magnitude}`;
748
+ }
749
+
750
+ const point = exponent + 1;
751
+
752
+ if (point <= 0) return `${sign}0.${"0".repeat(-point)}${digits}`;
753
+
754
+ if (point >= digits.length) return `${sign}${digits}${"0".repeat(point - digits.length)}`;
755
+
756
+ return `${sign}${digits.slice(0, point)}.${digits.slice(point)}`;
757
+ }
758
+
759
+ // -- the leaves ----------------------------------------------------------------------------------
760
+
761
+ function leafOutcomes(
762
+ expr: OperatorOperand,
763
+ context: TranslationContext
764
+ ): Outcomes | undefined {
765
+ const { operator, operands } = expr;
766
+
767
+ // A pattern RE2 rejects fails on every row it is evaluated for.
768
+ if (operator === "matches" && operands.length === 2) {
769
+ const pattern = operands[1]!;
770
+
771
+ if (isValueOperand(pattern) && isString(pattern.value) && isInvalidPattern(pattern.value)) {
772
+ return { error: true };
773
+ }
774
+ }
775
+
776
+ // A list where a boolean is required: CEL's logical operators raise a no-overload error on it,
777
+ // and a condition that is not a boolean denies.
778
+ if (LIST_OPERATORS.has(operator)) return { error: true };
779
+
780
+ if (COMPARISON_OPERATORS.has(operator) && operands.length === 2) {
781
+ const [left, right] = pair(operands);
782
+ // NaN equals nothing and orders against nothing — not even a string, on the current PDP.
783
+ const nan = [left, right].find((o) => isValueOperand(o) && Number.isNaN(o.value));
784
+
785
+ if (nan !== undefined) {
786
+ const other = nan === left ? right : left;
787
+
788
+ if (!isNamedOperand(other) && !isValueOperand(other)) return undefined;
789
+
790
+ return definite(operator === "ne", [other], context);
791
+ }
792
+
793
+ const nullComparison = omittedNullComparison(operator, left, right, context);
794
+
795
+ if (nullComparison !== undefined) return nullComparison;
796
+
797
+ const size = [left, right].find(
798
+ (o): o is OperatorOperand => isOperatorOperand(o) && o.operator === "size"
799
+ );
800
+
801
+ if (size !== undefined) {
802
+ // size() has overloads for strings, lists and maps only.
803
+ const [subject] = size.operands;
804
+ const type = subject && scalarFieldType(subject, context);
805
+
806
+ return type === "number" || type === "boolean" ? { error: true } : undefined;
807
+ }
808
+
809
+ // A bare date-time column is the RFC 3339 STRING the application sends, and a string never
810
+ // orders against a timestamp: no overload, so an error on every row. Equality across the two
811
+ // types is heterogeneous, and false.
812
+ if (comparesRawTemporalWithTimestamp(left, right, context)) {
813
+ return ORDERING_OPERATORS.has(operator)
814
+ ? { error: true }
815
+ : definite(operator !== "eq", [left, right], context);
816
+ }
817
+
818
+ const leftType = operandType(left, context);
819
+ const rightType = operandType(right, context);
820
+
821
+ if (
822
+ leftType === undefined ||
823
+ rightType === undefined ||
824
+ leftType.type === rightType.type ||
825
+ leftType.type === "null" ||
826
+ rightType.type === "null"
827
+ ) {
828
+ return undefined;
829
+ }
830
+
831
+ if (ORDERING_OPERATORS.has(operator)) return { error: true };
832
+
833
+ return definite(operator !== "eq", [left, right], context);
834
+ }
835
+
836
+ if (STRING_OPERATORS.has(operator) && operands.length === 2) {
837
+ const mistyped = operands.some((operand) => {
838
+ const type = operandType(operand, context)?.type;
839
+
840
+ return type !== undefined && type !== "string" && type !== "null";
841
+ });
842
+
843
+ return mistyped ? { error: true } : undefined;
844
+ }
845
+
846
+ if (HIERARCHY_OPERATORS.has(operator)) {
847
+ // hierarchy() parses a string; any other scalar is a no-overload error.
848
+ const mistyped = operands.some((operand) => {
849
+ if (!isOperatorOperand(operand) || operand.operator !== "hierarchy") return false;
850
+ const [subject] = operand.operands;
851
+ const type = subject && scalarFieldType(subject, context);
852
+
853
+ return type === "number" || type === "boolean";
854
+ });
855
+
856
+ return mistyped ? { error: true } : undefined;
857
+ }
858
+
859
+ if (operator === "in" && operands.length === 2) {
860
+ const [member, collection] = pair(operands);
861
+ // A literal member against a mapped list whose elements can never equal it.
862
+ const memberType = literalType(member);
863
+ const list = collectionElementType(collection, context);
864
+
865
+ if (memberType !== undefined && memberType !== "null" && list !== undefined) {
866
+ return memberType === list.type ? undefined : { false: true, error: list.mayBeMissing };
867
+ }
868
+
869
+ // A field member against a literal list none of whose elements it can equal.
870
+ const field = operandType(member, context);
871
+ const elements = literalElements(collection);
872
+
873
+ if (field !== undefined && elements !== undefined) {
874
+ return elements.every((element) => isMismatch(field.type, element))
875
+ ? definite(false, [member], context)
876
+ : undefined;
877
+ }
878
+
879
+ return undefined;
880
+ }
881
+
882
+ if (operator === "hasIntersection" && operands.length === 2) {
883
+ const [left, right] = pair(operands);
884
+
885
+ for (const [list, literal] of [
886
+ [left, right],
887
+ [right, left],
888
+ ] as const) {
889
+ const elementType = collectionElementType(list, context);
890
+ const elements = literalElements(literal);
891
+
892
+ if (elementType !== undefined && elements !== undefined) {
893
+ return elements.every((element) => isMismatch(elementType.type, element))
894
+ ? { false: true, error: elementType.mayBeMissing }
895
+ : undefined;
896
+ }
897
+ }
898
+ }
899
+
900
+ return undefined;
901
+ }
902
+
903
+ /** Whether one operand is a bare `dateTime` column and the other a `timestamp(...)` call. */
904
+ function comparesRawTemporalWithTimestamp(
905
+ left: PlanExpressionOperand,
906
+ right: PlanExpressionOperand,
907
+ context: TranslationContext
908
+ ): boolean {
909
+ return [
910
+ [left, right],
911
+ [right, left],
912
+ ].some(
913
+ ([column, other]) =>
914
+ isNamedOperand(column!) &&
915
+ isOperatorOperand(other!) &&
916
+ other.operator === "timestamp" &&
917
+ resolveFieldReference(column.name, context).valueType === "dateTime"
918
+ );
919
+ }
920
+
921
+ /**
922
+ * `x == null` / `x != null` over a column the caller omits when NULL: a present value is never
923
+ * null, and an absent one is a missing-attribute error, so neither outcome can select the NULL
924
+ * rows an `IS NULL` would.
925
+ *
926
+ * A column read through to-one relations (`parent.x`) is also missing when a relation on the way
927
+ * is absent. Its presence test is `reached` rather than `missing`: a relation filter is not
928
+ * guaranteed to be UNKNOWN on the rows it fails, so it only answers where polarity is fixed.
929
+ */
930
+ function omittedNullComparison(
931
+ operator: string,
932
+ left: PlanExpressionOperand,
933
+ right: PlanExpressionOperand,
934
+ context: TranslationContext
935
+ ): Outcomes | undefined {
936
+ if (operator !== "eq" && operator !== "ne") return undefined;
937
+ const [field, literal] = isNamedOperand(left) ? [left, right] : [right, left];
938
+
939
+ if (!isNamedOperand(field) || !isValueOperand(literal) || literal.value !== null) {
940
+ return undefined;
941
+ }
942
+
943
+ const fieldRef = resolveFieldReference(field.name, context);
944
+ const relations = fieldRef.relations ?? [];
945
+
946
+ if (relations.some((relation) => relation.type !== "one")) return undefined;
947
+ const convention = fieldRef.nullAttributeRepresentation ?? context.nullRepresentation;
948
+
949
+ if (convention !== "omitted") return undefined;
950
+ const test = presence(field, fieldRef);
951
+
952
+ const presenceTest =
953
+ test === undefined ? {} : relations.length > 0 ? { reached: test } : { missing: [test] };
954
+
955
+ return {
956
+ [operator === "eq" ? "false" : "true"]: true,
957
+ error: true,
958
+ ...presenceTest,
959
+ };
960
+ }
961
+
962
+ /**
963
+ * A leaf that is `value` whenever the named operands among `operands` are present, and a
964
+ * missing-attribute error when one is not.
965
+ */
966
+ function definite(
967
+ value: boolean,
968
+ operands: PlanExpressionOperand[],
969
+ context: TranslationContext
970
+ ): Outcomes {
971
+ const missing: PlanExpressionOperand[] = [];
972
+ let unknowable = false;
973
+
974
+ const visit = (operand: PlanExpressionOperand): void => {
975
+ if (literalValue(operand) !== undefined) return;
976
+
977
+ // Arithmetic raises the error of any column it reads; another expression's errors are not
978
+ // known here, so its leaf is left as it stands.
979
+ if (isOperatorOperand(operand)) {
980
+ if (ARITHMETIC_OPERATORS.has(operand.operator)) operand.operands.forEach(visit);
981
+ else unknowable = true;
982
+
983
+ return;
984
+ }
985
+
986
+ if (!isNamedOperand(operand)) return;
987
+ const fieldRef = resolveFieldReference(operand.name, context);
988
+
989
+ if (!mayBeMissing(fieldRef)) return;
990
+ const test = presence(operand, fieldRef);
991
+
992
+ if (test === undefined) unknowable = true;
993
+ else missing.push(test);
994
+ };
995
+
996
+ operands.forEach(visit);
997
+
998
+ return {
999
+ [value ? "true" : "false"]: true,
1000
+ error: unknowable || missing.length > 0,
1001
+ missing: unknowable ? undefined : missing,
1002
+ };
1003
+ }
1004
+
1005
+ /**
1006
+ * A predicate TRUE for a present value of the column and UNKNOWN for a NULL one, spelled with
1007
+ * the plain comparisons the translator leaves to SQL's three-valued logic. Undefined for a column
1008
+ * whose type is not declared.
1009
+ */
1010
+ export function presence(
1011
+ column: NamedOperand,
1012
+ fieldRef: ResolvedFieldReference
1013
+ ): PlanExpressionOperand | undefined {
1014
+ const compare = (operator: string, value: Value): PlanExpressionOperand => ({
1015
+ operator,
1016
+ operands: [column, { value }],
1017
+ });
1018
+
1019
+ switch (scalarType(fieldRef)) {
1020
+ case "string":
1021
+ return compare("startsWith", "");
1022
+ case "number":
1023
+ return { operator: "or", operands: [compare("gt", 0), compare("le", 0)] };
1024
+ case "boolean":
1025
+ return { operator: "or", operands: [compare("eq", true), compare("eq", false)] };
1026
+ default:
1027
+ return undefined;
1028
+ }
1029
+ }
1030
+
1031
+ /**
1032
+ * Drops, from a literal list compared by element equality, every element that can never equal
1033
+ * the other side's elements: each one contributes a definite false to CEL's membership test, so
1034
+ * the list without it decides every row the same way.
1035
+ */
1036
+ function rewriteLiteralLists(
1037
+ expr: OperatorOperand,
1038
+ context: TranslationContext
1039
+ ): PlanExpressionOperand {
1040
+ const { operator, operands } = expr;
1041
+
1042
+ if (operands.length !== 2) return expr;
1043
+ const [first, second] = pair(operands);
1044
+
1045
+ if (operator === "in") {
1046
+ const field = operandType(first, context);
1047
+
1048
+ if (field === undefined) return expr;
1049
+ const pruned = pruneLiteralList(second, field.type);
1050
+
1051
+ return pruned === undefined ? expr : { operator, operands: [first, pruned] };
1052
+ }
1053
+
1054
+ if (operator === "hasIntersection") {
1055
+ const leftList = collectionElementType(first, context);
1056
+
1057
+ if (leftList !== undefined) {
1058
+ const pruned = pruneLiteralList(second, leftList.type);
1059
+
1060
+ return pruned === undefined ? expr : { operator, operands: [first, pruned] };
1061
+ }
1062
+
1063
+ const rightList = collectionElementType(second, context);
1064
+
1065
+ if (rightList !== undefined) {
1066
+ const pruned = pruneLiteralList(first, rightList.type);
1067
+
1068
+ return pruned === undefined ? expr : { operator, operands: [pruned, second] };
1069
+ }
1070
+ }
1071
+
1072
+ return expr;
1073
+ }
1074
+
1075
+ /** The literal list without its mismatched elements, or undefined when nothing is dropped. */
1076
+ function pruneLiteralList(
1077
+ operand: PlanExpressionOperand,
1078
+ type: CelType
1079
+ ): PlanExpressionOperand | undefined {
1080
+ if (!isValueOperand(operand) || !Array.isArray(operand.value)) return undefined;
1081
+ const kept = operand.value.filter((element) => !isMismatch(type, element));
1082
+
1083
+ if (kept.length === operand.value.length || kept.length === 0) return undefined;
1084
+
1085
+ return { value: kept };
1086
+ }
1087
+
1088
+ // -- operand types -------------------------------------------------------------------------------
1089
+
1090
+ type KnownType = { type: CelType; mayBeMissing: boolean };
1091
+
1092
+ function valueType(value: Value): CelType | undefined {
1093
+ if (value === null) return "null";
1094
+
1095
+ if (Array.isArray(value)) return "list";
1096
+
1097
+ switch (typeofValue(value)) {
1098
+ case "string":
1099
+ return "string";
1100
+ case "number":
1101
+ return "number";
1102
+ case "boolean":
1103
+ return "boolean";
1104
+ case "object":
1105
+ return "map";
1106
+ default:
1107
+ return undefined;
1108
+ }
1109
+ }
1110
+
1111
+ function isMismatch(type: CelType, element: Value): boolean {
1112
+ const elementType = valueType(element);
1113
+
1114
+ return elementType !== undefined && elementType !== "null" && elementType !== type;
1115
+ }
1116
+
1117
+ /** The CEL type of a literal operand: a value, or a `list(...)`/`struct(...)` constructor. */
1118
+ function literalType(operand: PlanExpressionOperand): CelType | undefined {
1119
+ if (isValueOperand(operand)) return valueType(operand.value);
1120
+
1121
+ if (isOperatorOperand(operand)) {
1122
+ if (operand.operator === "list") return "list";
1123
+
1124
+ if (operand.operator === "struct") return "map";
1125
+ }
1126
+
1127
+ return undefined;
1128
+ }
1129
+
1130
+ /** The elements of a literal list operand, with nested constructors standing in for their type. */
1131
+ function literalElements(operand: PlanExpressionOperand): Value[] | undefined {
1132
+ if (isValueOperand(operand)) {
1133
+ return Array.isArray(operand.value) ? operand.value : undefined;
1134
+ }
1135
+
1136
+ if (isOperatorOperand(operand) && operand.operator === "list") {
1137
+ const elements: Value[] = [];
1138
+
1139
+ for (const element of operand.operands) {
1140
+ if (isValueOperand(element)) {
1141
+ elements.push(element.value);
1142
+ continue;
1143
+ }
1144
+
1145
+ const type = literalType(element);
1146
+
1147
+ if (type === "list") elements.push([]);
1148
+ else if (type === "map") elements.push({});
1149
+ else return undefined;
1150
+ }
1151
+
1152
+ return elements;
1153
+ }
1154
+
1155
+ return undefined;
1156
+ }
1157
+
1158
+ function operandType(
1159
+ operand: PlanExpressionOperand,
1160
+ context: TranslationContext
1161
+ ): KnownType | undefined {
1162
+ const literal = literalType(operand);
1163
+
1164
+ if (literal !== undefined) return { type: literal, mayBeMissing: false };
1165
+
1166
+ if (isOperatorOperand(operand)) return arithmeticType(operand, context);
1167
+
1168
+ if (!isNamedOperand(operand)) return undefined;
1169
+ const fieldRef = resolveFieldReference(operand.name, context);
1170
+ const type = scalarType(fieldRef);
1171
+
1172
+ if (type === undefined) return undefined;
1173
+
1174
+ // A relation-backed reference that is not a to-one scalar is a collection, not a scalar.
1175
+ if (fieldRef.relations?.some((relation) => relation.type !== "one")) return undefined;
1176
+
1177
+ return { type, mayBeMissing: mayBeMissing(fieldRef) };
1178
+ }
1179
+
1180
+ /**
1181
+ * The type of `a + b`, `a - b`, `a * b` or `a / b` when both operands are known numbers, or of
1182
+ * `a + b` when both are strings: the result's type, missing when either operand is. Any other
1183
+ * pairing is left unknown, as a no-overload error is only certain of the typed ones.
1184
+ */
1185
+ function arithmeticType(
1186
+ expr: OperatorOperand,
1187
+ context: TranslationContext
1188
+ ): KnownType | undefined {
1189
+ if (!ARITHMETIC_OPERATORS.has(expr.operator) || expr.operands.length !== 2) return undefined;
1190
+ const [left, right] = expr.operands.map((operand) => operandType(operand, context));
1191
+
1192
+ if (left === undefined || right === undefined || left.type !== right.type) return undefined;
1193
+
1194
+ if (left.type !== "number" && (left.type !== "string" || expr.operator !== "add")) {
1195
+ return undefined;
1196
+ }
1197
+
1198
+ return { type: left.type, mayBeMissing: left.mayBeMissing || right.mayBeMissing };
1199
+ }
1200
+
1201
+ function scalarFieldType(
1202
+ operand: PlanExpressionOperand,
1203
+ context: TranslationContext
1204
+ ): CelType | undefined {
1205
+ return operandType(operand, context)?.type;
1206
+ }
1207
+
1208
+ /** Date-time columns are RFC 3339 strings in CEL but not in the store, so they stay unknown. */
1209
+ function scalarType(fieldRef: ResolvedFieldReference): CelType | undefined {
1210
+ switch (fieldRef.valueType) {
1211
+ case "string":
1212
+ case "number":
1213
+ case "boolean":
1214
+ return fieldRef.valueType;
1215
+ default:
1216
+ return undefined;
1217
+ }
1218
+ }
1219
+
1220
+ /**
1221
+ * Whether evaluating the reference can raise a missing-attribute error: a to-one hop that may be
1222
+ * absent, or a column that may be NULL. A column sent as an explicit null holds a null VALUE,
1223
+ * which heterogeneous equality compares definitely. Only `nullable: false` says a column is never
1224
+ * NULL: an undeclared mapping is not evidence of NOT NULL, and settling a comparison as if it
1225
+ * were would admit the NULL rows under a negation.
1226
+ */
1227
+ function mayBeMissing(fieldRef: ResolvedFieldReference): boolean {
1228
+ if (fieldRef.relations?.some((relation) => relation.type === "one")) return true;
1229
+
1230
+ if (fieldRef.nullAttributeRepresentation === "explicit") return false;
1231
+
1232
+ return fieldRef.nullable !== false;
1233
+ }
1234
+
1235
+ /**
1236
+ * The element type of a mapped list: a relation-backed list reference, or `map()` projecting one
1237
+ * element column. Undefined when the mapping does not declare it.
1238
+ */
1239
+ function collectionElementType(
1240
+ operand: PlanExpressionOperand,
1241
+ context: TranslationContext
1242
+ ): KnownType | undefined {
1243
+ if (isNamedOperand(operand)) {
1244
+ const fieldRef = resolveFieldReference(operand.name, context);
1245
+ const relations = fieldRef.relations;
1246
+
1247
+ if (!relations || relations.length === 0) return undefined;
1248
+ const last = relations[relations.length - 1]!;
1249
+
1250
+ if (last.type !== "many" || last.field === undefined) return undefined;
1251
+
1252
+ const declared =
1253
+ lookupMapping(context.mapper, operand.name)?.valueType ??
1254
+ elementFieldValueType(context, operand.name, last.field);
1255
+
1256
+ const type = declared && scalarType({ path: [], valueType: declared });
1257
+
1258
+ if (type === undefined) return undefined;
1259
+
1260
+ return { type, mayBeMissing: relations.some((relation) => relation.type === "one") };
1261
+ }
1262
+
1263
+ if (isOperatorOperand(operand) && operand.operator === "map") {
1264
+ const [collection, lambda] = operand.operands;
1265
+
1266
+ if (
1267
+ collection === undefined ||
1268
+ !isNamedOperand(collection) ||
1269
+ lambda === undefined ||
1270
+ !isOperatorOperand(lambda) ||
1271
+ lambda.operator !== "lambda"
1272
+ ) {
1273
+ return undefined;
1274
+ }
1275
+
1276
+ const [projection, variable] = lambda.operands;
1277
+
1278
+ if (
1279
+ projection === undefined ||
1280
+ !isNamedOperand(projection) ||
1281
+ variable === undefined ||
1282
+ !isNamedOperand(variable)
1283
+ ) {
1284
+ return undefined;
1285
+ }
1286
+
1287
+ const scoped = scopedContext(context, collection.name, variable.name);
1288
+
1289
+ if (scoped === undefined) return undefined;
1290
+ const type = scalarType(resolveFieldReference(projection.name, scoped));
1291
+
1292
+ if (type === undefined) return undefined;
1293
+
1294
+ // A projected element column can be missing on an element, which errors the whole map().
1295
+ return { type, mayBeMissing: true };
1296
+ }
1297
+
1298
+ return undefined;
1299
+ }
1300
+
1301
+ /** The declared valueType of the projected element column of a relation-backed list. */
1302
+ function elementFieldValueType(
1303
+ context: TranslationContext,
1304
+ reference: string,
1305
+ field: string
1306
+ ): MapperConfig["valueType"] {
1307
+ const relation = lookupMapping(context.mapper, reference)?.relation;
1308
+
1309
+ return relation?.fields?.[field]?.valueType;
1310
+ }