@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/hierarchy.ts
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
// The hierarchy operators: `overlaps`, `ancestorOf`, `descendentOf` over `hierarchy(...)`
|
|
2
|
+
// operands, lowered to equality, IN-lists of strict prefixes, and a prefix match on a column.
|
|
3
|
+
|
|
4
|
+
import type { PlanExpressionOperand } from "@cerbos/core";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
assertStringField,
|
|
8
|
+
buildFieldFilter,
|
|
9
|
+
buildImpossibleFilter,
|
|
10
|
+
buildPrefixFilter,
|
|
11
|
+
} from "./fields";
|
|
12
|
+
import type { PrismaFilter } from "./index";
|
|
13
|
+
import { isResolvedValue, resolveFieldReference } from "./mapping";
|
|
14
|
+
import type { ResolvedFieldReference, TranslationContext } from "./mapping";
|
|
15
|
+
import {
|
|
16
|
+
assertDefined,
|
|
17
|
+
isNamedOperand,
|
|
18
|
+
isOperatorOperand,
|
|
19
|
+
isValueOperand,
|
|
20
|
+
} from "./plan";
|
|
21
|
+
import type { OperatorOperand } from "./plan";
|
|
22
|
+
import { resolveOperand } from "./translate";
|
|
23
|
+
import { UnsupportedQueryPlanError } from "./errors";
|
|
24
|
+
|
|
25
|
+
type ConstantSegment = { type: "constant"; value: string };
|
|
26
|
+
|
|
27
|
+
type FieldSegment = { type: "field"; fieldRef: ResolvedFieldReference };
|
|
28
|
+
|
|
29
|
+
type HierarchySegment = ConstantSegment | FieldSegment;
|
|
30
|
+
|
|
31
|
+
type ConstantHierarchy = {
|
|
32
|
+
type: "constant";
|
|
33
|
+
segments: string[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type FieldHierarchy = {
|
|
37
|
+
type: "field";
|
|
38
|
+
fieldRef: ResolvedFieldReference;
|
|
39
|
+
delimiter: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type SegmentedHierarchy = {
|
|
43
|
+
type: "segmented";
|
|
44
|
+
segments: HierarchySegment[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type ResolvedHierarchy = ConstantHierarchy | FieldHierarchy | SegmentedHierarchy;
|
|
48
|
+
|
|
49
|
+
function resolveHierarchy(
|
|
50
|
+
expr: OperatorOperand,
|
|
51
|
+
context: TranslationContext
|
|
52
|
+
): ResolvedHierarchy {
|
|
53
|
+
const operands = expr.operands;
|
|
54
|
+
|
|
55
|
+
if (operands.length === 2) {
|
|
56
|
+
const strOperand = assertDefined(operands[0], "hierarchy requires operands");
|
|
57
|
+
|
|
58
|
+
const delimOperand = assertDefined(
|
|
59
|
+
operands[1],
|
|
60
|
+
"hierarchy requires a delimiter"
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (!isValueOperand(delimOperand)) {
|
|
64
|
+
throw new UnsupportedQueryPlanError("hierarchy delimiter must be a value");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const delimiter = String(delimOperand.value);
|
|
68
|
+
|
|
69
|
+
if (isValueOperand(strOperand)) {
|
|
70
|
+
return {
|
|
71
|
+
type: "constant",
|
|
72
|
+
segments: splitPath(String(strOperand.value), delimiter),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (isNamedOperand(strOperand)) {
|
|
77
|
+
return {
|
|
78
|
+
type: "field",
|
|
79
|
+
fieldRef: resolveFieldReference(strOperand.name, context),
|
|
80
|
+
delimiter,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw new UnsupportedQueryPlanError("hierarchy(string, delimiter) requires a value or field operand");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (operands.length === 1) {
|
|
88
|
+
const inner = assertDefined(operands[0], "hierarchy requires an operand");
|
|
89
|
+
|
|
90
|
+
if (isValueOperand(inner)) {
|
|
91
|
+
return { type: "constant", segments: String(inner.value).split(".") };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (isNamedOperand(inner)) {
|
|
95
|
+
return {
|
|
96
|
+
type: "field",
|
|
97
|
+
fieldRef: resolveFieldReference(inner.name, context),
|
|
98
|
+
delimiter: ".",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (isOperatorOperand(inner) && inner.operator === "list") {
|
|
103
|
+
const segments = inner.operands.map((op): HierarchySegment => {
|
|
104
|
+
const resolved = resolveOperand(op, context);
|
|
105
|
+
|
|
106
|
+
if (isResolvedValue(resolved)) {
|
|
107
|
+
return { type: "constant", value: String(resolved.value) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { type: "field", fieldRef: resolved };
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return { type: "segmented", segments };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
throw new UnsupportedQueryPlanError("hierarchy requires a value, field, or list operand");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
throw new UnsupportedQueryPlanError("hierarchy requires 1 or 2 operands");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Cerbos splits a path with Go's `strings.Split`: an empty delimiter yields one segment per code
|
|
124
|
+
* point, and an empty path then has no segments at all.
|
|
125
|
+
*/
|
|
126
|
+
function splitPath(path: string, delimiter: string): string[] {
|
|
127
|
+
return delimiter === "" ? Array.from(path) : path.split(delimiter);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function toSegments(resolved: ResolvedHierarchy): HierarchySegment[] {
|
|
131
|
+
switch (resolved.type) {
|
|
132
|
+
case "constant":
|
|
133
|
+
return resolved.segments.map((s) => ({ type: "constant" as const, value: s }));
|
|
134
|
+
case "segmented":
|
|
135
|
+
return resolved.segments;
|
|
136
|
+
case "field":
|
|
137
|
+
throw new UnsupportedQueryPlanError(
|
|
138
|
+
"Cannot get segments from a field-reference hierarchy"
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** A segmented hierarchy whose segments are all constants is a constant hierarchy. */
|
|
144
|
+
function normalizeHierarchy(h: ResolvedHierarchy): ResolvedHierarchy {
|
|
145
|
+
if (h.type !== "segmented") return h;
|
|
146
|
+
const constants: string[] = [];
|
|
147
|
+
|
|
148
|
+
for (const segment of h.segments) {
|
|
149
|
+
if (segment.type !== "constant") return h;
|
|
150
|
+
constants.push(segment.value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { type: "constant", segments: constants };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function checkPrefixConditions(
|
|
157
|
+
shorter: HierarchySegment[],
|
|
158
|
+
longer: HierarchySegment[]
|
|
159
|
+
): PrismaFilter | null {
|
|
160
|
+
if (shorter.length > longer.length) return null;
|
|
161
|
+
|
|
162
|
+
const conditions: PrismaFilter[] = [];
|
|
163
|
+
|
|
164
|
+
for (let i = 0; i < shorter.length; i++) {
|
|
165
|
+
const s = shorter[i]!;
|
|
166
|
+
const l = longer[i]!;
|
|
167
|
+
|
|
168
|
+
if (s.type === "constant" && l.type === "constant") {
|
|
169
|
+
if (s.value !== l.value) return null;
|
|
170
|
+
} else if (s.type === "field" && l.type === "constant") {
|
|
171
|
+
conditions.push(buildFieldFilter(s.fieldRef, "equals", l.value));
|
|
172
|
+
} else if (s.type === "constant" && l.type === "field") {
|
|
173
|
+
conditions.push(buildFieldFilter(l.fieldRef, "equals", s.value));
|
|
174
|
+
} else {
|
|
175
|
+
throw new UnsupportedQueryPlanError(
|
|
176
|
+
"Cannot compare two field references in hierarchy overlap"
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (conditions.length === 0) return {};
|
|
182
|
+
|
|
183
|
+
if (conditions.length === 1) return conditions[0]!;
|
|
184
|
+
|
|
185
|
+
return { AND: conditions };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function handleOverlapsOperator(
|
|
189
|
+
operands: PlanExpressionOperand[],
|
|
190
|
+
context: TranslationContext,
|
|
191
|
+
): PrismaFilter {
|
|
192
|
+
const [left, right] = extractHierarchyOperands("overlaps", operands, context);
|
|
193
|
+
|
|
194
|
+
if (left.type === "field" || right.type === "field") {
|
|
195
|
+
return handleFieldOverlaps(left, right);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const leftSegs = toSegments(left);
|
|
199
|
+
const rightSegs = toSegments(right);
|
|
200
|
+
|
|
201
|
+
const leftPrefixOfRight = checkPrefixConditions(leftSegs, rightSegs);
|
|
202
|
+
const rightPrefixOfLeft = checkPrefixConditions(rightSegs, leftSegs);
|
|
203
|
+
|
|
204
|
+
const validConditions = [leftPrefixOfRight, rightPrefixOfLeft].filter(
|
|
205
|
+
(c): c is PrismaFilter => c !== null
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Hierarchy construction evaluates every segment, including a trailing segment that
|
|
209
|
+
// neither prefix comparison needs. Preserve its SQL UNKNOWN under outer negation too.
|
|
210
|
+
const fields = [...leftSegs, ...rightSegs]
|
|
211
|
+
.filter((segment): segment is FieldSegment => segment.type === "field")
|
|
212
|
+
.filter((segment) => segment.fieldRef.nullable !== false);
|
|
213
|
+
|
|
214
|
+
const pairs = fields.map(({ fieldRef }) => ({
|
|
215
|
+
equal: buildFieldFilter(fieldRef, "equals", ""),
|
|
216
|
+
unequal: buildFieldFilter(fieldRef, "not", ""),
|
|
217
|
+
}));
|
|
218
|
+
|
|
219
|
+
const presence = pairs.map(({ equal, unequal }) => ({
|
|
220
|
+
OR: [equal, unequal],
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
const unknown = pairs.map(({ equal, unequal }) => ({
|
|
224
|
+
AND: [equal, unequal],
|
|
225
|
+
}));
|
|
226
|
+
|
|
227
|
+
if (validConditions.length === 0) {
|
|
228
|
+
if (unknown.length > 0) return { OR: unknown };
|
|
229
|
+
|
|
230
|
+
const field = [...leftSegs, ...rightSegs].find(
|
|
231
|
+
(segment): segment is FieldSegment => segment.type === "field",
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
if (field) return buildImpossibleFilter(field.fieldRef);
|
|
235
|
+
throw new UnsupportedQueryPlanError("Cannot determine overlap: no field references found");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (
|
|
239
|
+
validConditions.some((condition) => Object.keys(condition).length === 0)
|
|
240
|
+
) {
|
|
241
|
+
return presence.length === 0 ? {} : { AND: presence };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Equal-length comparisons are identical in either direction. A contradiction is
|
|
245
|
+
// FALSE for a present field and UNKNOWN for NULL, restoring errors even if the
|
|
246
|
+
// prefix comparison is FALSE (FALSE AND NULL alone would lose that error).
|
|
247
|
+
const condition = validConditions[0]!;
|
|
248
|
+
|
|
249
|
+
return presence.length === 0
|
|
250
|
+
? condition
|
|
251
|
+
: {
|
|
252
|
+
OR: [{ AND: [condition, ...presence] }, ...unknown],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function handleFieldOverlaps(
|
|
257
|
+
left: ResolvedHierarchy,
|
|
258
|
+
right: ResolvedHierarchy
|
|
259
|
+
): PrismaFilter {
|
|
260
|
+
if (left.type === "field" && right.type === "field") {
|
|
261
|
+
throw new UnsupportedQueryPlanError("overlaps: cannot compare two field-reference hierarchies");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// The caller routes here only when at least one side is a field hierarchy.
|
|
265
|
+
const field = left.type === "field" ? left : right.type === "field" ? right : undefined;
|
|
266
|
+
|
|
267
|
+
if (field === undefined) {
|
|
268
|
+
throw new UnsupportedQueryPlanError("overlaps: neither side is a field-reference hierarchy");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const other = field === left ? right : left;
|
|
272
|
+
|
|
273
|
+
if (other.type !== "constant") {
|
|
274
|
+
throw new UnsupportedQueryPlanError("overlaps: segmented hierarchies with field hierarchies are not supported");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const delimiter = field.delimiter;
|
|
278
|
+
const otherRaw = other.segments.join(delimiter);
|
|
279
|
+
const strictPrefixes = getStrictPrefixes(other.segments, delimiter);
|
|
280
|
+
|
|
281
|
+
const conditions: PrismaFilter[] = [];
|
|
282
|
+
|
|
283
|
+
if (strictPrefixes.length > 0) {
|
|
284
|
+
conditions.push(buildFieldFilter(field.fieldRef, "in", strictPrefixes));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
conditions.push(buildFieldFilter(field.fieldRef, "equals", otherRaw));
|
|
288
|
+
conditions.push(buildPrefixFilter(field.fieldRef, otherRaw + delimiter));
|
|
289
|
+
|
|
290
|
+
return { OR: conditions };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function extractHierarchyOperands(
|
|
294
|
+
operatorName: string,
|
|
295
|
+
operands: PlanExpressionOperand[],
|
|
296
|
+
context: TranslationContext
|
|
297
|
+
): [ResolvedHierarchy, ResolvedHierarchy] {
|
|
298
|
+
if (operands.length !== 2) {
|
|
299
|
+
throw new UnsupportedQueryPlanError(`${operatorName} requires exactly two operands`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const leftOp = assertDefined(operands[0], `${operatorName} requires a left operand`);
|
|
303
|
+
const rightOp = assertDefined(operands[1], `${operatorName} requires a right operand`);
|
|
304
|
+
|
|
305
|
+
if (
|
|
306
|
+
!isOperatorOperand(leftOp) || leftOp.operator !== "hierarchy" ||
|
|
307
|
+
!isOperatorOperand(rightOp) || rightOp.operator !== "hierarchy"
|
|
308
|
+
) {
|
|
309
|
+
throw new UnsupportedQueryPlanError(`${operatorName} requires two hierarchy operands`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return [
|
|
313
|
+
normalizeHierarchy(resolveHierarchy(leftOp, context)),
|
|
314
|
+
normalizeHierarchy(resolveHierarchy(rightOp, context)),
|
|
315
|
+
];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function getStrictPrefixes(segments: string[], delimiter: string): string[] {
|
|
319
|
+
if (delimiter === "") {
|
|
320
|
+
// Per code point, the empty path is itself a (zero-segment) strict prefix.
|
|
321
|
+
return segments.map((_, length) => segments.slice(0, length).join(""));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (segments.length <= 1) return [];
|
|
325
|
+
const prefixes: string[] = [];
|
|
326
|
+
let current = segments[0]!;
|
|
327
|
+
prefixes.push(current);
|
|
328
|
+
|
|
329
|
+
for (let i = 1; i < segments.length - 1; i++) {
|
|
330
|
+
current = current + delimiter + segments[i]!;
|
|
331
|
+
prefixes.push(current);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return prefixes;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function handleAncestorDescendantOperator(
|
|
338
|
+
operands: PlanExpressionOperand[],
|
|
339
|
+
context: TranslationContext,
|
|
340
|
+
direction: "ancestor" | "descendant",
|
|
341
|
+
): PrismaFilter {
|
|
342
|
+
const operatorName = direction === "ancestor" ? "ancestorOf" : "descendentOf";
|
|
343
|
+
const [left, right] = extractHierarchyOperands(operatorName, operands, context);
|
|
344
|
+
|
|
345
|
+
// ancestorOf(A, B) = A is strict prefix of B
|
|
346
|
+
// descendentOf(A, B) = B is strict prefix of A
|
|
347
|
+
const ancestor = direction === "ancestor" ? left : right;
|
|
348
|
+
const descendant = direction === "ancestor" ? right : left;
|
|
349
|
+
|
|
350
|
+
if (ancestor.type === "field")
|
|
351
|
+
assertStringField(ancestor.fieldRef, operatorName);
|
|
352
|
+
|
|
353
|
+
if (descendant.type === "field")
|
|
354
|
+
assertStringField(descendant.fieldRef, operatorName);
|
|
355
|
+
|
|
356
|
+
if (ancestor.type === "constant" && descendant.type === "field") {
|
|
357
|
+
if (descendant.delimiter === "") {
|
|
358
|
+
// Per code point, a descendant is the ancestor followed by at least one more character.
|
|
359
|
+
return buildPrefixFilter(descendant.fieldRef, ancestor.segments.join(""), true);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return buildPrefixFilter(
|
|
363
|
+
descendant.fieldRef,
|
|
364
|
+
ancestor.segments.join(descendant.delimiter) + descendant.delimiter
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (ancestor.type === "field" && descendant.type === "constant") {
|
|
369
|
+
const delimiter = ancestor.delimiter;
|
|
370
|
+
const prefixes = getStrictPrefixes(descendant.segments, delimiter);
|
|
371
|
+
|
|
372
|
+
if (prefixes.length === 0) {
|
|
373
|
+
if (ancestor.fieldRef.nullable === false) return buildImpossibleFilter(ancestor.fieldRef);
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
AND: [
|
|
377
|
+
buildFieldFilter(ancestor.fieldRef, "equals", ""),
|
|
378
|
+
buildFieldFilter(ancestor.fieldRef, "not", ""),
|
|
379
|
+
],
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (prefixes.length === 1) {
|
|
384
|
+
return buildFieldFilter(ancestor.fieldRef, "equals", prefixes[0]!);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return buildFieldFilter(ancestor.fieldRef, "in", prefixes);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (ancestor.type === "constant" && descendant.type === "constant") {
|
|
391
|
+
const ancestorSegs = ancestor.segments;
|
|
392
|
+
const descendantSegs = descendant.segments;
|
|
393
|
+
|
|
394
|
+
if (
|
|
395
|
+
descendantSegs.length > ancestorSegs.length &&
|
|
396
|
+
ancestorSegs.every((seg, i) => seg === descendantSegs[i])
|
|
397
|
+
) {
|
|
398
|
+
return {};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
throw new UnsupportedQueryPlanError(`${operatorName}: constants do not satisfy ${direction} relationship`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
throw new UnsupportedQueryPlanError(`${operatorName}: unsupported hierarchy type combination`);
|
|
405
|
+
}
|