@cerbos/orm-prisma 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +611 -460
- package/lib/collections.d.ts +30 -0
- package/lib/collections.d.ts.map +1 -0
- package/lib/collections.js +421 -0
- package/lib/collections.js.map +1 -0
- package/lib/comparison.d.ts +24 -0
- package/lib/comparison.d.ts.map +1 -0
- package/lib/comparison.js +731 -0
- package/lib/comparison.js.map +1 -0
- package/lib/errors.d.ts +13 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +17 -0
- package/lib/errors.js.map +1 -0
- package/lib/evaluate.d.ts +5 -0
- package/lib/evaluate.d.ts.map +1 -0
- package/lib/evaluate.js +118 -0
- package/lib/evaluate.js.map +1 -0
- package/lib/fields.d.ts +34 -0
- package/lib/fields.d.ts.map +1 -0
- package/lib/fields.js +253 -0
- package/lib/fields.js.map +1 -0
- package/lib/functions.d.ts +5 -0
- package/lib/functions.d.ts.map +1 -0
- package/lib/functions.js +408 -0
- package/lib/functions.js.map +1 -0
- package/lib/hierarchy.d.ts +6 -0
- package/lib/hierarchy.d.ts.map +1 -0
- package/lib/hierarchy.js +266 -0
- package/lib/hierarchy.js.map +1 -0
- package/lib/index.d.ts +91 -8
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +34 -2301
- package/lib/index.js.map +1 -1
- package/lib/literals.d.ts +20 -0
- package/lib/literals.d.ts.map +1 -0
- package/lib/literals.js +290 -0
- package/lib/literals.js.map +1 -0
- package/lib/mapping.d.ts +95 -0
- package/lib/mapping.d.ts.map +1 -0
- package/lib/mapping.js +316 -0
- package/lib/mapping.js.map +1 -0
- package/lib/narrowing.d.ts +12 -0
- package/lib/narrowing.d.ts.map +1 -0
- package/lib/narrowing.js +32 -0
- package/lib/narrowing.js.map +1 -0
- package/lib/plan.d.ts +49 -0
- package/lib/plan.d.ts.map +1 -0
- package/lib/plan.js +86 -0
- package/lib/plan.js.map +1 -0
- package/lib/presence.d.ts +25 -0
- package/lib/presence.d.ts.map +1 -0
- package/lib/presence.js +82 -0
- package/lib/presence.js.map +1 -0
- package/lib/regex.d.ts +33 -0
- package/lib/regex.d.ts.map +1 -0
- package/lib/regex.js +480 -0
- package/lib/regex.js.map +1 -0
- package/lib/relations.d.ts +58 -0
- package/lib/relations.d.ts.map +1 -0
- package/lib/relations.js +175 -0
- package/lib/relations.js.map +1 -0
- package/lib/rewrite.d.ts +36 -0
- package/lib/rewrite.d.ts.map +1 -0
- package/lib/rewrite.js +366 -0
- package/lib/rewrite.js.map +1 -0
- package/lib/solve.d.ts +20 -0
- package/lib/solve.d.ts.map +1 -0
- package/lib/solve.js +77 -0
- package/lib/solve.js.map +1 -0
- package/lib/strings.d.ts +12 -0
- package/lib/strings.d.ts.map +1 -0
- package/lib/strings.js +110 -0
- package/lib/strings.js.map +1 -0
- package/lib/ternary.d.ts +12 -0
- package/lib/ternary.d.ts.map +1 -0
- package/lib/ternary.js +192 -0
- package/lib/ternary.js.map +1 -0
- package/lib/timestamp.d.ts +45 -0
- package/lib/timestamp.d.ts.map +1 -0
- package/lib/timestamp.js +225 -0
- package/lib/timestamp.js.map +1 -0
- package/lib/translate.d.ts +20 -0
- package/lib/translate.d.ts.map +1 -0
- package/lib/translate.js +287 -0
- package/lib/translate.js.map +1 -0
- package/lib/types.d.ts +29 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +980 -0
- package/lib/types.js.map +1 -0
- package/lib/value.d.ts +8 -0
- package/lib/value.d.ts.map +1 -0
- package/lib/value.js +7 -0
- package/lib/value.js.map +1 -0
- package/package.json +31 -18
- package/src/adversarial.test.ts +488 -488
- package/src/collections.ts +738 -0
- package/src/comparison.ts +1157 -0
- package/src/corpus.ts +597 -0
- package/src/errors.ts +12 -0
- package/src/evaluate.ts +170 -0
- package/src/fields.ts +373 -0
- package/src/functions.ts +548 -0
- package/src/generated/prisma-adversarial/browser.ts +20 -0
- package/src/generated/prisma-adversarial/client.ts +20 -0
- package/src/generated/prisma-adversarial/commonInputTypes.ts +116 -127
- package/src/generated/prisma-adversarial/internal/class.ts +44 -4
- package/src/generated/prisma-adversarial/internal/prismaNamespace.ts +350 -3
- package/src/generated/prisma-adversarial/internal/prismaNamespaceBrowser.ts +49 -2
- package/src/generated/prisma-adversarial/models/AdversarialBoolListElement.ts +1272 -0
- package/src/generated/prisma-adversarial/models/AdversarialInner.ts +1369 -0
- package/src/generated/prisma-adversarial/models/AdversarialNumberListElement.ts +1316 -0
- package/src/generated/prisma-adversarial/models/AdversarialParent.ts +1474 -0
- package/src/generated/prisma-adversarial/models/AdversarialResource.ts +547 -75
- package/src/generated/prisma-adversarial/models.ts +4 -0
- package/src/generated/prisma-adversarial-mysql/browser.ts +64 -0
- package/src/generated/{prisma → prisma-adversarial-mysql}/client.ts +23 -18
- package/src/generated/{prisma → prisma-adversarial-mysql}/commonInputTypes.ts +167 -92
- package/src/generated/prisma-adversarial-mysql/internal/class.ts +284 -0
- package/src/generated/prisma-adversarial-mysql/internal/prismaNamespace.ts +1518 -0
- package/src/generated/prisma-adversarial-mysql/internal/prismaNamespaceBrowser.ts +274 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialBoolListElement.ts +1157 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialCategory.ts +1286 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialInner.ts +1247 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialLabel.ts +1157 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialNumberListElement.ts +1201 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialParent.ts +1352 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialResource.ts +2021 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialSubCategory.ts +1286 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialTag.ts +1190 -0
- package/src/generated/prisma-adversarial-mysql/models.ts +20 -0
- package/src/generated/prisma-adversarial-mysql-v6/index.d.ts +13822 -0
- package/src/generated/prisma-adversarial-pg/browser.ts +64 -0
- package/src/generated/prisma-adversarial-pg/client.ts +86 -0
- package/src/generated/prisma-adversarial-pg/commonInputTypes.ts +345 -0
- package/src/generated/prisma-adversarial-pg/enums.ts +15 -0
- package/src/generated/prisma-adversarial-pg/internal/class.ts +284 -0
- package/src/generated/prisma-adversarial-pg/internal/prismaNamespace.ts +1542 -0
- package/src/generated/{prisma → prisma-adversarial-pg}/internal/prismaNamespaceBrowser.ts +72 -43
- package/src/generated/prisma-adversarial-pg/models/AdversarialBoolListElement.ts +1275 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialCategory.ts +1404 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialInner.ts +1371 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialLabel.ts +1275 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialNumberListElement.ts +1319 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialParent.ts +1476 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialResource.ts +2139 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialSubCategory.ts +1404 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialTag.ts +1310 -0
- package/src/generated/prisma-adversarial-pg/models.ts +20 -0
- package/src/generated/prisma-adversarial-pg-v6/client.d.ts +1 -0
- package/src/generated/prisma-adversarial-pg-v6/default.d.ts +1 -0
- package/src/generated/prisma-adversarial-pg-v6/edge.d.ts +1 -0
- package/src/generated/prisma-adversarial-pg-v6/index.d.ts +14918 -0
- package/src/generated/prisma-adversarial-pg-v6/runtime/index-browser.d.ts +370 -0
- package/src/generated/prisma-adversarial-pg-v6/runtime/library.d.ts +3982 -0
- package/src/generated/prisma-adversarial-pg-v6/wasm.d.ts +1 -0
- package/src/generated/prisma-adversarial-v6/index.d.ts +6515 -304
- package/src/hierarchy.ts +405 -0
- package/src/index.ts +134 -3325
- package/src/literals.ts +363 -0
- package/src/mapping.ts +474 -0
- package/src/narrowing.ts +33 -0
- package/src/plan.ts +125 -0
- package/src/presence.ts +114 -0
- package/src/regex.ts +596 -0
- package/src/relations.ts +226 -0
- package/src/rewrite.ts +524 -0
- package/src/solve.ts +96 -0
- package/src/strings.ts +152 -0
- package/src/ternary.ts +359 -0
- package/src/test-setup.adversarial-url.ts +21 -0
- package/src/test-setup.adversarial.mysql.v6.ts +13 -0
- package/src/test-setup.adversarial.mysql.v7.ts +12 -0
- package/src/test-setup.adversarial.pg.v6.ts +13 -0
- package/src/test-setup.adversarial.pg.v7.ts +9 -0
- package/src/test-setup.adversarial.v7.ts +1 -0
- package/src/timestamp.ts +308 -0
- package/src/translate.ts +419 -0
- package/src/translator.test.ts +766 -0
- package/src/types.ts +1310 -0
- package/src/value.ts +16 -0
- package/src/generated/prisma/browser.ts +0 -59
- package/src/generated/prisma/internal/class.ts +0 -274
- package/src/generated/prisma/internal/prismaNamespace.ts +0 -1403
- package/src/generated/prisma/models/Category.ts +0 -1412
- package/src/generated/prisma/models/Label.ts +0 -1276
- package/src/generated/prisma/models/NestedResource.ts +0 -1562
- package/src/generated/prisma/models/NextLevelNestedResource.ts +0 -1337
- package/src/generated/prisma/models/Resource.ts +0 -2196
- package/src/generated/prisma/models/SubCategory.ts +0 -1412
- package/src/generated/prisma/models/Tag.ts +0 -1276
- package/src/generated/prisma/models/User.ts +0 -1503
- package/src/generated/prisma/models.ts +0 -19
- package/src/generated/prisma-v6/index.d.ts +0 -13726
- package/src/index.test.ts +0 -8885
- package/src/test-setup.v6.ts +0 -5
- package/src/test-setup.v7.ts +0 -7
- /package/src/generated/{prisma → prisma-adversarial-mysql}/enums.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/client.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/default.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/edge.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/runtime/index-browser.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/runtime/library.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/wasm.d.ts +0 -0
package/src/timestamp.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// RFC 3339 timestamp literals, validated to CEL's instant range and Prisma's millisecond precision.
|
|
2
|
+
|
|
3
|
+
import type { PlanExpressionOperand } from "@cerbos/core";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
COMPARISON_OPERATORS,
|
|
7
|
+
isNamedOperand,
|
|
8
|
+
isOperatorOperand,
|
|
9
|
+
isValueOperand,
|
|
10
|
+
mirrorOperator,
|
|
11
|
+
} from "./plan";
|
|
12
|
+
import type { OperatorOperand } from "./plan";
|
|
13
|
+
import { UnsupportedQueryPlanError } from "./errors";
|
|
14
|
+
import { isString, pair } from "./narrowing";
|
|
15
|
+
|
|
16
|
+
const RFC3339_MILLISECOND_TIMESTAMP =
|
|
17
|
+
/^((?!0000)\d{4})-(\d{2})-(\d{2})[Tt](?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.(\d{1,9}))?(?:[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
|
|
18
|
+
|
|
19
|
+
const MIN_RFC3339_TIMESTAMP_MILLISECONDS = Date.parse(
|
|
20
|
+
"0001-01-01T00:00:00.000Z"
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const MAX_RFC3339_TIMESTAMP_MILLISECONDS = Date.parse(
|
|
24
|
+
"9999-12-31T23:59:59.999Z"
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parses an RFC 3339 literal into the ISO string Prisma binds. Anything a lenient `Date` parse
|
|
29
|
+
* would coerce into some other instant — a missing time part, a day that does not exist, digits
|
|
30
|
+
* past the millisecond, an instant outside CEL's range — is refused instead.
|
|
31
|
+
*/
|
|
32
|
+
export function normalizeRfc3339Milliseconds(value: string): string {
|
|
33
|
+
const { iso, subMillisecond } = parseRfc3339Instant(value);
|
|
34
|
+
|
|
35
|
+
if (subMillisecond) {
|
|
36
|
+
throw new UnsupportedQueryPlanError(
|
|
37
|
+
`Timestamp value exceeds millisecond precision: ${value}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return iso;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type ParsedInstant = {
|
|
45
|
+
iso: string;
|
|
46
|
+
subMillisecond: boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parses an RFC 3339 literal like normalizeRfc3339Milliseconds, except that an instant between
|
|
51
|
+
* two milliseconds is accepted: `iso` is then the NEXT millisecond, and `subMillisecond` says so.
|
|
52
|
+
* A comparison against it has to be rewritten for that rounding (see roundSubMillisecond).
|
|
53
|
+
*/
|
|
54
|
+
export function parseRfc3339Instant(value: string): ParsedInstant {
|
|
55
|
+
const match = RFC3339_MILLISECOND_TIMESTAMP.exec(value);
|
|
56
|
+
const yearText = match?.[1];
|
|
57
|
+
const monthText = match?.[2];
|
|
58
|
+
const dayText = match?.[3];
|
|
59
|
+
const fraction = match?.[4] ?? "";
|
|
60
|
+
|
|
61
|
+
if (!yearText || !monthText || !dayText) {
|
|
62
|
+
throw new UnsupportedQueryPlanError(`Invalid RFC 3339 timestamp value: ${value}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const subMillisecond = [...fraction.slice(3)].some((digit) => digit !== "0");
|
|
66
|
+
|
|
67
|
+
const year = Number(yearText);
|
|
68
|
+
const month = Number(monthText);
|
|
69
|
+
const day = Number(dayText);
|
|
70
|
+
const calendarDate = new Date(0);
|
|
71
|
+
calendarDate.setUTCHours(0, 0, 0, 0);
|
|
72
|
+
calendarDate.setUTCFullYear(year, month - 1, day);
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
calendarDate.getUTCFullYear() !== year ||
|
|
76
|
+
calendarDate.getUTCMonth() !== month - 1 ||
|
|
77
|
+
calendarDate.getUTCDate() !== day
|
|
78
|
+
) {
|
|
79
|
+
throw new UnsupportedQueryPlanError(`Invalid RFC 3339 timestamp value: ${value}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const milliseconds = Date.parse(value);
|
|
83
|
+
|
|
84
|
+
if (Number.isNaN(milliseconds)) {
|
|
85
|
+
throw new UnsupportedQueryPlanError(`Invalid RFC 3339 timestamp value: ${value}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
milliseconds < MIN_RFC3339_TIMESTAMP_MILLISECONDS ||
|
|
90
|
+
milliseconds > MAX_RFC3339_TIMESTAMP_MILLISECONDS
|
|
91
|
+
) {
|
|
92
|
+
throw new UnsupportedQueryPlanError(
|
|
93
|
+
`Timestamp value is outside CEL's supported instant range: ${value}`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Date.parse drops the digits past the millisecond, so a sub-millisecond instant lies strictly
|
|
98
|
+
// between `milliseconds` and the next one.
|
|
99
|
+
const rounded = subMillisecond ? milliseconds + 1 : milliseconds;
|
|
100
|
+
|
|
101
|
+
if (rounded > MAX_RFC3339_TIMESTAMP_MILLISECONDS) {
|
|
102
|
+
throw new UnsupportedQueryPlanError(
|
|
103
|
+
`Timestamp value is outside CEL's supported instant range: ${value}`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { iso: new Date(rounded).toISOString(), subMillisecond };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A Prisma `DateTime` carries milliseconds, so the attribute an application builds from one is a
|
|
112
|
+
* whole millisecond `a`, truncated from whatever the column stores. Against an instant `T` strictly
|
|
113
|
+
* between two milliseconds, with `C` the millisecond after it: `a < T` and `a <= T` both mean
|
|
114
|
+
* `column < C`, `a > T` and `a >= T` both mean `column >= C`, and `a == T` never holds. Returns the
|
|
115
|
+
* operator to compare against `C`, or null for the equality family.
|
|
116
|
+
*/
|
|
117
|
+
export function roundSubMillisecond(operator: string): string | null {
|
|
118
|
+
switch (operator) {
|
|
119
|
+
case "lt":
|
|
120
|
+
case "le":
|
|
121
|
+
return "lt";
|
|
122
|
+
case "gt":
|
|
123
|
+
case "ge":
|
|
124
|
+
return "ge";
|
|
125
|
+
default:
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// -- timestamp arithmetic, solved for the column -------------------------------------------------
|
|
131
|
+
|
|
132
|
+
const MIN_INSTANT = MIN_RFC3339_TIMESTAMP_MILLISECONDS;
|
|
133
|
+
const MAX_INSTANT = MAX_RFC3339_TIMESTAMP_MILLISECONDS;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A comparison over timestamp arithmetic, rewritten as a comparison of the bare `timestamp(x)`
|
|
137
|
+
* against a literal instant, or undefined for any other shape:
|
|
138
|
+
*
|
|
139
|
+
* - `timeSince(timestamp(x)) CMP duration(d)`: timeSince is `now - x`, read from the clock when
|
|
140
|
+
* the query runs, so it holds exactly when `x` lies on the mirrored side of `now - d`. Cerbos
|
|
141
|
+
* computes it with Go's saturating `time.Sub`, which raises no error.
|
|
142
|
+
* - `timestamp(x) ± duration(d) CMP timestamp(t)`: the shift is monotone, so it holds exactly when
|
|
143
|
+
* `x CMP t ∓ d`. CEL raises an error when the shifted instant leaves 0001-01-01..9999-12-31,
|
|
144
|
+
* which the comparison alone cannot carry: `positive` says whether the leaf sits under an even
|
|
145
|
+
* number of negations, and the rows that overflow are then excluded from (positive) or included
|
|
146
|
+
* in (negative) the leaf, so the enclosing negations never select them.
|
|
147
|
+
*
|
|
148
|
+
* Both sides may come in either order. A duration or instant that is not a whole millisecond, or a
|
|
149
|
+
* bound outside CEL's range, is left alone for the translator to refuse.
|
|
150
|
+
*/
|
|
151
|
+
export function rewriteTemporalComparison(
|
|
152
|
+
expr: OperatorOperand,
|
|
153
|
+
positive: boolean,
|
|
154
|
+
now: number = Date.now()
|
|
155
|
+
): PlanExpressionOperand | undefined {
|
|
156
|
+
if (!COMPARISON_OPERATORS.has(expr.operator) || expr.operands.length !== 2) return undefined;
|
|
157
|
+
let [left, right] = pair(expr.operands);
|
|
158
|
+
let operator = expr.operator;
|
|
159
|
+
|
|
160
|
+
if (!isTemporalArithmetic(left) && isTemporalArithmetic(right)) {
|
|
161
|
+
[left, right] = [right, left];
|
|
162
|
+
operator = mirrorOperator(operator);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!isOperatorOperand(left)) return undefined;
|
|
166
|
+
|
|
167
|
+
if (left.operator === "timeSince") {
|
|
168
|
+
const [column] = left.operands;
|
|
169
|
+
const duration = durationMilliseconds(right);
|
|
170
|
+
|
|
171
|
+
if (left.operands.length !== 1 || !isTimestampColumn(column) || duration === undefined) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const bound = instantLiteral(now - duration);
|
|
176
|
+
|
|
177
|
+
// now - x CMP d <=> x mirror(CMP) now - d
|
|
178
|
+
return bound && compareInstant(mirrorOperator(operator), column, bound);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if ((left.operator !== "add" && left.operator !== "sub") || left.operands.length !== 2) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const [a, b] = pair(left.operands);
|
|
186
|
+
let column: OperatorOperand;
|
|
187
|
+
let duration: number | undefined;
|
|
188
|
+
|
|
189
|
+
if (isTimestampColumn(a) && durationMilliseconds(b) !== undefined) {
|
|
190
|
+
[column, duration] = [a, durationMilliseconds(b)];
|
|
191
|
+
} else if (left.operator === "add" && isTimestampColumn(b)) {
|
|
192
|
+
[column, duration] = [b, durationMilliseconds(a)];
|
|
193
|
+
} else {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const instant = instantMilliseconds(right);
|
|
198
|
+
|
|
199
|
+
if (duration === undefined || instant === undefined) return undefined;
|
|
200
|
+
const shift = left.operator === "add" ? duration : -duration;
|
|
201
|
+
const bound = instantLiteral(instant - shift);
|
|
202
|
+
|
|
203
|
+
if (bound === undefined) return undefined;
|
|
204
|
+
const solved = compareInstant(operator, column, bound);
|
|
205
|
+
|
|
206
|
+
if (shift === 0) return solved;
|
|
207
|
+
const limit = instantLiteral(shift > 0 ? MAX_INSTANT - shift : MIN_INSTANT - shift);
|
|
208
|
+
|
|
209
|
+
if (limit === undefined) return undefined;
|
|
210
|
+
const inRange = compareInstant(shift > 0 ? "le" : "ge", column, limit);
|
|
211
|
+
|
|
212
|
+
return positive
|
|
213
|
+
? { operator: "and", operands: [solved, inRange] }
|
|
214
|
+
: { operator: "or", operands: [solved, { operator: "not", operands: [inRange] }] };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isTemporalArithmetic(operand: PlanExpressionOperand): boolean {
|
|
218
|
+
return (
|
|
219
|
+
isOperatorOperand(operand) &&
|
|
220
|
+
(operand.operator === "timeSince" ||
|
|
221
|
+
((operand.operator === "add" || operand.operator === "sub") &&
|
|
222
|
+
operand.operands.some(isTimestampColumn)))
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** `timestamp(column)`. */
|
|
227
|
+
function isTimestampColumn(
|
|
228
|
+
operand: PlanExpressionOperand | undefined
|
|
229
|
+
): operand is OperatorOperand {
|
|
230
|
+
return (
|
|
231
|
+
operand !== undefined &&
|
|
232
|
+
isOperatorOperand(operand) &&
|
|
233
|
+
operand.operator === "timestamp" &&
|
|
234
|
+
operand.operands.length === 1 &&
|
|
235
|
+
isNamedOperand(operand.operands[0]!)
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const PROTOBUF_DURATION = /^(-)?(\d+)(?:\.(\d{1,9}))?s$/;
|
|
240
|
+
|
|
241
|
+
/** A `duration("...")` literal in the planner's protobuf spelling, in whole milliseconds. */
|
|
242
|
+
function durationMilliseconds(operand: PlanExpressionOperand): number | undefined {
|
|
243
|
+
if (
|
|
244
|
+
!isOperatorOperand(operand) ||
|
|
245
|
+
operand.operator !== "duration" ||
|
|
246
|
+
operand.operands.length !== 1
|
|
247
|
+
) {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const [literal] = operand.operands;
|
|
252
|
+
|
|
253
|
+
if (!isValueOperand(literal!) || !isString(literal.value)) return undefined;
|
|
254
|
+
const match = PROTOBUF_DURATION.exec(literal.value);
|
|
255
|
+
|
|
256
|
+
if (!match) return undefined;
|
|
257
|
+
const fraction = (match[3] ?? "").padEnd(9, "0");
|
|
258
|
+
|
|
259
|
+
if (fraction.slice(3) !== "000000") return undefined;
|
|
260
|
+
const milliseconds = Number(match[2]) * 1000 + Number(fraction.slice(0, 3));
|
|
261
|
+
|
|
262
|
+
if (!Number.isSafeInteger(milliseconds)) return undefined;
|
|
263
|
+
|
|
264
|
+
return match[1] ? -milliseconds : milliseconds;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** A `timestamp("...")` literal at a whole millisecond. */
|
|
268
|
+
function instantMilliseconds(operand: PlanExpressionOperand): number | undefined {
|
|
269
|
+
if (
|
|
270
|
+
!isOperatorOperand(operand) ||
|
|
271
|
+
operand.operator !== "timestamp" ||
|
|
272
|
+
operand.operands.length !== 1
|
|
273
|
+
) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const [literal] = operand.operands;
|
|
278
|
+
|
|
279
|
+
if (!isValueOperand(literal!) || !isString(literal.value)) return undefined;
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const { iso, subMillisecond } = parseRfc3339Instant(literal.value);
|
|
283
|
+
|
|
284
|
+
return subMillisecond ? undefined : Date.parse(iso);
|
|
285
|
+
} catch {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function instantLiteral(milliseconds: number): PlanExpressionOperand | undefined {
|
|
291
|
+
if (
|
|
292
|
+
!Number.isFinite(milliseconds) ||
|
|
293
|
+
milliseconds < MIN_INSTANT ||
|
|
294
|
+
milliseconds > MAX_INSTANT
|
|
295
|
+
) {
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return { operator: "timestamp", operands: [{ value: new Date(milliseconds).toISOString() }] };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function compareInstant(
|
|
303
|
+
operator: string,
|
|
304
|
+
column: PlanExpressionOperand,
|
|
305
|
+
instant: PlanExpressionOperand
|
|
306
|
+
): PlanExpressionOperand {
|
|
307
|
+
return { operator, operands: [column, instant] };
|
|
308
|
+
}
|