@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.
Files changed (203) hide show
  1. package/README.md +611 -460
  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 +91 -8
  31. package/lib/index.d.ts.map +1 -1
  32. package/lib/index.js +34 -2301
  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 +31 -18
  95. package/src/adversarial.test.ts +488 -488
  96. package/src/collections.ts +738 -0
  97. package/src/comparison.ts +1157 -0
  98. package/src/corpus.ts +597 -0
  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 +20 -0
  104. package/src/generated/prisma-adversarial/client.ts +20 -0
  105. package/src/generated/prisma-adversarial/commonInputTypes.ts +116 -127
  106. package/src/generated/prisma-adversarial/internal/class.ts +44 -4
  107. package/src/generated/prisma-adversarial/internal/prismaNamespace.ts +350 -3
  108. package/src/generated/prisma-adversarial/internal/prismaNamespaceBrowser.ts +49 -2
  109. package/src/generated/prisma-adversarial/models/AdversarialBoolListElement.ts +1272 -0
  110. package/src/generated/prisma-adversarial/models/AdversarialInner.ts +1369 -0
  111. package/src/generated/prisma-adversarial/models/AdversarialNumberListElement.ts +1316 -0
  112. package/src/generated/prisma-adversarial/models/AdversarialParent.ts +1474 -0
  113. package/src/generated/prisma-adversarial/models/AdversarialResource.ts +547 -75
  114. package/src/generated/prisma-adversarial/models.ts +4 -0
  115. package/src/generated/prisma-adversarial-mysql/browser.ts +64 -0
  116. package/src/generated/{prisma → prisma-adversarial-mysql}/client.ts +23 -18
  117. package/src/generated/{prisma → prisma-adversarial-mysql}/commonInputTypes.ts +167 -92
  118. package/src/generated/prisma-adversarial-mysql/internal/class.ts +284 -0
  119. package/src/generated/prisma-adversarial-mysql/internal/prismaNamespace.ts +1518 -0
  120. package/src/generated/prisma-adversarial-mysql/internal/prismaNamespaceBrowser.ts +274 -0
  121. package/src/generated/prisma-adversarial-mysql/models/AdversarialBoolListElement.ts +1157 -0
  122. package/src/generated/prisma-adversarial-mysql/models/AdversarialCategory.ts +1286 -0
  123. package/src/generated/prisma-adversarial-mysql/models/AdversarialInner.ts +1247 -0
  124. package/src/generated/prisma-adversarial-mysql/models/AdversarialLabel.ts +1157 -0
  125. package/src/generated/prisma-adversarial-mysql/models/AdversarialNumberListElement.ts +1201 -0
  126. package/src/generated/prisma-adversarial-mysql/models/AdversarialParent.ts +1352 -0
  127. package/src/generated/prisma-adversarial-mysql/models/AdversarialResource.ts +2021 -0
  128. package/src/generated/prisma-adversarial-mysql/models/AdversarialSubCategory.ts +1286 -0
  129. package/src/generated/prisma-adversarial-mysql/models/AdversarialTag.ts +1190 -0
  130. package/src/generated/prisma-adversarial-mysql/models.ts +20 -0
  131. package/src/generated/prisma-adversarial-mysql-v6/index.d.ts +13822 -0
  132. package/src/generated/prisma-adversarial-pg/browser.ts +64 -0
  133. package/src/generated/prisma-adversarial-pg/client.ts +86 -0
  134. package/src/generated/prisma-adversarial-pg/commonInputTypes.ts +345 -0
  135. package/src/generated/prisma-adversarial-pg/enums.ts +15 -0
  136. package/src/generated/prisma-adversarial-pg/internal/class.ts +284 -0
  137. package/src/generated/prisma-adversarial-pg/internal/prismaNamespace.ts +1542 -0
  138. package/src/generated/{prisma → prisma-adversarial-pg}/internal/prismaNamespaceBrowser.ts +72 -43
  139. package/src/generated/prisma-adversarial-pg/models/AdversarialBoolListElement.ts +1275 -0
  140. package/src/generated/prisma-adversarial-pg/models/AdversarialCategory.ts +1404 -0
  141. package/src/generated/prisma-adversarial-pg/models/AdversarialInner.ts +1371 -0
  142. package/src/generated/prisma-adversarial-pg/models/AdversarialLabel.ts +1275 -0
  143. package/src/generated/prisma-adversarial-pg/models/AdversarialNumberListElement.ts +1319 -0
  144. package/src/generated/prisma-adversarial-pg/models/AdversarialParent.ts +1476 -0
  145. package/src/generated/prisma-adversarial-pg/models/AdversarialResource.ts +2139 -0
  146. package/src/generated/prisma-adversarial-pg/models/AdversarialSubCategory.ts +1404 -0
  147. package/src/generated/prisma-adversarial-pg/models/AdversarialTag.ts +1310 -0
  148. package/src/generated/prisma-adversarial-pg/models.ts +20 -0
  149. package/src/generated/prisma-adversarial-pg-v6/client.d.ts +1 -0
  150. package/src/generated/prisma-adversarial-pg-v6/default.d.ts +1 -0
  151. package/src/generated/prisma-adversarial-pg-v6/edge.d.ts +1 -0
  152. package/src/generated/prisma-adversarial-pg-v6/index.d.ts +14918 -0
  153. package/src/generated/prisma-adversarial-pg-v6/runtime/index-browser.d.ts +370 -0
  154. package/src/generated/prisma-adversarial-pg-v6/runtime/library.d.ts +3982 -0
  155. package/src/generated/prisma-adversarial-pg-v6/wasm.d.ts +1 -0
  156. package/src/generated/prisma-adversarial-v6/index.d.ts +6515 -304
  157. package/src/hierarchy.ts +405 -0
  158. package/src/index.ts +134 -3325
  159. package/src/literals.ts +363 -0
  160. package/src/mapping.ts +474 -0
  161. package/src/narrowing.ts +33 -0
  162. package/src/plan.ts +125 -0
  163. package/src/presence.ts +114 -0
  164. package/src/regex.ts +596 -0
  165. package/src/relations.ts +226 -0
  166. package/src/rewrite.ts +524 -0
  167. package/src/solve.ts +96 -0
  168. package/src/strings.ts +152 -0
  169. package/src/ternary.ts +359 -0
  170. package/src/test-setup.adversarial-url.ts +21 -0
  171. package/src/test-setup.adversarial.mysql.v6.ts +13 -0
  172. package/src/test-setup.adversarial.mysql.v7.ts +12 -0
  173. package/src/test-setup.adversarial.pg.v6.ts +13 -0
  174. package/src/test-setup.adversarial.pg.v7.ts +9 -0
  175. package/src/test-setup.adversarial.v7.ts +1 -0
  176. package/src/timestamp.ts +308 -0
  177. package/src/translate.ts +419 -0
  178. package/src/translator.test.ts +766 -0
  179. package/src/types.ts +1310 -0
  180. package/src/value.ts +16 -0
  181. package/src/generated/prisma/browser.ts +0 -59
  182. package/src/generated/prisma/internal/class.ts +0 -274
  183. package/src/generated/prisma/internal/prismaNamespace.ts +0 -1403
  184. package/src/generated/prisma/models/Category.ts +0 -1412
  185. package/src/generated/prisma/models/Label.ts +0 -1276
  186. package/src/generated/prisma/models/NestedResource.ts +0 -1562
  187. package/src/generated/prisma/models/NextLevelNestedResource.ts +0 -1337
  188. package/src/generated/prisma/models/Resource.ts +0 -2196
  189. package/src/generated/prisma/models/SubCategory.ts +0 -1412
  190. package/src/generated/prisma/models/Tag.ts +0 -1276
  191. package/src/generated/prisma/models/User.ts +0 -1503
  192. package/src/generated/prisma/models.ts +0 -19
  193. package/src/generated/prisma-v6/index.d.ts +0 -13726
  194. package/src/index.test.ts +0 -8885
  195. package/src/test-setup.v6.ts +0 -5
  196. package/src/test-setup.v7.ts +0 -7
  197. /package/src/generated/{prisma → prisma-adversarial-mysql}/enums.ts +0 -0
  198. /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/client.d.ts +0 -0
  199. /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/default.d.ts +0 -0
  200. /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/edge.d.ts +0 -0
  201. /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/runtime/index-browser.d.ts +0 -0
  202. /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/runtime/library.d.ts +0 -0
  203. /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/wasm.d.ts +0 -0
@@ -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
+ }