@quereus/store 4.11.0 → 4.12.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 (77) hide show
  1. package/README.md +131 -10
  2. package/dist/src/common/cached-kv-store.d.ts +16 -0
  3. package/dist/src/common/cached-kv-store.d.ts.map +1 -1
  4. package/dist/src/common/cached-kv-store.js +43 -0
  5. package/dist/src/common/cached-kv-store.js.map +1 -1
  6. package/dist/src/common/index.d.ts +1 -0
  7. package/dist/src/common/index.d.ts.map +1 -1
  8. package/dist/src/common/index.js +2 -0
  9. package/dist/src/common/index.js.map +1 -1
  10. package/dist/src/common/key-builder.d.ts +9 -6
  11. package/dist/src/common/key-builder.d.ts.map +1 -1
  12. package/dist/src/common/key-builder.js +17 -6
  13. package/dist/src/common/key-builder.js.map +1 -1
  14. package/dist/src/common/kv-store.d.ts +89 -3
  15. package/dist/src/common/kv-store.d.ts.map +1 -1
  16. package/dist/src/common/kv-store.js +21 -1
  17. package/dist/src/common/kv-store.js.map +1 -1
  18. package/dist/src/common/memory-store.d.ts +1 -0
  19. package/dist/src/common/memory-store.d.ts.map +1 -1
  20. package/dist/src/common/memory-store.js +7 -0
  21. package/dist/src/common/memory-store.js.map +1 -1
  22. package/dist/src/common/pk-key-resolution.d.ts +17 -4
  23. package/dist/src/common/pk-key-resolution.d.ts.map +1 -1
  24. package/dist/src/common/pk-key-resolution.js +23 -8
  25. package/dist/src/common/pk-key-resolution.js.map +1 -1
  26. package/dist/src/common/store-module-access-plan.d.ts +1 -1
  27. package/dist/src/common/store-module-access-plan.d.ts.map +1 -1
  28. package/dist/src/common/store-module-access-plan.js +434 -105
  29. package/dist/src/common/store-module-access-plan.js.map +1 -1
  30. package/dist/src/common/store-module-alter-column.d.ts.map +1 -1
  31. package/dist/src/common/store-module-alter-column.js +25 -2
  32. package/dist/src/common/store-module-alter-column.js.map +1 -1
  33. package/dist/src/common/store-module-alter.d.ts.map +1 -1
  34. package/dist/src/common/store-module-alter.js +36 -13
  35. package/dist/src/common/store-module-alter.js.map +1 -1
  36. package/dist/src/common/store-module-catalog.d.ts +6 -4
  37. package/dist/src/common/store-module-catalog.d.ts.map +1 -1
  38. package/dist/src/common/store-module-catalog.js +8 -6
  39. package/dist/src/common/store-module-catalog.js.map +1 -1
  40. package/dist/src/common/store-module-index.d.ts +98 -7
  41. package/dist/src/common/store-module-index.d.ts.map +1 -1
  42. package/dist/src/common/store-module-index.js +236 -81
  43. package/dist/src/common/store-module-index.js.map +1 -1
  44. package/dist/src/common/store-module.d.ts.map +1 -1
  45. package/dist/src/common/store-module.js +7 -3
  46. package/dist/src/common/store-module.js.map +1 -1
  47. package/dist/src/common/store-table-base.d.ts +30 -0
  48. package/dist/src/common/store-table-base.d.ts.map +1 -1
  49. package/dist/src/common/store-table-base.js +71 -12
  50. package/dist/src/common/store-table-base.js.map +1 -1
  51. package/dist/src/common/store-table-constraints.js +3 -3
  52. package/dist/src/common/store-table-constraints.js.map +1 -1
  53. package/dist/src/common/store-table-scan.d.ts +208 -98
  54. package/dist/src/common/store-table-scan.d.ts.map +1 -1
  55. package/dist/src/common/store-table-scan.js +353 -105
  56. package/dist/src/common/store-table-scan.js.map +1 -1
  57. package/dist/src/testing/index.d.ts +17 -5
  58. package/dist/src/testing/index.d.ts.map +1 -1
  59. package/dist/src/testing/index.js +16 -5
  60. package/dist/src/testing/index.js.map +1 -1
  61. package/dist/src/testing/kv-conformance.d.ts +49 -0
  62. package/dist/src/testing/kv-conformance.d.ts.map +1 -1
  63. package/dist/src/testing/kv-conformance.js +125 -0
  64. package/dist/src/testing/kv-conformance.js.map +1 -1
  65. package/dist/src/testing/kv-lifecycle.d.ts +22 -0
  66. package/dist/src/testing/kv-lifecycle.d.ts.map +1 -0
  67. package/dist/src/testing/kv-lifecycle.js +12 -0
  68. package/dist/src/testing/kv-lifecycle.js.map +1 -0
  69. package/dist/src/testing/kv-naming-conformance.d.ts +77 -0
  70. package/dist/src/testing/kv-naming-conformance.d.ts.map +1 -0
  71. package/dist/src/testing/kv-naming-conformance.js +208 -0
  72. package/dist/src/testing/kv-naming-conformance.js.map +1 -0
  73. package/dist/src/testing/kv-reclaim-conformance.d.ts +52 -0
  74. package/dist/src/testing/kv-reclaim-conformance.d.ts.map +1 -0
  75. package/dist/src/testing/kv-reclaim-conformance.js +287 -0
  76. package/dist/src/testing/kv-reclaim-conformance.js.map +1 -0
  77. package/package.json +5 -5
@@ -11,6 +11,30 @@ import { IndexConstraintOp, type IdxStrSpec, type CollationFunction, type Compar
11
11
  import type { IterateOptions, KVStore } from './kv-store.js';
12
12
  import type { KeyValueTransform } from './encoding.js';
13
13
  import { StoreTableBase } from './store-table-base.js';
14
+ /**
15
+ * Index entries per row-resolution batch: the scan arms below collect up to this many
16
+ * surviving index entries (or multi-seek point keys) and resolve them with ONE
17
+ * {@link StoreTableBase.readEffectiveRowsByKeys} call, so an indexed read of N rows
18
+ * costs on the order of N / 256 store round trips instead of N serialized point reads
19
+ * (on IndexedDB, one readonly transaction instead of N). This is the bound
20
+ * {@link KVStore.getMany}'s "bounded by the caller" note refers to.
21
+ *
22
+ * 256 matches the iterate pager's default page (`paged-iterate.ts` DEFAULT_BATCH_SIZE)
23
+ * and makes the same trade that pager already makes and the {@link KVStore.iterate}
24
+ * contract documents: a little early-termination cost for throughput — `limit 1`
25
+ * collects one batch rather than one row — while keeping peak memory independent of
26
+ * the range size.
27
+ *
28
+ * NOTE: that early-termination cost is bounded but not free — `limit 1` deserializes up
29
+ * to 256 rows to yield one, and holds them all at once. It is charged against ROUND
30
+ * TRIPS, which is what dominates on the backends this exists for (the index side already
31
+ * pages 256 entries per read, so a stopped scan costs one data round trip either way),
32
+ * so it is accepted as-is. Revisit if a `limit`-shaped query (a correlated `EXISTS`, a
33
+ * semi-join probe) shows up in a profile as row-decode or allocation cost: the fix is a
34
+ * SMALLER FIRST batch that ramps toward this bound, which keeps a drain's round-trip
35
+ * count within a log factor while making a stopped scan cost ~one row again.
36
+ */
37
+ export declare const ROW_RESOLUTION_BATCH = 256;
14
38
  /**
15
39
  * Read-path layer of the generic KVStore-backed virtual table.
16
40
  *
@@ -131,26 +155,61 @@ export declare abstract class StoreTableScan extends StoreTableBase {
131
155
  * Analyze filter info to determine a secondary-index access pattern, mirroring
132
156
  * {@link analyzePKAccess} but over the index chosen in `idxStr`.
133
157
  *
134
- * A contiguous leading-prefix EQ on the index columns yields a `point` window
135
- * (the prefix covers every entry sharing those leading values — an index seek
136
- * is a PREFIX scan, not a single row, since the index need not be unique and
137
- * the PK suffix varies); otherwise a range (LT/LE/GT/GE) on the LEADING index
138
- * column yields a `range` window. Returns null when neither applies or when the
139
- * index is unresolved. {@link matchesFilters} stays the authoritative row filter,
140
- * so the window need only be a SUPERSET.
158
+ * Three window shapes, in the order they are tried:
159
+ *
160
+ * - a contiguous leading-prefix EQ plus a LT/LE/GT/GE bound on the NEXT index column
161
+ * (the `prefixRangeSeek` / `plan=7` plan) — one `range` window covering just the
162
+ * bounded slice INSIDE the prefix;
163
+ * - a contiguous leading-prefix EQ alone → a `point` window (the prefix covers every
164
+ * entry sharing those leading values — an index seek is a PREFIX scan, not a single
165
+ * row, since the index need not be unique and the PK suffix varies);
166
+ * - otherwise a range (LT/LE/GT/GE) on the LEADING index column → a `range` window.
167
+ *
168
+ * Returns null when none applies or when the index is unresolved.
169
+ * {@link matchesFilters} stays the authoritative row filter, so the window need only be
170
+ * a SUPERSET — which is why every degraded path below WIDENS (a prefix-range that
171
+ * cannot narrow falls back to the prefix window, a prefix that cannot encode a member
172
+ * stops short) rather than declining outright.
141
173
  *
142
174
  * Index-column bytes are encoded under each column's own key collation
143
175
  * ({@link indexKeyCollations}); {@link matchesFilters} re-checks a fetched row under the
144
176
  * index column's `COLLATE` else the table column's declared collation. The EQ/prefix
145
177
  * window is EXACTLY the qualifying set when those two agree, which
146
- * {@link indexPrefixSeekIsCollationExact} decides; the RANGE window additionally equates
147
- * memcmp of the key bytes with the residual's comparison ORDER, which
148
- * {@link indexRangeIsOrderSafe} decides. `tryIndexAccessPlan`
149
- * (store-module-access-plan.ts) consults the SAME two predicates before claiming the
150
- * filters handled, so the two decisions cannot disagree. Either failing returns null and
151
- * the caller full-scans, where the residual is authoritative.
178
+ * {@link indexPrefixSeekIsCollationExact} decides; a RANGE window additionally equates
179
+ * memcmp of the key bytes with the residual's comparison ORDER at the bounded column's
180
+ * position, which {@link indexRangeIsOrderSafe} decides. `tryIndexAccessPlan`
181
+ * (store-module-access-plan.ts) consults the SAME two predicates, at the same positions,
182
+ * before claiming the filters handled, so the two decisions cannot disagree. An EQ-prefix
183
+ * failure returns null and the caller full-scans, where the residual is authoritative.
152
184
  */
153
185
  protected analyzeIndexAccess(filterInfo: FilterInfo): IndexAccessPattern | null;
186
+ /**
187
+ * The prefix length a `prefixRangeSeek` (`plan=7`) FilterInfo declares, or null when
188
+ * this FilterInfo is not one.
189
+ *
190
+ * Honoring the plan's own `prefixLen` — rather than re-deriving it from the constraint
191
+ * list — is what makes a plan/scan disagreement LOUD: a value outside `1 …
192
+ * index.columns.length - 1` cannot describe a window over this index, and the plan
193
+ * already dropped the residual for the filters it claimed, so guessing a length would
194
+ * silently address the wrong columns.
195
+ */
196
+ private prefixRangeSeekLength;
197
+ /** The pushed LT/LE/GT/GE constraints on `columnIndex`, paired with their bound values. */
198
+ private rangeConstraintsOn;
199
+ /**
200
+ * The narrowed window for a prefix-equality + trailing-range seek: the equality prefix
201
+ * bytes, further bounded by the LT/LE/GT/GE constraints on the index column right after
202
+ * it. Null when the trailing bound cannot be used, in which case the caller falls back
203
+ * to the plain prefix window — a strict SUPERSET, so the answer is unchanged and only
204
+ * the narrowing is lost. Two reasons for that:
205
+ *
206
+ * - the trailing column's key bytes do not reproduce its residual comparator's ORDER
207
+ * ({@link indexRangeIsOrderSafe}) — `tryIndexAccessPlan` degrades to the plain prefix
208
+ * seek through the same predicate, so this is the matching scan-side move rather than
209
+ * a disagreement;
210
+ * - no bound constraint survived on that column at all (nothing to narrow with).
211
+ */
212
+ private buildPrefixRangeWindow;
154
213
  /**
155
214
  * Lazy cache behind {@link indexKeyCollations}: one resolved array per index-schema
156
215
  * object, invalidated when the column array it was resolved against is replaced (an
@@ -209,23 +268,35 @@ export declare abstract class StoreTableScan extends StoreTableBase {
209
268
  */
210
269
  getIndexComparator(indexName: string): CompareFn[] | undefined;
211
270
  /**
212
- * True when a byte window over the LEADING column of `index` reproduces the order the
213
- * residual comparison uses. Delegates to {@link indexLeadingRangeIsOrderSafe} — the SAME
214
- * call the range arm of `tryIndexAccessPlan` (store-module-access-plan.ts) makes before
215
- * it marks the range filters handled, so the "build a window" and "mark handled"
216
- * decisions cannot disagree. The memoized {@link indexKeyCollations} is passed as the
217
- * key side, so the predicate judges the bytes this scan actually addresses.
271
+ * True when a byte window bounded on index column `position` reproduces the order the
272
+ * residual comparison uses there. Delegates to {@link indexRangeAtPositionIsOrderSafe} —
273
+ * the SAME call `tryIndexAccessPlan` (store-module-access-plan.ts) makes, at the same
274
+ * position, before it marks the range filters handled, so the "build a window" and "mark
275
+ * handled" decisions cannot disagree. The memoized {@link indexKeyCollations} is passed
276
+ * as the key side, so the predicate judges the bytes this scan actually addresses.
277
+ *
278
+ * `position` is 0 for a standalone leading-column range and the prefix length for a
279
+ * prefix-equality + trailing-range window.
218
280
  */
219
- protected indexRangeIsOrderSafe(index: TableIndexSchema): boolean;
281
+ protected indexRangeIsOrderSafe(index: TableIndexSchema, position: number): boolean;
220
282
  /**
221
- * Convert leading-index-column LT/LE/GT/GE constraints into one encoded-byte
222
- * `gte`/`lt` window — the secondary-index analogue of {@link buildPKRangeBounds}.
283
+ * Convert LT/LE/GT/GE constraints on ONE index column into a single encoded-byte
284
+ * `gte`/`lt` window, optionally nested inside a fixed equality prefix — the
285
+ * secondary-index analogue of {@link buildPKRangeBounds}.
286
+ *
287
+ * The bounded column sits at position `prefixValues.length`; `directions`,
288
+ * `collations` and `transforms` are the index's own per-column arrays sliced to cover
289
+ * the prefix AND that column. An empty `prefixValues` is the standalone leading-column
290
+ * range; a non-empty one is the trailing bound of a `prefixRangeSeek`.
223
291
  *
224
- * Each bound value is encoded under the leading index column's own key `collation`
225
- * ({@link indexKeyCollations}) and DESC `dir` — exactly as {@link buildIndexKey}
226
- * encodes that column — via {@link buildIndexPrefixBounds}, giving the byte region
227
- * `[lo, hi)` whose leading column equals that value. The op maps that region's
228
- * endpoints onto `gte`/`lt`, with the same DESC lower/upper SWAP as the PK path:
292
+ * Each bound value is encoded BEHIND the prefix, under the bounded column's own key
293
+ * collation ({@link indexKeyCollations}) and DESC direction — exactly as
294
+ * {@link buildIndexKey} encodes that column — via {@link buildIndexPrefixBounds},
295
+ * giving the byte region `[lo, hi)` whose prefix columns equal `prefixValues` and whose
296
+ * bounded column equals that value. The op maps that region's endpoints onto
297
+ * `gte`/`lt`, with the same DESC lower/upper SWAP as the PK path — driven by the BOUNDED
298
+ * column's direction alone, since a DESC prefix column merely inverts bytes *inside* a
299
+ * prefix that stays fixed:
229
300
  *
230
301
  * | op | ASC | DESC |
231
302
  * |----|----------|----------|
@@ -234,40 +305,94 @@ export declare abstract class StoreTableScan extends StoreTableBase {
234
305
  * | LE | lt = hi | gte = lo |
235
306
  * | LT | lt = lo | gte = hi |
236
307
  *
237
- * Across constraints keep the MAX lower and MIN upper. An `undefined` upper (an
238
- * `hi` whose increment overflowed all-0xff) leaves that side unbounded — a safe
239
- * SUPERSET. A NULL/missing bound value is skipped (the planner never pushes
240
- * `= NULL`, and a range op against NULL rejects every row in matchesFilters).
241
- * A bound over a semantic-ordering leading column (`logicalType`) whose VALUE has
242
- * no faithful byte position ({@link semanticProbeIsKeyFaithful}) is skipped the
243
- * same way — a dropped RANGE bound only widens the window, and matchesFilters
244
- * reproduces the predicate under the type's compare; see the widen-vs-decline
245
- * note on {@link buildPKRangeBounds}. `transform` is the leading column's key
246
- * value transform ({@link indexKeyTransforms}), so a surviving bound addresses
247
- * the same transformed bytes the index store holds.
308
+ * Both sides START at the prefix's own window (`[P, incr(P))`, or unbounded for an
309
+ * empty prefix) and are then tightened: keep the MAX lower and MIN upper across
310
+ * constraints. So every degraded path lands back on the prefix window, never wider:
311
+ * an `undefined` upper (an `hi` whose increment overflowed all-0xff), a NULL/missing
312
+ * bound value (the planner never pushes `= NULL`, and a range op against NULL rejects
313
+ * every row in matchesFilters), and a bound over a semantic-ordering column
314
+ * (`logicalType`) whose VALUE has no faithful byte position
315
+ * ({@link semanticProbeIsKeyFaithful}) are all simply skipped — a dropped RANGE bound
316
+ * only widens the window, and matchesFilters reproduces the predicate under the type's
317
+ * compare; see the widen-vs-decline note on {@link buildPKRangeBounds}. `transforms`
318
+ * carry each column's key value transform ({@link indexKeyTransforms}), so a surviving
319
+ * bound addresses the same transformed bytes the index store holds.
248
320
  */
249
- protected buildIndexRangeBounds(constraints: Array<{
321
+ protected buildIndexRangeBounds(prefixValues: readonly SqlValue[], constraints: Array<{
250
322
  op: IndexConstraintOp;
251
323
  value?: SqlValue;
252
- }>, dir: boolean, collation: string | undefined, logicalType: LogicalType | undefined, transform: KeyValueTransform | undefined): IterateOptions;
324
+ }>, directions: ReadonlyArray<boolean>, collations: ReadonlyArray<string | undefined>, logicalType: LogicalType | undefined, transforms: ReadonlyArray<KeyValueTransform | undefined>): IterateOptions;
253
325
  /**
254
326
  * Scan a secondary index over `access.bounds`, resolving each index entry to its
255
327
  * base row and re-filtering.
256
328
  *
257
- * {@link iterateEffective} yields the committed index entries merged with this
258
- * transaction's pending index puts/deletes (read-your-own-writes over the
259
- * index), in index-key byte order — the order the isolation overlay merge relies
260
- * on (`isolated-table.ts` § buildSortKey). We resolve each entry to its row via
261
- * its stored data-key value WITHOUT reordering, so index-key order is preserved.
262
- *
263
- * Defense in depth mirroring the memory layer's live-recheck: a resolved-null
264
- * row (the entry's row was deleted — a pending index delete would normally
265
- * suppress the entry, but a committed entry can lag) is skipped, and every
266
- * resolved row is re-checked by {@link matchesFilters} (the byte window is only
267
- * a superset, and a stale entry whose indexed column no longer matches is
268
- * dropped).
329
+ * One window of the producer/consumer pair below: {@link produceIndexEntries}
330
+ * walks the window's index entries and {@link resolveIndexEntries} resolves them
331
+ * to rows in {@link ROW_RESOLUTION_BATCH}-bounded batches — one
332
+ * {@link StoreTableBase.readEffectiveRowsByKeys} round trip per batch instead of
333
+ * one awaited point read per entry. A multi-seek runs the SAME consumer over the
334
+ * concatenation of every window's producer instead of calling this method — see
335
+ * {@link scanMultiSeek}.
336
+ */
337
+ protected scanIndex(indexStore: KVStore, access: IndexAccessPattern, filterInfo: FilterInfo): AsyncIterable<Row>;
338
+ /**
339
+ * Producer half of the index-scan pair: walk one byte window's index entries —
340
+ * {@link iterateEffective} yields the committed entries merged with this
341
+ * transaction's pending index puts/deletes (read-your-own-writes over the index),
342
+ * in index-key byte order, the order the isolation overlay merge relies on
343
+ * (`isolated-table.ts` § buildSortKey) — and yield, per surviving entry, its data
344
+ * key plus the FilterInfos a resolved row may match. Entries stay in visit order
345
+ * end to end, so index-key order is preserved through the batching consumer.
346
+ *
347
+ * Only the defenses that need nothing but the entry itself run here, BEFORE the
348
+ * row read; everything row-dependent lives in {@link resolveRowBatch}.
269
349
  */
270
- protected scanIndex(indexStore: KVStore, access: IndexAccessPattern, filterInfo: FilterInfo, multi?: MultiSeekWindowContext): AsyncIterable<Row>;
350
+ private produceIndexEntries;
351
+ /**
352
+ * Consumer half of the index-scan pair: collect up to {@link ROW_RESOLUTION_BATCH}
353
+ * produced entries and resolve each batch through {@link resolveRowBatch} — one
354
+ * store round trip per batch. Batching changes the round-trip count, never the row
355
+ * set or its order: batches are resolved and yielded in entry order.
356
+ *
357
+ * Early termination: a downstream `break` mid-batch finishes this generator, whose
358
+ * `for await` closes `entries` — and the iterate cursor under it — on the way out;
359
+ * no further batch is collected or read.
360
+ */
361
+ private resolveIndexEntries;
362
+ /**
363
+ * Resolve one bounded batch of index entries to rows — a single
364
+ * {@link StoreTableBase.readEffectiveRowsByKeys} call (which applies this
365
+ * transaction's pending deletes/puts BEFORE touching the committed store; the
366
+ * precedence is not re-implemented here) — then run the row-dependent defenses in
367
+ * entry order, mirroring the memory layer's live-recheck:
368
+ *
369
+ * - a resolved-null row (the entry's row was deleted — a pending index delete
370
+ * would normally suppress the entry, but a committed entry can lag) is skipped,
371
+ * and cannot shift later positions (the read answers positionally);
372
+ * - the multi-seek `seen` set is RE-checked after the read: the producer's
373
+ * pre-check ran before this batch was collected, so two entries at one data key
374
+ * inside a single batch (a stale index entry) are only caught here — without
375
+ * the re-check the row would yield twice;
376
+ * - a row is accepted when it matches ANY of the entry's FilterInfos (the byte
377
+ * window is only a superset, and a stale entry whose indexed column no longer
378
+ * matches is dropped), and `seen` is added to on yield ALONE — a stale entry
379
+ * that fails its residual must not suppress the row's live entry later.
380
+ *
381
+ * NOTE: still one data-store point read per matched index entry — the row lives in
382
+ * the data store, not the index (the index value carries only the data key, no
383
+ * covering payload); batching collapses the reads' round trips, not their count.
384
+ * If index-covered scans ever dominate a profile, consider storing the serialized
385
+ * row as a covering index value, at the cost of an index rewrite on EVERY column
386
+ * change (not just indexed columns) — deliberately not done here.
387
+ */
388
+ private resolveRowBatch;
389
+ /**
390
+ * Fail loudly on a structurally impossible FilterInfo — one whose own idxStr describes
391
+ * an access this table's schema cannot express. Not a soundness fallback: everything
392
+ * that is merely UNSERVEABLE degrades to a wider window plus the {@link matchesFilters}
393
+ * residual instead of raising. `kind` names the plan shape for the message.
394
+ */
395
+ private malformedFilterInfo;
271
396
  /**
272
397
  * Fail loudly on a multi-seek FilterInfo this table cannot serve. The plan already
273
398
  * dropped the residual Filter, and {@link matchesFilters} ANDs every pushed
@@ -303,27 +428,43 @@ export declare abstract class StoreTableScan extends StoreTableBase {
303
428
  * away before scanning:
304
429
  * - Tuples whose encoded prefix byte-matches (duplicate bound parameters, or
305
430
  * case variants under a NOCASE key collation C) share ONE window, each kept
306
- * as a residual alternative — see {@link MultiSeekWindowContext}.
431
+ * as a residual alternative — see {@link PendingIndexEntry}.
307
432
  * - A window with no finite upper bound (all-0xff prefix — see
308
433
  * buildIndexPrefixBounds) contains every later-sorting window outright (any
309
434
  * key ≥ an all-0xff prefix necessarily starts with it), so those windows fold
310
435
  * into it instead of re-scanning an overlapping range out of order.
311
436
  *
312
- * Emission is lazy per window — `… in (…) limit 1` stops after the first yielded
313
- * row without materializing the remaining windows. Each window goes through
314
- * {@link scanIndex} → {@link iterateEffective}, so read-your-own-writes and the
315
- * stale-entry / deleted-row defenses hold per seek key.
437
+ * Emission is lazy and batched ACROSS windows: every window's
438
+ * {@link produceIndexEntries} feeds ONE {@link resolveIndexEntries} consumer, whose
439
+ * batches are bounded by ENTRY count, not window count — so a hundred single-row
440
+ * windows collapse into a single row-resolution round trip, and
441
+ * `… in (…) limit 1` materializes at most one {@link ROW_RESOLUTION_BATCH} batch
442
+ * rather than every window. Each window's entries still come from
443
+ * {@link iterateEffective}, so read-your-own-writes and the stale-entry /
444
+ * deleted-row defenses hold per seek key.
316
445
  */
317
446
  protected scanMultiSeek(spec: IdxStrSpec, filterInfo: FilterInfo): AsyncIterable<Row>;
447
+ /**
448
+ * Concatenate the disjoint multi-seek windows' producers, in window (byte) order.
449
+ * Each entry carries its window's full FilterInfo list — the primary tuple plus
450
+ * the byte-equal merged alternatives — so acceptance stays per row, in order (see
451
+ * {@link PendingIndexEntry}).
452
+ */
453
+ private produceMultiSeekEntries;
318
454
  /**
319
455
  * Multi-seek over the PRIMARY key: each tuple is a full PK, resolved by a point
320
456
  * read in ascending encoded-data-key order (the table's native emission order).
321
457
  *
322
- * NOT reachable from this module's own plans today: `computeBestAccessPlan` (store-module-access-plan.ts)
323
- * claims IN-list filters only for secondary indexes (see its EQ_OPS vs EQ_OR_IN_OPS
324
- * split and tickets/backlog/feat-store-pk-in-list-multiseek). The branch exists so
325
- * a `_primary_` multi-seek arriving from a future plan gets a correct answer rather
326
- * than the scan arm's silent zero rows, and it is what PK-IN enablement will build on.
458
+ * Reached from `computeBestAccessPlan`'s primary-key multi-seek arm
459
+ * (store-module-access-plan.ts), which claims a whole-primary-key `IN` — literal list
460
+ * or runtime-valued set — through `EQ_OR_IN_OPS` and advertises `_primary_` with every
461
+ * PK column as its seek columns. That arm declines exactly the two shapes the
462
+ * {@link multiSeekMalformed} throws below assert against (over the seek cap, and a
463
+ * semantic-ordering PK member), so a throw here means the plan and the scan disagreed.
464
+ *
465
+ * The ascending-encoded-key emission order is a contract, not an implementation
466
+ * detail: the isolation layer merges this stream with its overlay by primary key
467
+ * (`bug-isolation-multiseek-merge-order`), and the arm advertises PK ordering off it.
327
468
  */
328
469
  protected scanMultiSeekPrimary(tuples: MultiSeekTuple[], seekWidth: number, filterInfo: FilterInfo): AsyncIterable<Row>;
329
470
  /**
@@ -397,9 +538,10 @@ interface PKAccessPattern {
397
538
  /**
398
539
  * Secondary-index access pattern analysis result: the chosen index plus the
399
540
  * encoded byte window {@link StoreTableScan.scanIndex} iterates. `point` is a
400
- * leading-prefix EQ window, `range` a leading-column LT/LE/GT/GE window; both
401
- * resolve to a `bounds` scan (an index seek is always a prefix scan, never a
402
- * single entry).
541
+ * leading-prefix EQ window; `range` is an LT/LE/GT/GE window, either on the leading
542
+ * column or nested inside an equality prefix (`prefixRangeSeek`). Both resolve to a
543
+ * `bounds` scan (an index seek is always a prefix scan, never a single entry) — nothing
544
+ * downstream branches on the tag, which is descriptive only.
403
545
  */
404
546
  interface IndexAccessPattern {
405
547
  index: TableIndexSchema;
@@ -416,37 +558,5 @@ interface MultiSeekTuple {
416
558
  entries: ConstraintEntry[];
417
559
  values: SqlValue[];
418
560
  }
419
- /**
420
- * Per-window context a multi-seek passes to {@link StoreTableScan.scanIndex} (absent for
421
- * ordinary single-window scans).
422
- *
423
- * `seen` — data-key hexes already YIELDED by an earlier window. Checked before the
424
- * data-store read (a cross-window duplicate costs no extra `get`), added only on a
425
- * yield: adding at visit time would let a stale index entry (row re-keyed since
426
- * indexing) that FAILS its residual poison the set and suppress the row's live entry
427
- * in a later window.
428
- * NOTE: holds one hex string per row the multi-seek yields, for the whole seek — the
429
- * only unbounded allocation on this path. Windows are byte-disjoint, so the only real
430
- * duplicate source is a stale index entry; if a large-result `IN` ever shows up as a
431
- * memory problem, the set can be scoped per window (or dropped for a consistent store).
432
- *
433
- * `collations` — the constraint collation map, resolved once per multi-seek (it is
434
- * identical for every window).
435
- *
436
- * `extraTuples` — additional seek tuples whose encoded window byte-equals this
437
- * window's (duplicate bound parameters, or case-variant values under a NOCASE key
438
- * collation). A row is yielded when it matches the primary FilterInfo OR any of
439
- * these. Index bytes now encode under the same collation C the residual compares
440
- * under (`indexKeyCollations`), so byte-equal tuples are C-equal and each merged
441
- * tuple's residual admits the same rows — the OR is redundancy, kept because it is
442
- * what makes the fold safe by construction rather than by that argument, and it is
443
- * what a custom equality-only normalizer (byte-equal ⇏ comparator-equal order but
444
- * equal partition) still relies on.
445
- */
446
- interface MultiSeekWindowContext {
447
- seen: Set<string>;
448
- collations: ReadonlyMap<number, CollationFunction>;
449
- extraTuples: readonly FilterInfo[];
450
- }
451
561
  export {};
452
562
  //# sourceMappingURL=store-table-scan.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"store-table-scan.d.ts","sourceRoot":"","sources":["../../../src/common/store-table-scan.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"store-table-scan.d.ts","sourceRoot":"","sources":["../../../src/common/store-table-scan.ts"],"names":[],"mappings":"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"}