effect-dynamodb 1.36.0 → 1.37.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 (94) hide show
  1. package/dist/Aggregate.d.ts +29 -17
  2. package/dist/Aggregate.d.ts.map +1 -1
  3. package/dist/Aggregate.js +795 -173
  4. package/dist/Aggregate.js.map +1 -1
  5. package/dist/Batch.d.ts.map +1 -1
  6. package/dist/Batch.js +196 -77
  7. package/dist/Batch.js.map +1 -1
  8. package/dist/Collection.d.ts.map +1 -1
  9. package/dist/Collection.js +11 -2
  10. package/dist/Collection.js.map +1 -1
  11. package/dist/Collections.d.ts +15 -6
  12. package/dist/Collections.d.ts.map +1 -1
  13. package/dist/Collections.js +2 -1
  14. package/dist/Collections.js.map +1 -1
  15. package/dist/DynamoClient.d.ts +119 -10
  16. package/dist/DynamoClient.d.ts.map +1 -1
  17. package/dist/DynamoClient.js +1148 -142
  18. package/dist/DynamoClient.js.map +1 -1
  19. package/dist/Entity.d.ts +126 -51
  20. package/dist/Entity.d.ts.map +1 -1
  21. package/dist/Entity.js +3477 -539
  22. package/dist/Entity.js.map +1 -1
  23. package/dist/EventStore.d.ts +4 -3
  24. package/dist/EventStore.d.ts.map +1 -1
  25. package/dist/EventStore.js +616 -53
  26. package/dist/EventStore.js.map +1 -1
  27. package/dist/Query.d.ts +311 -25
  28. package/dist/Query.d.ts.map +1 -1
  29. package/dist/Query.js +1159 -103
  30. package/dist/Query.js.map +1 -1
  31. package/dist/Table.d.ts +172 -3
  32. package/dist/Table.d.ts.map +1 -1
  33. package/dist/Table.js +85 -4
  34. package/dist/Table.js.map +1 -1
  35. package/dist/Transaction.d.ts +18 -2
  36. package/dist/Transaction.d.ts.map +1 -1
  37. package/dist/Transaction.js +62 -8
  38. package/dist/Transaction.js.map +1 -1
  39. package/dist/change-feed/DynamoStreamsClient.d.ts +1 -1
  40. package/dist/change-feed/DynamoStreamsClient.d.ts.map +1 -1
  41. package/dist/change-feed/DynamoStreamsClient.js +3 -1
  42. package/dist/change-feed/DynamoStreamsClient.js.map +1 -1
  43. package/dist/index.d.ts +6 -3
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +1 -1
  46. package/dist/index.js.map +1 -1
  47. package/dist/internal/BoundQuery.d.ts +226 -32
  48. package/dist/internal/BoundQuery.d.ts.map +1 -1
  49. package/dist/internal/BoundQuery.js +96 -18
  50. package/dist/internal/BoundQuery.js.map +1 -1
  51. package/dist/internal/Capacity.d.ts +8 -0
  52. package/dist/internal/Capacity.d.ts.map +1 -1
  53. package/dist/internal/Capacity.js +24 -4
  54. package/dist/internal/Capacity.js.map +1 -1
  55. package/dist/internal/CredentialRefusal.d.ts +32 -0
  56. package/dist/internal/CredentialRefusal.d.ts.map +1 -0
  57. package/dist/internal/CredentialRefusal.js +80 -0
  58. package/dist/internal/CredentialRefusal.js.map +1 -0
  59. package/dist/internal/Cursor.d.ts +90 -0
  60. package/dist/internal/Cursor.d.ts.map +1 -0
  61. package/dist/internal/Cursor.js +179 -0
  62. package/dist/internal/Cursor.js.map +1 -0
  63. package/dist/internal/ExpressionLimits.d.ts +18 -2
  64. package/dist/internal/ExpressionLimits.d.ts.map +1 -1
  65. package/dist/internal/ExpressionLimits.js +35 -2
  66. package/dist/internal/ExpressionLimits.js.map +1 -1
  67. package/dist/internal/ProjectionEngine.d.ts +16 -0
  68. package/dist/internal/ProjectionEngine.d.ts.map +1 -1
  69. package/dist/internal/ProjectionEngine.js +242 -46
  70. package/dist/internal/ProjectionEngine.js.map +1 -1
  71. package/dist/internal/RangePlanner.d.ts +394 -0
  72. package/dist/internal/RangePlanner.d.ts.map +1 -0
  73. package/dist/internal/RangePlanner.js +1296 -0
  74. package/dist/internal/RangePlanner.js.map +1 -0
  75. package/dist/internal/Rekey.d.ts +78 -0
  76. package/dist/internal/Rekey.d.ts.map +1 -0
  77. package/dist/internal/Rekey.js +323 -0
  78. package/dist/internal/Rekey.js.map +1 -0
  79. package/dist/internal/TransactPlan.d.ts +8 -1
  80. package/dist/internal/TransactPlan.d.ts.map +1 -1
  81. package/dist/internal/TransactPlan.js +9 -2
  82. package/dist/internal/TransactPlan.js.map +1 -1
  83. package/dist/internal/TransactWriteOps.d.ts +10 -7
  84. package/dist/internal/TransactWriteOps.d.ts.map +1 -1
  85. package/dist/internal/TransactWriteOps.js +82 -6
  86. package/dist/internal/TransactWriteOps.js.map +1 -1
  87. package/dist/internal/TransactableOps.d.ts +31 -0
  88. package/dist/internal/TransactableOps.d.ts.map +1 -1
  89. package/dist/internal/TransactableOps.js +50 -1
  90. package/dist/internal/TransactableOps.js.map +1 -1
  91. package/dist/internal/WriteDiagnosis.d.ts.map +1 -1
  92. package/dist/internal/WriteDiagnosis.js +10 -7
  93. package/dist/internal/WriteDiagnosis.js.map +1 -1
  94. package/package.json +2 -2
@@ -0,0 +1,1296 @@
1
+ /**
2
+ * @internal RangePlanner — the pure half of a planned query (#171, design §9).
3
+ *
4
+ * A planner never sends a request. It turns a plan and each response into the
5
+ * next *step* — a `Read` of one key range, a `Probe` that locates where to read
6
+ * next, or `Done` — and the terminal's loop (`Query.ts`) sends every request,
7
+ * so every request counts against `maxPages`, is observed by `onRequest` and
8
+ * is never sent once the call is about to return.
9
+ *
10
+ * Plans over a composite sort key (§7):
11
+ *
12
+ * - `key` — the bounds reach the key condition directly: one range.
13
+ * - `skip-scan` — composites left of the first bound have no bound (the
14
+ * *skip tuple*): a probe (`Limit: 1`) finds each distinct tuple `T`, then
15
+ * `T`'s own range is read, then a probe from just past `T`, and so on.
16
+ * - `filter` — one range over everything under the pinned prefix, every bound
17
+ * checked per row: when skip-scan is not sound (§7.3), after the probe budget
18
+ * is spent (§7.4) or at a key the tuple parse can't read.
19
+ *
20
+ * Plans over a Z-order sort key (§5): inside each prefix tuple, `naive` reads
21
+ * the box's hull, `jump` follows the last examined key to the next in-box
22
+ * address, `precise` reads a capped range plan; a missing prefix composite is
23
+ * walked by a binary skip-scan (§7.5).
24
+ *
25
+ * Positions are what a cursor records (§10): what has been **completed**, never
26
+ * a range to trust. Resume recomputes the plan and places the position in it.
27
+ */
28
+ import { KEY_DELIMITER } from "@effect-dynamodb/schema/DynamoSchema.js";
29
+ import { ValidationError } from "@effect-dynamodb/schema/Errors.js";
30
+ import { bigmin, concatBytes, corners, fromBytes, inBox, litmax, nextInBox, prevInBox, REFINE_FACTOR, succ, toBytes, } from "@effect-dynamodb/schema/internal/ZOrderCurve.js";
31
+ import * as Cursor from "./Cursor.js";
32
+ import { compileExpr } from "./Expr.js";
33
+ /** Default skip-scan probe budget per terminal call (§7.4). */
34
+ export const DEFAULT_MAX_SKIPS = 32;
35
+ /** Default range cap of a Z-order `precise` plan and of `auto`'s choice (§3.4). */
36
+ export const DEFAULT_MAX_RANGES = 16;
37
+ /** A jump run's first `Limit`, and what it resets to after a jump (§8.5). */
38
+ export const JUMP_INITIAL = 16;
39
+ /** The largest `Limit` a jump run grows to (§8.5). */
40
+ export const JUMP_MAX_LIMIT = 1_000;
41
+ // ---------------------------------------------------------------------------
42
+ // Key values: order, display, conditions
43
+ // ---------------------------------------------------------------------------
44
+ /**
45
+ * `SK_MAX_SENTINEL` (`DynamoClient.ts`): above every character key
46
+ * composition emits, so `prefix + MAX` bounds every key under `prefix`.
47
+ */
48
+ export const KEY_MAX = "￿";
49
+ const encoder = new TextEncoder();
50
+ const compareBytes = (a, b) => {
51
+ const n = Math.min(a.length, b.length);
52
+ for (let i = 0; i < n; i++) {
53
+ if (a[i] !== b[i])
54
+ return a[i] < b[i] ? -1 : 1;
55
+ }
56
+ return a.length === b.length ? 0 : a.length < b.length ? -1 : 1;
57
+ };
58
+ /**
59
+ * Order two key values as DynamoDB does: by UTF-8 bytes. For text that is
60
+ * code point order, which JS string comparison (UTF-16 code units) is not
61
+ * above U+FFFF — so it is never used for keys.
62
+ */
63
+ export const compareUtf8 = (a, b) => {
64
+ if (typeof a !== "string" || typeof b !== "string") {
65
+ return compareBytes(typeof a === "string" ? encoder.encode(a) : a, typeof b === "string" ? encoder.encode(b) : b);
66
+ }
67
+ let i = 0;
68
+ let j = 0;
69
+ while (i < a.length && j < b.length) {
70
+ const x = a.codePointAt(i);
71
+ const y = b.codePointAt(j);
72
+ if (x !== y)
73
+ return x < y ? -1 : 1;
74
+ i += x > 0xffff ? 2 : 1;
75
+ j += y > 0xffff ? 2 : 1;
76
+ }
77
+ return i < a.length ? 1 : j < b.length ? -1 : 0;
78
+ };
79
+ /**
80
+ * The highest code point: above every character a key can hold but itself.
81
+ * Where the text after a prefix is a value's own (no label precedes it),
82
+ * `prefix + KEY_TOP` bounds every key under the prefix; {@link KEY_MAX} does
83
+ * not, since an emoji (UTF-8 `F0 …`) outsorts it.
84
+ */
85
+ export const KEY_TOP = String.fromCodePoint(0x10ffff);
86
+ /**
87
+ * An inclusive high end for a strict `<` on key text: below `key`, and at or
88
+ * above every key below `key` that does not hold U+10FFFF. Its last code
89
+ * point is stepped down one (past the surrogates) and followed by U+10FFFF: a
90
+ * key below `key` either stops short of it — a prefix, which the bound keeps —
91
+ * or is smaller at some character, and from there sorts under the bound. A
92
+ * last code point of U+0000 has no predecessor; `key` without it is then the
93
+ * bound. (U+10FFFF rather than {@link KEY_MAX}, which an emoji outsorts.)
94
+ */
95
+ export const keyJustBelow = (key) => {
96
+ const width = key.length > 1 && key.codePointAt(key.length - 2) > 0xffff ? 2 : 1;
97
+ const head = key.slice(0, key.length - width);
98
+ const last = key.codePointAt(key.length - width);
99
+ if (last === undefined || last === 0)
100
+ return head;
101
+ const previous = last - 1 >= 0xd800 && last - 1 <= 0xdfff ? 0xd7ff : last - 1;
102
+ return `${head}${String.fromCodePoint(previous)}${KEY_TOP}`;
103
+ };
104
+ const startsWithKey = (value, prefix) => {
105
+ if (typeof value === "string" && typeof prefix === "string")
106
+ return value.startsWith(prefix);
107
+ const v = typeof value === "string" ? encoder.encode(value) : value;
108
+ const p = typeof prefix === "string" ? encoder.encode(prefix) : prefix;
109
+ return p.length <= v.length && compareBytes(v.subarray(0, p.length), p) === 0;
110
+ };
111
+ /** Does `value` satisfy a sort key condition, as DynamoDB evaluates it? */
112
+ export const keyConditionHolds = (condition, value) => {
113
+ if ("eq" in condition)
114
+ return compareUtf8(value, condition.eq) === 0;
115
+ if ("lt" in condition)
116
+ return compareUtf8(value, condition.lt) < 0;
117
+ if ("lte" in condition)
118
+ return compareUtf8(value, condition.lte) <= 0;
119
+ if ("gt" in condition)
120
+ return compareUtf8(value, condition.gt) > 0;
121
+ if ("gte" in condition)
122
+ return compareUtf8(value, condition.gte) >= 0;
123
+ if ("between" in condition) {
124
+ return (compareUtf8(value, condition.between[0]) >= 0 && compareUtf8(value, condition.between[1]) <= 0);
125
+ }
126
+ return startsWithKey(value, condition.beginsWith);
127
+ };
128
+ const isEmptyKey = (value) => value.length === 0;
129
+ /**
130
+ * The key condition a read of `range` sends; `undefined` reads the whole
131
+ * partition. A zero-length operand is a defect: DynamoDB rejects it, and no
132
+ * plan should ever compose one.
133
+ */
134
+ export const toKeyCondition = (range) => {
135
+ const condition = range.beginsWith !== undefined
136
+ ? { beginsWith: range.beginsWith }
137
+ : range.lower !== undefined && range.upper !== undefined
138
+ ? compareUtf8(range.lower, range.upper) === 0
139
+ ? { eq: range.lower }
140
+ : { between: [range.lower, range.upper] }
141
+ : range.lower !== undefined
142
+ ? { gte: range.lower }
143
+ : range.upper !== undefined
144
+ ? { lte: range.upper }
145
+ : undefined;
146
+ const operands = condition === undefined
147
+ ? []
148
+ : "between" in condition
149
+ ? condition.between
150
+ : [Object.values(condition)[0]];
151
+ if (operands.some(isEmptyKey)) {
152
+ throw new Error("[internal] A planned key condition has a zero-length operand, which DynamoDB rejects.");
153
+ }
154
+ return condition;
155
+ };
156
+ /** Is `value` inside `range` (as its key condition selects)? */
157
+ export const rangeContains = (range, value) => {
158
+ const condition = toKeyCondition(range);
159
+ return condition === undefined || keyConditionHolds(condition, value);
160
+ };
161
+ /** A key value as `explain` and `onRequest` show it: text as is, bytes as `0x…` hex. */
162
+ export const displayKeyValue = (value) => {
163
+ if (typeof value === "string")
164
+ return value;
165
+ let hex = "0x";
166
+ for (const byte of value)
167
+ hex += byte.toString(16).padStart(2, "0");
168
+ return hex;
169
+ };
170
+ /**
171
+ * The display bounds of a sort key condition: `eq` as `[v, v]`, `between` as
172
+ * `[a, b]`, one-sided operators open on the other side, `begins_with` as
173
+ * `[prefix, undefined]`, none as `[undefined, undefined]`.
174
+ */
175
+ export const displayKeyCondition = (condition) => {
176
+ if (condition === undefined)
177
+ return [undefined, undefined];
178
+ const show = displayKeyValue;
179
+ if ("eq" in condition)
180
+ return [show(condition.eq), show(condition.eq)];
181
+ if ("between" in condition)
182
+ return [show(condition.between[0]), show(condition.between[1])];
183
+ if ("gt" in condition)
184
+ return [show(condition.gt), undefined];
185
+ if ("gte" in condition)
186
+ return [show(condition.gte), undefined];
187
+ if ("lt" in condition)
188
+ return [undefined, show(condition.lt)];
189
+ if ("lte" in condition)
190
+ return [undefined, show(condition.lte)];
191
+ return [show(condition.beginsWith), undefined];
192
+ };
193
+ // ---------------------------------------------------------------------------
194
+ // Fingerprint (§10.3)
195
+ // ---------------------------------------------------------------------------
196
+ /** JSON with every object's keys sorted, so equal inputs give equal text. */
197
+ export const canonicalJson = (value) => {
198
+ if (value instanceof Uint8Array)
199
+ return JSON.stringify(displayKeyValue(value));
200
+ if (Array.isArray(value))
201
+ return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
202
+ if (typeof value === "object" && value !== null) {
203
+ const entries = Object.entries(value)
204
+ .filter(([, item]) => item !== undefined)
205
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
206
+ return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
207
+ }
208
+ if (typeof value === "bigint")
209
+ return JSON.stringify(value.toString());
210
+ return JSON.stringify(value) ?? "null";
211
+ };
212
+ /**
213
+ * An expression as `explain` shows it: its compiled text with every name and
214
+ * value placeholder replaced by what it stands for.
215
+ */
216
+ export const displayExpr = (expr) => {
217
+ const compiled = compileExpr(expr);
218
+ const shown = (value) => value.S !== undefined
219
+ ? JSON.stringify(value.S)
220
+ : value.N !== undefined
221
+ ? value.N
222
+ : value.BOOL !== undefined
223
+ ? String(value.BOOL)
224
+ : JSON.stringify(value);
225
+ return compiled.expression.replace(/[#:]e\d+/g, (placeholder) => placeholder.startsWith("#")
226
+ ? (compiled.names[placeholder] ?? placeholder)
227
+ : compiled.values[placeholder] === undefined
228
+ ? placeholder
229
+ : shown(compiled.values[placeholder]));
230
+ };
231
+ /** FNV-1a (32-bit) of `text`'s UTF-8 bytes, as 8 hex digits. */
232
+ export const fnv1a32 = (text) => {
233
+ let hash = 0x811c9dc5;
234
+ for (const byte of encoder.encode(text)) {
235
+ hash ^= byte;
236
+ hash = Math.imul(hash, 0x01000193) >>> 0;
237
+ }
238
+ return hash.toString(16).padStart(8, "0");
239
+ };
240
+ // ---------------------------------------------------------------------------
241
+ // Step helpers
242
+ // ---------------------------------------------------------------------------
243
+ /**
244
+ * `[EDD-9094]`: a cursor that doesn't belong to the query it was given to —
245
+ * malformed, from another query, or a position outside the recomputed plan.
246
+ * Raised by the terminal (cursors are decoded there), never by `startFrom`.
247
+ */
248
+ export const cursorRefusal = (entityType, why) => new ValidationError({
249
+ entityType,
250
+ operation: "query.startFrom",
251
+ cause: `[EDD-9094] The cursor does not belong to this query: ${why}. A cursor resumes only ` +
252
+ `the query that returned it — the same index, pinned composites, .within() bounds ` +
253
+ `and direction.`,
254
+ });
255
+ /**
256
+ * `step` resumed after `startKey`: what a read that returned a
257
+ * `LastEvaluatedKey`, or stopped inside its page, continues with. A filter
258
+ * read's position drops its tuple — `ExclusiveStartKey` alone places it.
259
+ */
260
+ export const readFrom = (step, startKey) => ({
261
+ ...step,
262
+ startKey,
263
+ at: {
264
+ ...step.at,
265
+ tuple: step.at.mode === "filter" ? undefined : step.at.tuple,
266
+ boundary: { _tag: "At", startKey },
267
+ },
268
+ });
269
+ const done = { _tag: "Done" };
270
+ const estimator = { _tag: "estimator" };
271
+ /** The cursor of a plan with this fingerprint at `step` (§10.1): its position, or `null` once done. */
272
+ const positionCursor = (fingerprint, step) => {
273
+ if (step._tag === "Done")
274
+ return null;
275
+ const at = step.at;
276
+ const boundary = at.boundary;
277
+ return {
278
+ v: 2,
279
+ f: fingerprint,
280
+ s: at.mode,
281
+ ...(at.tuple === undefined ? {} : { t: Cursor.keyValueToWire(at.tuple) }),
282
+ ...(boundary._tag === "At"
283
+ ? { k: Cursor.keyToWire(boundary.startKey) ?? {} }
284
+ : boundary._tag === "Through"
285
+ ? { a: Cursor.keyValueToWire(boundary.through) }
286
+ : boundary._tag === "TupleDone"
287
+ ? { d: 1 }
288
+ : {}),
289
+ ...(at.pass === undefined ? {} : { p: at.pass }),
290
+ };
291
+ };
292
+ /** A planned range as `explain` shows it. */
293
+ const shownRange = (range) => {
294
+ const [lower, upper] = displayKeyCondition(toKeyCondition(range));
295
+ return { lower, upper, exact: range.exact };
296
+ };
297
+ /**
298
+ * A planner for a composite sort key plan. `start` places a cursor in the
299
+ * recomputed plan (§10.3): string ranges are kept or dropped whole, never cut.
300
+ */
301
+ export const compositePlanner = (spec) => ({ reverse, maxSkips }) => {
302
+ const space = spec.space;
303
+ const fingerprint = fnv1a32(canonicalJson({ plan: spec.fingerprint, reverse }));
304
+ const P = spec.filterPlan.prefix;
305
+ const UPPER = `${P}${KEY_MAX}`;
306
+ const tupled = space._tag === "string";
307
+ const mode = spec.inner.empty
308
+ ? "empty"
309
+ : !tupled
310
+ ? "key"
311
+ : spec.ineligible !== undefined || maxSkips === 0
312
+ ? "filter"
313
+ : "skip-scan";
314
+ const reason = spec.inner.empty
315
+ ? "the bounds admit no value"
316
+ : space._tag === "none"
317
+ ? "leading composites bounded"
318
+ : (spec.ineligible ??
319
+ (maxSkips === 0 ? "maxSkips: 0" : `skip tuple: ${space.fields.join(", ")}`));
320
+ const refuse = (why) => cursorRefusal(spec.entityType, why);
321
+ // -- tuples (§7.2) --
322
+ const tupleOf = (key) => {
323
+ if (space._tag !== "string" || !key.startsWith(space.prefix))
324
+ return undefined;
325
+ let pos = space.prefix.length;
326
+ for (const label of space.labels) {
327
+ if (!key.startsWith(label, pos))
328
+ return undefined;
329
+ const end = key.indexOf(KEY_DELIMITER, pos + label.length);
330
+ if (end < 0)
331
+ return undefined;
332
+ pos = end + 1;
333
+ }
334
+ return key.startsWith(space.next, pos) ? key.slice(0, pos) : undefined;
335
+ };
336
+ /** Just above every key under `tuple`: its delimiter (`#`) bumped to the next character. */
337
+ const successor = (tuple) => `${tuple.slice(0, -1)}${String.fromCharCode(KEY_DELIMITER.charCodeAt(0) + 1)}`;
338
+ // -- steps --
339
+ const read = (at, range, startKey, rowCheck) => ({
340
+ _tag: "Read",
341
+ at: startKey === undefined ? at : { ...at, boundary: { _tag: "At", startKey } },
342
+ range,
343
+ startKey,
344
+ planIndex: undefined,
345
+ sizing: estimator,
346
+ rowCheck,
347
+ });
348
+ const keyRead = (boundary, startKey) => read({ mode: "key", tuple: undefined, boundary, skips: 0 }, spec.inner.conditionFor(undefined), startKey, spec.inner.rowCheck);
349
+ const tupleRead = (tuple, boundary, skips, startKey) => read({ mode: "skip-scan", tuple, boundary, skips }, spec.inner.conditionFor(tuple), startKey, spec.inner.rowCheck);
350
+ /** The filter range from a position: past its finished tuple, or all of `P`. */
351
+ const filterRange = (tuple) => {
352
+ const after = typeof tuple === "string" ? tuple : undefined;
353
+ return reverse
354
+ ? { lower: P, upper: after ?? UPPER, exact: spec.filterPlan.rowCheck === undefined }
355
+ : {
356
+ lower: after === undefined ? P : successor(after),
357
+ upper: UPPER,
358
+ exact: spec.filterPlan.rowCheck === undefined,
359
+ };
360
+ };
361
+ const filterRead = (from, startKey) => read({ ...from, mode: "filter", tuple: startKey === undefined ? from.tuple : undefined }, startKey === undefined ? filterRange(from.tuple) : filterRange(undefined), startKey, spec.filterPlan.rowCheck);
362
+ /** The next probe from `from` — or, once the budget is spent, the filter plan (§7.4). */
363
+ const probeFrom = (from) => {
364
+ if (from.skips >= maxSkips)
365
+ return filterRead(from);
366
+ const tuple = typeof from.tuple === "string" ? from.tuple : undefined;
367
+ return {
368
+ _tag: "Probe",
369
+ at: { ...from, mode: "skip-scan", skips: from.skips + 1 },
370
+ lower: reverse ? P : tuple === undefined ? P : successor(tuple),
371
+ upper: reverse ? (tuple ?? UPPER) : UPPER,
372
+ };
373
+ };
374
+ const fresh = (tuple, boundary) => ({
375
+ mode,
376
+ tuple,
377
+ boundary,
378
+ skips: 0,
379
+ });
380
+ const initial = () => mode === "empty"
381
+ ? done
382
+ : mode === "key"
383
+ ? keyRead({ _tag: "Start" })
384
+ : mode === "filter"
385
+ ? filterRead(fresh(undefined, { _tag: "Start" }))
386
+ : probeFrom(fresh(undefined, { _tag: "Start" }));
387
+ // -- resume (§10.3) --
388
+ /** `k` as a start key, when it lies in `range` under this partition. */
389
+ const startKeyIn = (key, range) => {
390
+ const pk = key[spec.pkField];
391
+ const sk = key[spec.skField];
392
+ if (pk === undefined || !("S" in pk) || pk.S !== spec.pkValue) {
393
+ return refuse("its key is in another partition");
394
+ }
395
+ const skValue = sk === undefined ? undefined : Cursor.wireToKeyValue(sk);
396
+ if (skValue === undefined || !rangeContains(range, skValue)) {
397
+ return refuse("its key is outside the recomputed plan");
398
+ }
399
+ return Cursor.keyFromWire(key);
400
+ };
401
+ /**
402
+ * Where `through` leaves a string range: entirely before it (`dropped`),
403
+ * entirely after it (`kept`), or across it, which no string plan's cursor
404
+ * can be (§10.3).
405
+ */
406
+ const clip = (range, through) => {
407
+ const p = range.beginsWith;
408
+ const before = (value) => compareUtf8(through, value) < 0;
409
+ const after = (value) => compareUtf8(through, value) > 0;
410
+ // Every key in the range is past `through` in scan direction?
411
+ const allPast = p !== undefined
412
+ ? reverse
413
+ ? after(p) && !startsWithKey(through, p)
414
+ : before(p)
415
+ : reverse
416
+ ? range.upper !== undefined && after(range.upper)
417
+ : range.lower !== undefined && before(range.lower);
418
+ if (allPast)
419
+ return "kept";
420
+ // …or none of them is?
421
+ const nonePast = p !== undefined
422
+ ? reverse
423
+ ? !after(p)
424
+ : after(p) && !startsWithKey(through, p)
425
+ : reverse
426
+ ? range.lower !== undefined && !after(range.lower)
427
+ : range.upper !== undefined && !before(range.upper);
428
+ return nonePast ? "dropped" : "straddle";
429
+ };
430
+ const start = (cursor) => {
431
+ if (cursor === undefined)
432
+ return initial();
433
+ if (cursor.f === undefined) {
434
+ // A legacy cursor, or a plain query's `{ k }`: a position inside a single range.
435
+ if (mode !== "key") {
436
+ return refuse(`it comes from a query without .within(), and this plan is ${mode}`);
437
+ }
438
+ if (cursor.k === undefined || cursor.s !== undefined || cursor.t !== undefined) {
439
+ return refuse("it carries no position in this plan");
440
+ }
441
+ const startKey = startKeyIn(cursor.k, spec.inner.conditionFor(undefined));
442
+ return startKey instanceof ValidationError
443
+ ? startKey
444
+ : keyRead({ _tag: "At", startKey }, startKey);
445
+ }
446
+ if (cursor.f !== fingerprint)
447
+ return refuse("its plan fingerprint differs");
448
+ // Nothing to resume: no cursor of an empty plan is ever handed out.
449
+ if (mode === "empty")
450
+ return done;
451
+ const s = cursor.s;
452
+ if (s === undefined ||
453
+ !(s === "key"
454
+ ? !tupled
455
+ : s === "filter"
456
+ ? tupled
457
+ : s === "skip-scan" && tupled && spec.ineligible === undefined)) {
458
+ return refuse(`its plan (${s ?? "none"}) can't continue as ${mode}`);
459
+ }
460
+ let tuple;
461
+ if (cursor.t !== undefined) {
462
+ const t = "S" in cursor.t ? cursor.t.S : undefined;
463
+ if (t === undefined || space._tag !== "string" || tupleOf(`${t}${space.next}`) !== t) {
464
+ return refuse("its skip tuple is not one of this plan's");
465
+ }
466
+ tuple = t;
467
+ }
468
+ const position = (boundary) => ({
469
+ mode: s,
470
+ tuple,
471
+ boundary,
472
+ skips: 0,
473
+ });
474
+ if (cursor.k !== undefined) {
475
+ if (s === "filter") {
476
+ if (tuple !== undefined)
477
+ return refuse("a filter position inside a tuple");
478
+ const startKey = startKeyIn(cursor.k, filterRange(undefined));
479
+ return startKey instanceof ValidationError
480
+ ? startKey
481
+ : filterRead(position({ _tag: "At", startKey }), startKey);
482
+ }
483
+ if (s === "skip-scan" && tuple === undefined)
484
+ return refuse("a key outside any tuple");
485
+ const range = spec.inner.conditionFor(tuple);
486
+ const startKey = startKeyIn(cursor.k, range);
487
+ if (startKey instanceof ValidationError)
488
+ return startKey;
489
+ return s === "key"
490
+ ? keyRead({ _tag: "At", startKey }, startKey)
491
+ : tupleRead(tuple, { _tag: "At", startKey }, 0, startKey);
492
+ }
493
+ if (cursor.a !== undefined) {
494
+ const through = Cursor.wireToKeyValue(cursor.a);
495
+ if (through === undefined || s === "filter" || (s === "skip-scan" && tuple === undefined)) {
496
+ return refuse("its 'through' position is not one of this plan's");
497
+ }
498
+ const clipped = clip(spec.inner.conditionFor(tuple), through);
499
+ if (clipped === "straddle")
500
+ return refuse("its position cuts a range of this plan");
501
+ const boundary = { _tag: "Through", through };
502
+ if (s === "key")
503
+ return clipped === "kept" ? keyRead(boundary) : done;
504
+ return clipped === "kept"
505
+ ? tupleRead(tuple, boundary, 0)
506
+ : probeFrom(position({ _tag: "TupleDone" }));
507
+ }
508
+ if (cursor.d !== undefined) {
509
+ if (tuple === undefined)
510
+ return refuse("a finished tuple without the tuple");
511
+ return s === "filter"
512
+ ? filterRead(position({ _tag: "TupleDone" }))
513
+ : probeFrom(position({ _tag: "TupleDone" }));
514
+ }
515
+ // Start: of tuple `t`, or of the plan.
516
+ if (tuple !== undefined) {
517
+ return s === "skip-scan"
518
+ ? tupleRead(tuple, { _tag: "Start" }, 0)
519
+ : refuse("a filter position at the start of a tuple");
520
+ }
521
+ return s === "key"
522
+ ? keyRead({ _tag: "Start" })
523
+ : s === "filter"
524
+ ? filterRead(position({ _tag: "Start" }))
525
+ : probeFrom(position({ _tag: "Start" }));
526
+ };
527
+ // -- transitions --
528
+ const afterRead = (step, lastEvaluatedKey) => {
529
+ if (lastEvaluatedKey !== undefined)
530
+ return readFrom(step, lastEvaluatedKey);
531
+ return step.at.mode === "skip-scan"
532
+ ? probeFrom({ ...step.at, boundary: { _tag: "TupleDone" } })
533
+ : done;
534
+ };
535
+ const afterProbe = (step, key) => {
536
+ if (key === undefined)
537
+ return done;
538
+ const tuple = typeof key === "string" ? tupleOf(key) : undefined;
539
+ const previous = typeof step.at.tuple === "string" ? step.at.tuple : undefined;
540
+ // A key the parse can't read — or, defensively, a tuple that doesn't
541
+ // advance — leaves the rest to the filter plan, from where the probe began.
542
+ if (tuple === undefined ||
543
+ (previous !== undefined &&
544
+ (reverse ? compareUtf8(tuple, previous) >= 0 : compareUtf8(tuple, previous) <= 0))) {
545
+ return filterRead(step.at);
546
+ }
547
+ return tupleRead(tuple, { _tag: "Start" }, step.at.skips);
548
+ };
549
+ // -- cursors and explain --
550
+ const cursor = (step) => positionCursor(fingerprint, step);
551
+ const rowCheck = mode === "filter" ? spec.filterPlan.rowCheck : spec.inner.rowCheck;
552
+ const explain = (sizing) => ({
553
+ index: spec.index,
554
+ plan: mode,
555
+ reason,
556
+ // The ranges known before anything is read: the key plan's one range,
557
+ // or the span the probes (or the filter read) walk.
558
+ keyConditions: mode === "empty"
559
+ ? []
560
+ : mode === "key"
561
+ ? [shownRange(spec.inner.conditionFor(undefined))]
562
+ : [{ lower: P, upper: UPPER, exact: false }],
563
+ filter: undefined,
564
+ rowChecks: rowCheck?.fields ?? [],
565
+ sizing,
566
+ ...(mode === "skip-scan" ? { maxSkips } : {}),
567
+ });
568
+ return {
569
+ fingerprint,
570
+ mode,
571
+ filter: undefined,
572
+ rowCheck,
573
+ start,
574
+ afterRead,
575
+ afterProbe,
576
+ following: () => [],
577
+ cursor,
578
+ explain,
579
+ };
580
+ };
581
+ /** The key range of a pass's condition: its inclusive forms. */
582
+ const passRange = (condition) => {
583
+ if (condition === undefined)
584
+ return { lower: undefined, upper: undefined, exact: true };
585
+ if ("beginsWith" in condition) {
586
+ return { lower: undefined, upper: undefined, beginsWith: condition.beginsWith, exact: true };
587
+ }
588
+ if ("eq" in condition)
589
+ return { lower: condition.eq, upper: condition.eq, exact: true };
590
+ if ("between" in condition) {
591
+ return { lower: condition.between[0], upper: condition.between[1], exact: true };
592
+ }
593
+ if ("gte" in condition)
594
+ return { lower: condition.gte, upper: undefined, exact: true };
595
+ if ("lte" in condition)
596
+ return { lower: undefined, upper: condition.lte, exact: true };
597
+ throw new Error("[internal] A key pass reads its pins: a strict bound has no 1.36 pass.");
598
+ };
599
+ /**
600
+ * A planner reading a query's own key and the key 1.36 composed for the same
601
+ * pins, one after the other (#171): rows 1.36 wrote whose key text changed
602
+ * stay under the 1.36 key until they are written again or `rekey` moves
603
+ * them. Those are the older rows, so a forward read takes the 1.36 key first
604
+ * and `.reverse()` takes it last; each key is read in the query's direction.
605
+ * The plan is `key`. When both keys are in one partition, a read of the 1.36
606
+ * key drops the rows the current key selects too (a value whose 1.36 text
607
+ * prefixes its escaped text: `50%` of `50%25`), so no row is read twice.
608
+ *
609
+ * A position is the start of a pass or a key inside one; its cursor names the
610
+ * pass in `p`. A cursor without a fingerprint (a plain query's `{ k }`, from
611
+ * before the fallback had anything to read, or from 1.36) is placed by its
612
+ * key: in the 1.36 pass when the key lies under the 1.36 key and not the
613
+ * current one.
614
+ */
615
+ export const keyPassesPlanner = (spec) => ({ reverse }) => {
616
+ const keyOf = (pass) => ({ pkValue: pass.pkValue, condition: pass.condition });
617
+ const fingerprint = fnv1a32(canonicalJson({
618
+ passes: { current: keyOf(spec.current), legacy: keyOf(spec.legacy) },
619
+ reverse,
620
+ }));
621
+ const order = reverse ? [0, 1] : [1, 0];
622
+ const passOf = (pass) => (pass === 1 ? spec.legacy : spec.current);
623
+ const refuse = (why) => cursorRefusal(spec.entityType, why);
624
+ const skField = spec.skField;
625
+ const currentCondition = spec.current.condition;
626
+ const samePartition = spec.legacy.pkValue === spec.current.pkValue;
627
+ /** Does a sort key lie under the current pass's condition? */
628
+ const inCurrent = (key) => currentCondition === undefined || keyConditionHolds(currentCondition, key);
629
+ /** In one partition: the 1.36 read leaves the current key's rows to the current read. */
630
+ const dedupe = samePartition && skField !== undefined
631
+ ? {
632
+ fields: [],
633
+ reads: [skField],
634
+ accepts: (row) => {
635
+ const value = row[skField];
636
+ const key = value?.S ?? value?.B;
637
+ if (key === undefined)
638
+ return true;
639
+ return currentCondition !== undefined && !keyConditionHolds(currentCondition, key);
640
+ },
641
+ }
642
+ : undefined;
643
+ const rows = spec.legacy.rows;
644
+ const reads = rows === undefined
645
+ ? []
646
+ : [...new Set([spec.pkField, ...(skField === undefined ? [] : [skField]), ...rows.reads])];
647
+ /** Is the row stored at this key? */
648
+ const storedAt = (row, key) => row[spec.pkField]?.S === key.pk && (skField === undefined || row[skField]?.S === key.sk);
649
+ /** A row the current read keeps: stored at its key now, or at no value's 1.36 text. */
650
+ const currentAccepts = (rows, row) => {
651
+ const composed = rows.composed(row);
652
+ if (composed === undefined || storedAt(row, composed))
653
+ return true;
654
+ if (rows.composed136 === undefined)
655
+ return false;
656
+ const own136 = rows.composed136(row);
657
+ return own136 === undefined || !storedAt(row, own136);
658
+ };
659
+ /** Does this row's stored sort key lie under the current pass's condition? */
660
+ const inCurrentRange = (row) => {
661
+ const value = skField === undefined ? undefined : row[skField];
662
+ const key = value?.S ?? value?.B;
663
+ return key !== undefined && inCurrent(key);
664
+ };
665
+ /** The current read's row check: not another value's row under the current key. */
666
+ const currentCheck = rows === undefined
667
+ ? undefined
668
+ : { fields: [], reads, accepts: (row) => currentAccepts(rows, row) };
669
+ /** The 1.36 read's row check: a row stored elsewhere whose key now the current read selects. */
670
+ const legacyCheck = rows === undefined
671
+ ? dedupe
672
+ : {
673
+ fields: [],
674
+ reads,
675
+ accepts: (row) => {
676
+ const composed = rows.composed(row);
677
+ if (composed === undefined)
678
+ return dedupe === undefined || dedupe.accepts(row);
679
+ if (storedAt(row, composed))
680
+ return false;
681
+ // Both ranges hold it, in one partition: the current read returned it.
682
+ if (samePartition && inCurrentRange(row) && currentAccepts(rows, row))
683
+ return false;
684
+ if (composed.pk !== spec.current.pkValue)
685
+ return false;
686
+ return composed.sk === undefined || inCurrent(composed.sk);
687
+ },
688
+ };
689
+ const checkOf = (pass) => (pass === 1 ? legacyCheck : currentCheck);
690
+ const read = (pass, boundary, startKey) => ({
691
+ _tag: "Read",
692
+ at: {
693
+ mode: "key",
694
+ tuple: undefined,
695
+ boundary: startKey === undefined ? boundary : { _tag: "At", startKey },
696
+ skips: 0,
697
+ pass,
698
+ },
699
+ range: passRange(passOf(pass).condition),
700
+ pkValue: pass === 1 ? spec.legacy.pkValue : undefined,
701
+ startKey,
702
+ planIndex: undefined,
703
+ sizing: estimator,
704
+ rowCheck: checkOf(pass),
705
+ });
706
+ /** Does `key` lie in `pass` — its partition, and the range its condition selects? */
707
+ const inPass = (key, pass) => {
708
+ const pk = key[spec.pkField];
709
+ if (pk === undefined || !("S" in pk) || pk.S !== passOf(pass).pkValue)
710
+ return false;
711
+ if (skField === undefined)
712
+ return true;
713
+ const sk = key[skField];
714
+ const value = sk === undefined ? undefined : Cursor.wireToKeyValue(sk);
715
+ if (value === undefined)
716
+ return false;
717
+ const condition = passOf(pass).condition;
718
+ return condition === undefined || keyConditionHolds(condition, value);
719
+ };
720
+ const start = (cursor) => {
721
+ if (cursor === undefined)
722
+ return read(order[0], { _tag: "Start" });
723
+ if (cursor.f === undefined) {
724
+ // A plain query's `{ k }`, or 1.36's: placed by its key.
725
+ if (cursor.k === undefined || cursor.s !== undefined || cursor.t !== undefined) {
726
+ return refuse("it carries no position in this plan");
727
+ }
728
+ // Under both keys (one partition): the current pass's.
729
+ const pass = inPass(cursor.k, 1) && !(samePartition && inPass(cursor.k, 0))
730
+ ? 1
731
+ : inPass(cursor.k, 0)
732
+ ? 0
733
+ : undefined;
734
+ if (pass === undefined)
735
+ return refuse("its key is outside the recomputed plan");
736
+ const startKey = Cursor.keyFromWire(cursor.k);
737
+ return read(pass, { _tag: "At", startKey }, startKey);
738
+ }
739
+ if (cursor.f !== fingerprint)
740
+ return refuse("its plan fingerprint differs");
741
+ if (cursor.s !== "key" || cursor.t !== undefined || cursor.a !== undefined) {
742
+ return refuse("its position is not one of this plan's");
743
+ }
744
+ if (cursor.d !== undefined)
745
+ return refuse("its position is not one of this plan's");
746
+ const pass = cursor.p ?? order[0];
747
+ if (cursor.k === undefined)
748
+ return read(pass, { _tag: "Start" });
749
+ if (!inPass(cursor.k, pass))
750
+ return refuse("its key is outside the recomputed plan");
751
+ const startKey = Cursor.keyFromWire(cursor.k);
752
+ return read(pass, { _tag: "At", startKey }, startKey);
753
+ };
754
+ const afterRead = (step, lastEvaluatedKey) => {
755
+ if (lastEvaluatedKey !== undefined)
756
+ return readFrom(step, lastEvaluatedKey);
757
+ const next = order[order.indexOf(step.at.pass ?? 0) + 1];
758
+ return next === undefined ? done : read(next, { _tag: "Start" });
759
+ };
760
+ const explain = (sizing) => ({
761
+ index: spec.index,
762
+ plan: "key",
763
+ reason: "one key condition, and the 1.36 key of rows not yet moved",
764
+ keyConditions: order.map((pass) => ({
765
+ ...shownRange(passRange(passOf(pass).condition)),
766
+ exact: checkOf(pass) === undefined,
767
+ })),
768
+ filter: undefined,
769
+ rowChecks: [],
770
+ sizing,
771
+ });
772
+ return {
773
+ fingerprint,
774
+ mode: "key",
775
+ filter: undefined,
776
+ rowCheck: checkOf(order[0]),
777
+ start,
778
+ afterRead,
779
+ afterProbe: () => done,
780
+ following: () => [],
781
+ cursor: (step) => positionCursor(fingerprint, step),
782
+ explain,
783
+ };
784
+ };
785
+ /** `bytes`, unless empty: DynamoDB rejects a zero-length key operand (`EmptyKeyBound`). */
786
+ const nonEmpty = (bytes) => bytes === undefined || bytes.length === 0 ? undefined : bytes;
787
+ const startsWithBytes = (value, prefix) => prefix.length <= value.length && compareBytes(value.subarray(0, prefix.length), prefix) === 0;
788
+ /**
789
+ * A planner for a Z-order sort key plan. Inside each tuple `T` (the given
790
+ * prefix, or each one a binary skip-scan finds), a range `[lo, hi]` of
791
+ * addresses is read as `BETWEEN T‖lo AND T‖hi`; the strategy decides which
792
+ * ranges (§5.5). `start` places a cursor by clipping the recomputed plan at
793
+ * its address (§10.3), so a cursor resumes under any `maxRanges` or
794
+ * strategy.
795
+ */
796
+ export const zorderPlanner = (spec) => ({ reverse, maxSkips, zorder }) => {
797
+ const { layout, box } = spec;
798
+ const W = layout.bytes;
799
+ const MAXZ = (1n << BigInt(layout.bits)) - 1n;
800
+ const P = spec.prefix;
801
+ const succP = succ(P);
802
+ const tupled = spec.tuple.length > 0;
803
+ const maxRanges = zorder?.maxRanges ?? DEFAULT_MAX_RANGES;
804
+ const fingerprint = fnv1a32(canonicalJson({ plan: spec.fingerprint, reverse }));
805
+ const refuse = (why) => cursorRefusal(spec.entityType, why);
806
+ // -- the strategy (§5.6) --
807
+ const [strategy, why] = box === "empty"
808
+ ? ["empty", "empty box"]
809
+ : !spec.bounded
810
+ ? ["key", "no dimension bounded"]
811
+ : zorder?.strategy !== undefined && zorder.strategy !== "auto"
812
+ ? [zorder.strategy, "explicit strategy"]
813
+ : spec.ranges(maxRanges, 1).exact
814
+ ? ["precise", "exact plan fits maxRanges"]
815
+ : ["jump", "exact plan over maxRanges"];
816
+ const mode = strategy === "empty" ? "empty" : !tupled ? strategy : maxSkips === 0 ? "filter" : "skip-scan";
817
+ const reason = !tupled || strategy === "empty"
818
+ ? why
819
+ : maxSkips === 0
820
+ ? "maxSkips: 0"
821
+ : `skip tuple: ${spec.tuple.map((segment) => segment.field).join(", ")}; ${strategy} ` +
822
+ `inside each tuple (${why})`;
823
+ /** The mode a read inside a tuple (or the whole plan) records. */
824
+ const readMode = tupled ? "skip-scan" : strategy;
825
+ // -- addresses (§5.2, §5.3) --
826
+ const full = box === "empty" ? undefined : box;
827
+ const { zmin, zmax } = full === undefined ? { zmin: 0n, zmax: 0n } : corners(layout, full);
828
+ const single = full?.every((intervals) => intervals.length === 1) === true;
829
+ const contains = (z) => full !== undefined && inBox(layout, full, z);
830
+ /** The first in-box address at or after `z`. */
831
+ const nextIn = (z) => {
832
+ if (full === undefined || z > zmax)
833
+ return undefined;
834
+ if (z <= zmin)
835
+ return zmin;
836
+ return single ? bigmin(layout, z, zmin, zmax) : nextInBox(layout, full, z);
837
+ };
838
+ /** The last in-box address at or before `z`. */
839
+ const prevIn = (z) => {
840
+ if (full === undefined || z < zmin)
841
+ return undefined;
842
+ if (z >= zmax)
843
+ return zmax;
844
+ return single ? litmax(layout, z, zmin, zmax) : prevInBox(layout, full, z);
845
+ };
846
+ /** The ranges each tuple reads, in scan direction (`jump`: its first). */
847
+ const planned = full === undefined
848
+ ? []
849
+ : strategy === "key"
850
+ ? [{ lo: 0n, hi: MAXZ, exact: true }]
851
+ : strategy === "precise"
852
+ ? spec.ranges(maxRanges, REFINE_FACTOR).ranges
853
+ : [{ lo: zmin, hi: zmax, exact: false }];
854
+ const ordered = reverse ? [...planned].reverse() : planned;
855
+ /** Where a range ends, in scan direction. */
856
+ const end = (r) => (reverse ? r.lo : r.hi);
857
+ /** `[lo, hi]` under `T`: a whole subtree as `begins_with T` (no condition when `T` is empty). */
858
+ const keyRange = (T, r) => r.lo === 0n && r.hi === MAXZ
859
+ ? {
860
+ lower: undefined,
861
+ upper: undefined,
862
+ ...(T.length > 0 ? { beginsWith: T } : {}),
863
+ exact: r.exact,
864
+ }
865
+ : {
866
+ lower: concatBytes([T, toBytes(r.lo, W)]),
867
+ upper: concatBytes([T, toBytes(r.hi, W)]),
868
+ exact: r.exact,
869
+ };
870
+ const tupleOf = (at) => (at.tuple instanceof Uint8Array ? at.tuple : P);
871
+ /** The address of a sort key under `T`, or `undefined` when it isn't one. */
872
+ const addressOf = (T, key) => key instanceof Uint8Array && key.length === T.length + W && startsWithBytes(key, T)
873
+ ? fromBytes(key.subarray(T.length))
874
+ : undefined;
875
+ /** The addresses a read covers. */
876
+ const spanOf = (step) => {
877
+ const T = tupleOf(step.at);
878
+ return {
879
+ lo: addressOf(T, step.range.lower) ?? 0n,
880
+ hi: addressOf(T, step.range.upper) ?? MAXZ,
881
+ exact: step.range.exact,
882
+ };
883
+ };
884
+ // -- steps --
885
+ const position = (tuple, boundary, skips) => ({ mode: readMode, tuple, boundary, skips });
886
+ const read = (at, T, r, planIndex, startKey, run = JUMP_INITIAL) => ({
887
+ _tag: "Read",
888
+ at: startKey === undefined ? at : { ...at, boundary: { _tag: "At", startKey } },
889
+ range: keyRange(T, r),
890
+ startKey,
891
+ planIndex,
892
+ sizing: strategy === "jump" ? { _tag: "jump", run } : estimator,
893
+ rowCheck: spec.rowCheck,
894
+ });
895
+ /** Everything up to address `z` of tuple `T` is done. */
896
+ const through = (T, z) => ({
897
+ _tag: "Through",
898
+ through: concatBytes([T, toBytes(z, W)]),
899
+ });
900
+ /** Tuple `at` is finished: the next probe, or the end of the plan. */
901
+ const tupleDone = (at) => tupled ? probeFrom({ ...at, boundary: { _tag: "TupleDone" } }) : done;
902
+ /** The first read of tuple `T` (or of the plan, `tuple` absent). */
903
+ const firstRead = (T, tuple, skips) => {
904
+ const at = position(tuple, { _tag: "Start" }, skips);
905
+ const first = ordered[0];
906
+ return first === undefined
907
+ ? tupleDone(at)
908
+ : read(at, T, first, strategy === "precise" ? 0 : undefined, undefined);
909
+ };
910
+ /** The filter range from a position: past its finished tuple, or all of `P` (§7.4). */
911
+ const filterRange = (tuple) => {
912
+ const exact = !spec.bounded && spec.filterRowCheck === undefined;
913
+ if (reverse)
914
+ return { lower: nonEmpty(P), upper: tuple ?? succP, exact };
915
+ if (tuple === undefined)
916
+ return { lower: nonEmpty(P), upper: succP, exact };
917
+ const lower = succ(tuple);
918
+ return lower === undefined ? undefined : { lower, upper: succP, exact };
919
+ };
920
+ const filterRead = (from, startKey) => {
921
+ const tuple = startKey === undefined && from.tuple instanceof Uint8Array ? from.tuple : undefined;
922
+ const range = filterRange(tuple);
923
+ if (range === undefined)
924
+ return done;
925
+ return {
926
+ _tag: "Read",
927
+ at: {
928
+ ...from,
929
+ mode: "filter",
930
+ tuple,
931
+ boundary: startKey === undefined ? from.boundary : { _tag: "At", startKey },
932
+ },
933
+ range,
934
+ startKey,
935
+ planIndex: undefined,
936
+ sizing: estimator,
937
+ rowCheck: spec.filterRowCheck,
938
+ };
939
+ };
940
+ // -- the binary skip-scan (§7.5) --
941
+ /** A probe from `at` (lower and upper inclusive; never a zero-length operand). */
942
+ const probe = (at, lower, upper) => ({
943
+ _tag: "Probe",
944
+ at: { ...at, mode: "skip-scan", skips: at.skips + 1 },
945
+ lower: nonEmpty(lower),
946
+ upper: nonEmpty(upper),
947
+ });
948
+ /**
949
+ * The next probe from `from` — or, once the budget is spent, the filter
950
+ * plan (§7.4). No stored key equals `succ(P)` or a tuple `T` (each is
951
+ * followed by at least the address), so the inclusive bounds read as the
952
+ * exclusive ones of §7.5.
953
+ */
954
+ const probeFrom = (from) => {
955
+ if (from.skips >= maxSkips)
956
+ return filterRead(from);
957
+ const T = from.tuple instanceof Uint8Array ? from.tuple : undefined;
958
+ if (reverse)
959
+ return probe(from, P, T ?? succP);
960
+ if (T === undefined)
961
+ return probe(from, P, succP);
962
+ const lower = succ(T);
963
+ return lower === undefined ? done : probe(from, lower, succP);
964
+ };
965
+ /**
966
+ * A probed key's skip tuple (§7.5): every prefix segment after `P`, each
967
+ * given one compared with its value in the (length, bytes) order keys sort
968
+ * in. A given segment below its value SEEKs to it; one above SKIPs past
969
+ * the missing segments before it (mirrored in reverse: `to` is then the
970
+ * next probe's upper bound). `undefined` for a key the parse can't read.
971
+ */
972
+ const parse = (key) => {
973
+ if (!startsWithBytes(key, P))
974
+ return undefined;
975
+ let pos = P.length;
976
+ let missingEnd = P.length;
977
+ for (const segment of spec.tuple) {
978
+ const start = pos;
979
+ const stop = segment.hash !== undefined
980
+ ? start + segment.hash
981
+ : start + 2 > key.length
982
+ ? -1
983
+ : start + 2 + ((key[start] << 8) | key[start + 1]);
984
+ if (stop < 0 || stop > key.length)
985
+ return undefined;
986
+ if (segment.given === undefined) {
987
+ missingEnd = stop;
988
+ }
989
+ else {
990
+ const order = compareBytes(key.subarray(start, stop), segment.given);
991
+ if (order !== 0) {
992
+ const seek = concatBytes([key.subarray(0, start), segment.given]);
993
+ const missing = key.slice(0, missingEnd);
994
+ return reverse
995
+ ? { _tag: "move", to: order > 0 ? (succ(seek) ?? succP) : missing }
996
+ : { _tag: "move", to: order < 0 ? seek : succ(missing) };
997
+ }
998
+ }
999
+ pos = stop;
1000
+ }
1001
+ return key.length - pos === W ? { _tag: "tuple", tuple: key.slice(0, pos) } : undefined;
1002
+ };
1003
+ const initial = () => {
1004
+ const fresh = position(undefined, { _tag: "Start" }, 0);
1005
+ return mode === "empty"
1006
+ ? done
1007
+ : mode === "filter"
1008
+ ? filterRead(fresh)
1009
+ : tupled
1010
+ ? probeFrom(fresh)
1011
+ : firstRead(P, undefined, 0);
1012
+ };
1013
+ // -- resume (§10.3) --
1014
+ /** Everything up to address `a` (in scan direction) is done: clip the plan there. */
1015
+ const resumeThrough = (tuple, sk, a) => {
1016
+ const T = tuple ?? P;
1017
+ const at = position(tuple, { _tag: "Through", through: sk }, 0);
1018
+ if (strategy === "jump") {
1019
+ const n = reverse
1020
+ ? a === 0n
1021
+ ? undefined
1022
+ : prevIn(a - 1n)
1023
+ : a === MAXZ
1024
+ ? undefined
1025
+ : nextIn(a + 1n);
1026
+ if (n === undefined)
1027
+ return tupleDone(at);
1028
+ return read(at, T, reverse ? { lo: zmin, hi: n, exact: false } : { lo: n, hi: zmax, exact: false }, undefined, undefined);
1029
+ }
1030
+ for (let i = 0; i < ordered.length; i++) {
1031
+ const r = ordered[i];
1032
+ if (reverse ? r.lo < a : r.hi > a) {
1033
+ const clipped = reverse
1034
+ ? { lo: r.lo, hi: r.hi < a ? r.hi : a - 1n, exact: r.exact }
1035
+ : { lo: r.lo > a ? r.lo : a + 1n, hi: r.hi, exact: r.exact };
1036
+ return read(at, T, clipped, strategy === "precise" ? i : undefined, undefined);
1037
+ }
1038
+ }
1039
+ return tupleDone(at);
1040
+ };
1041
+ /** `k` as a start key: in this partition, its sort key an address under `T`. */
1042
+ const placeKey = (key, T) => {
1043
+ const pk = key[spec.pkField];
1044
+ if (pk === undefined || !("S" in pk) || pk.S !== spec.pkValue) {
1045
+ return refuse("its key is in another partition");
1046
+ }
1047
+ const wire = key[spec.skField];
1048
+ const sk = wire === undefined ? undefined : Cursor.wireToKeyValue(wire);
1049
+ const address = addressOf(T, sk);
1050
+ if (address === undefined || !(sk instanceof Uint8Array)) {
1051
+ return refuse("its key is outside the recomputed plan");
1052
+ }
1053
+ return { startKey: Cursor.keyFromWire(key), sk, address };
1054
+ };
1055
+ /**
1056
+ * Resume inside the range holding `k`'s address, clipped to start there.
1057
+ * An address no range holds is out of the box — the rows at it were never
1058
+ * results — so the position reads as everything up to it being done.
1059
+ */
1060
+ const resumeAt = (tuple, key) => {
1061
+ const T = tuple ?? P;
1062
+ const placed = placeKey(key, T);
1063
+ if (placed instanceof ValidationError)
1064
+ return placed;
1065
+ const { startKey, sk, address: a } = placed;
1066
+ const at = position(tuple, { _tag: "At", startKey }, 0);
1067
+ if (strategy === "jump") {
1068
+ if (!contains(a))
1069
+ return resumeThrough(tuple, sk, a);
1070
+ return read(at, T, reverse ? { lo: zmin, hi: a, exact: false } : { lo: a, hi: zmax, exact: false }, undefined, startKey);
1071
+ }
1072
+ const i = ordered.findIndex((r) => r.lo <= a && a <= r.hi);
1073
+ if (i < 0)
1074
+ return resumeThrough(tuple, sk, a);
1075
+ const r = ordered[i];
1076
+ return read(at, T, reverse ? { lo: r.lo, hi: a, exact: r.exact } : { lo: a, hi: r.hi, exact: r.exact }, strategy === "precise" ? i : undefined, startKey);
1077
+ };
1078
+ const start = (cursor) => {
1079
+ if (cursor?.f !== undefined && cursor.f !== fingerprint) {
1080
+ return refuse("its plan fingerprint differs");
1081
+ }
1082
+ if (cursor === undefined)
1083
+ return initial();
1084
+ if (strategy === "empty") {
1085
+ return cursor.f === undefined
1086
+ ? refuse("it comes from a query without .within(), and this plan is empty")
1087
+ : done;
1088
+ }
1089
+ if (cursor.f === undefined) {
1090
+ // A legacy cursor, or a plain query's `{ k }`: a position in a single-range plan.
1091
+ if (tupled || !(strategy === "key" || strategy === "naive")) {
1092
+ return refuse(`it comes from a query without .within(), and this plan is ${mode}`);
1093
+ }
1094
+ if (cursor.k === undefined || cursor.s !== undefined || cursor.t !== undefined) {
1095
+ return refuse("it carries no position in this plan");
1096
+ }
1097
+ return resumeAt(undefined, cursor.k);
1098
+ }
1099
+ // Any strategy continues any other: positions are addresses, never ranges.
1100
+ const s = cursor.s;
1101
+ const fits = s !== undefined &&
1102
+ (tupled
1103
+ ? s === "skip-scan" || s === "filter"
1104
+ : s === "key" || s === "naive" || s === "jump" || s === "precise");
1105
+ if (!fits)
1106
+ return refuse(`its plan (${s ?? "none"}) can't continue as ${mode}`);
1107
+ let tuple;
1108
+ if (cursor.t !== undefined) {
1109
+ const t = Cursor.wireToKeyValue(cursor.t);
1110
+ const parsed = tupled && t instanceof Uint8Array ? parse(concatBytes([t, new Uint8Array(W)])) : undefined;
1111
+ if (parsed?._tag !== "tuple" ||
1112
+ !(t instanceof Uint8Array) ||
1113
+ compareBytes(parsed.tuple, t) !== 0) {
1114
+ return refuse("its skip tuple is not one of this plan's");
1115
+ }
1116
+ tuple = t;
1117
+ }
1118
+ const at = (boundary) => ({ mode: s, tuple, boundary, skips: 0 });
1119
+ if (cursor.k !== undefined) {
1120
+ if (s === "filter") {
1121
+ if (tuple !== undefined)
1122
+ return refuse("a filter position inside a tuple");
1123
+ const pk = cursor.k[spec.pkField];
1124
+ const wire = cursor.k[spec.skField];
1125
+ const sk = wire === undefined ? undefined : Cursor.wireToKeyValue(wire);
1126
+ if (pk === undefined || !("S" in pk) || pk.S !== spec.pkValue) {
1127
+ return refuse("its key is in another partition");
1128
+ }
1129
+ const range = filterRange(undefined);
1130
+ if (!(sk instanceof Uint8Array) || range === undefined || !rangeContains(range, sk)) {
1131
+ return refuse("its key is outside the recomputed plan");
1132
+ }
1133
+ const startKey = Cursor.keyFromWire(cursor.k);
1134
+ return filterRead(at({ _tag: "At", startKey }), startKey);
1135
+ }
1136
+ if (tupled && tuple === undefined)
1137
+ return refuse("a key outside any tuple");
1138
+ return resumeAt(tuple, cursor.k);
1139
+ }
1140
+ if (cursor.a !== undefined) {
1141
+ const sk = Cursor.wireToKeyValue(cursor.a);
1142
+ const a = addressOf(tuple ?? P, sk);
1143
+ if (a === undefined ||
1144
+ !(sk instanceof Uint8Array) ||
1145
+ s === "filter" ||
1146
+ (tupled && tuple === undefined)) {
1147
+ return refuse("its 'through' position is not one of this plan's");
1148
+ }
1149
+ return resumeThrough(tuple, sk, a);
1150
+ }
1151
+ if (cursor.d !== undefined) {
1152
+ if (tuple === undefined)
1153
+ return refuse("a finished tuple without the tuple");
1154
+ return s === "filter"
1155
+ ? filterRead(at({ _tag: "TupleDone" }))
1156
+ : probeFrom(at({ _tag: "TupleDone" }));
1157
+ }
1158
+ // Start: of tuple `t`, or of the plan.
1159
+ if (tuple !== undefined) {
1160
+ return s === "skip-scan"
1161
+ ? firstRead(tuple, tuple, 0)
1162
+ : refuse("a filter position at the start of a tuple");
1163
+ }
1164
+ return s === "filter"
1165
+ ? filterRead(at({ _tag: "Start" }))
1166
+ : tupled
1167
+ ? probeFrom(at({ _tag: "Start" }))
1168
+ : initial();
1169
+ };
1170
+ // -- transitions --
1171
+ const afterRead = (step, lastEvaluatedKey) => {
1172
+ if (step.at.mode === "filter") {
1173
+ return lastEvaluatedKey === undefined ? done : readFrom(step, lastEvaluatedKey);
1174
+ }
1175
+ const T = tupleOf(step.at);
1176
+ if (lastEvaluatedKey !== undefined) {
1177
+ if (step.sizing._tag !== "jump")
1178
+ return readFrom(step, lastEvaluatedKey);
1179
+ // `jump` (§5.5): the last row examined — not the last returned, which
1180
+ // the filter may have dropped — decides whether the run goes on.
1181
+ const run = step.sizing.run;
1182
+ const sk = lastEvaluatedKey[spec.skField]?.B;
1183
+ const a = addressOf(T, sk);
1184
+ if (a === undefined || contains(a)) {
1185
+ // In the box: rows may follow at the same address. The next page doubles (§8.5).
1186
+ return {
1187
+ ...readFrom(step, lastEvaluatedKey),
1188
+ sizing: { _tag: "jump", run: Math.min(run * 2, JUMP_MAX_LIMIT) },
1189
+ };
1190
+ }
1191
+ // Out of the box: a new key condition from the next in-box address —
1192
+ // never a fabricated start key (a GSI start key needs the table key).
1193
+ const span = spanOf(step);
1194
+ const n = reverse ? (a === 0n ? undefined : prevIn(a - 1n)) : nextIn(a + 1n);
1195
+ if (n === undefined || (reverse ? n < span.lo : n > span.hi))
1196
+ return tupleDone(step.at);
1197
+ return read({ ...step.at, boundary: { _tag: "Through", through: sk } }, T, reverse ? { lo: span.lo, hi: n, exact: false } : { lo: n, hi: span.hi, exact: false }, undefined, undefined);
1198
+ }
1199
+ // The range is finished: the next planned one, or the tuple is.
1200
+ const i = step.planIndex;
1201
+ if (i !== undefined && i + 1 < ordered.length) {
1202
+ return read({ ...step.at, boundary: through(T, end(spanOf(step))) }, T, ordered[i + 1], i + 1, undefined);
1203
+ }
1204
+ return tupleDone(step.at);
1205
+ };
1206
+ const afterProbe = (step, key) => {
1207
+ if (key === undefined)
1208
+ return done;
1209
+ const parsed = key instanceof Uint8Array ? parse(key) : undefined;
1210
+ // A key the parse can't read leaves the rest to the filter plan.
1211
+ if (parsed === undefined)
1212
+ return filterRead(step.at);
1213
+ if (parsed._tag === "tuple") {
1214
+ const previous = step.at.tuple instanceof Uint8Array ? step.at.tuple : undefined;
1215
+ const order = previous === undefined ? undefined : compareBytes(parsed.tuple, previous);
1216
+ // Defensively: a tuple that doesn't advance.
1217
+ if (order !== undefined && (reverse ? order >= 0 : order <= 0)) {
1218
+ return filterRead(step.at);
1219
+ }
1220
+ return firstRead(parsed.tuple, parsed.tuple, step.at.skips);
1221
+ }
1222
+ // SEEK / SKIP: probe again from where the given segment says.
1223
+ const to = parsed.to;
1224
+ if (to === undefined)
1225
+ return reverse ? filterRead(step.at) : done;
1226
+ const from = reverse ? step.upper : step.lower;
1227
+ const advances = from === undefined || (reverse ? compareUtf8(to, from) < 0 : compareUtf8(to, from) > 0);
1228
+ if (!advances || step.at.skips >= maxSkips)
1229
+ return filterRead(step.at);
1230
+ return reverse ? probe(step.at, P, to) : probe(step.at, to, succP);
1231
+ };
1232
+ /** The precise ranges after `step` in its tuple, not started (§9.2). */
1233
+ const following = (step, n) => {
1234
+ const i = step.planIndex;
1235
+ if (i === undefined || step.at.mode === "filter" || n <= 0)
1236
+ return [];
1237
+ const T = tupleOf(step.at);
1238
+ const out = [];
1239
+ let previous = spanOf(step);
1240
+ for (let j = i + 1; j < ordered.length && out.length < n; j++) {
1241
+ const r = ordered[j];
1242
+ out.push(read({ ...step.at, boundary: through(T, end(previous)) }, T, r, j, undefined));
1243
+ previous = r;
1244
+ }
1245
+ return out;
1246
+ };
1247
+ // -- cursors and explain --
1248
+ const rowCheck = mode === "filter" ? spec.filterRowCheck : spec.rowCheck;
1249
+ const explain = (sizing) => {
1250
+ const exactPlan = full === undefined ? undefined : spec.ranges(maxRanges, 1);
1251
+ const volume = full === undefined
1252
+ ? 0n
1253
+ : full.reduce((product, intervals) => product * intervals.reduce((n, [lo, hi]) => n + (hi - lo + 1n), 0n), 1n);
1254
+ return {
1255
+ index: spec.index,
1256
+ plan: mode,
1257
+ reason,
1258
+ // The ranges known before anything is read: each tuple's plan, or —
1259
+ // with a skip tuple — the span the probes (or the filter read) walk.
1260
+ keyConditions: mode === "empty"
1261
+ ? []
1262
+ : tupled
1263
+ ? [shownRange({ ...filterRange(undefined), exact: false })]
1264
+ : ordered.map((r) => shownRange(keyRange(P, r))),
1265
+ filter: spec.filter === undefined ? undefined : displayExpr(spec.filter),
1266
+ rowChecks: rowCheck?.fields ?? [],
1267
+ sizing,
1268
+ zorder: {
1269
+ bits: layout.bits,
1270
+ box: layout.dimensions.map((dimension, i) => ({
1271
+ field: dimension.field,
1272
+ bits: dimension.bits,
1273
+ levels: full === undefined
1274
+ ? []
1275
+ : full[i].map(([lo, hi]) => [lo.toString(), hi.toString()]),
1276
+ })),
1277
+ fill: full === undefined ? 0 : Number(volume) / Number(zmax - zmin + 1n),
1278
+ exactRanges: exactPlan === undefined ? 0 : exactPlan.exact ? exactPlan.ranges.length : "over-cap",
1279
+ },
1280
+ ...(mode === "skip-scan" ? { maxSkips } : {}),
1281
+ };
1282
+ };
1283
+ return {
1284
+ fingerprint,
1285
+ mode,
1286
+ filter: spec.filter,
1287
+ rowCheck,
1288
+ start,
1289
+ afterRead,
1290
+ afterProbe,
1291
+ following,
1292
+ cursor: (step) => positionCursor(fingerprint, step),
1293
+ explain,
1294
+ };
1295
+ };
1296
+ //# sourceMappingURL=RangePlanner.js.map