Skip to content

feat(storage) Add bounded selected-row and field projection for stored V2 sections - #58

Merged
xerj-org merged 10 commits into
xerj-org:mainfrom
probelabs:feat/bounded-stored-projection
Jul 27, 2026
Merged

feat(storage) Add bounded selected-row and field projection for stored V2 sections#58
xerj-org merged 10 commits into
xerj-org:mainfrom
probelabs:feat/bounded-stored-projection

Conversation

@buger

@buger buger commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

This change adds a storage-level API that hydrates selected document ordinals and selected top-level _source fields directly from the existing ZBS2 stored format without reconstructing the full canonical JSON array.

It is a bounded-memory primitive for late materialization. It preserves exact selected bytes and supports every current ZBS2 codec, sparse fields, nested values, cross-column dependencies, malformed-input errors, cancellation, V1 compatibility fallback, and checked allocation behavior.

This branch does not connect the primitive to an engine search route and does not claim an end-to-end speedup. A dependent engine change integrates point GET and publish-time admission through the shared segment-hydration budget.

Why this is needed

The existing decode_stored_v2 path decodes every column into Vec<Value> and reconstructs the complete canonical stored JSON array even when a caller needs one document or a few fields.

On the deterministic 20,000-row probe, the canonical decoded representation is 86,832,275 bytes while the encoded section is 137,876 bytes. Full decode reached roughly 335–338 MB peak RSS. Hydrating one exact row used roughly 5.6–5.7 MB RSS.

The large reduction comes from retaining only selected values. RAW/zstd selection still streams through all rows, so this is a memory win rather than a latency win.

What changes

Selected row hydration

The new decoder accepts selected document ordinals, normalizes their ordering, removes duplicate ordinals, and returns exact selected stored documents without building the full stored corpus.

Top-level field projection

Callers may request selected top-level _source fields. Identity and sequence metadata remain available for exact document reconstruction and provenance.

Codec coverage

The implementation handles:

  • raw JSON columns;
  • LZ4 JSON columns;
  • constants;
  • dictionary plus bit-packed IDs;
  • cross-column dependencies;
  • typed vector and chunk representations;
  • V1/plain and full-decode compatibility fallback.

Controlled cancellation

Long row paths check cancellation every 128 consumed or produced rows. This includes invalid/nonfinite vector rows and repeated constant vectors/chunks.

Constant selected-row hydration uses fallible incremental allocation instead of eager vec![value; selected.len()]. Tests prove cancellation at output 128, normal parity, ordinal normalization, duplicate handling, and deterministic capacity failure.

Checked malformed-input behavior

Directory column counts are validated against minimum remaining framing before allocation. Header-sized vectors, sets, row outputs, constant expansion, dependency structures, dictionary entries, packed IDs, and exception lists use checked arithmetic and fallible reservation.

Unknown-size historical zstd frames remain readable through the compatibility path, but the retained-size upper-bound API returns None so a cache can skip predecode admission rather than guessing.

Conservative retained upper bound

stored_slices_retained_upper_bound inspects framing without performing full decode and returns a conservative bound when the encoded representation provides enough information. Callers can reserve or refuse before materializing a complete stored-slice cache entry.

Measured result

The committed release probe uses a deterministic 20,000-row finance-like fixture.

  • Fixture encoded size: 137,876 bytes.
  • Fixture SHA-256: 71aef08c71077cec61a2482a3d8089571007f5167e7fa6a0b1d89b6b6f25e76d.
  • All nine full, selected-row, and field-projected outputs have SHA-256 d505b2317c26eec30cf7ff40604d37a5b54986b652bb7348730c5d7ce5d2b35d.
Mode Median wall time Peak RSS range
Full decode 336.715 ms 334,647,296–337,702,912 bytes
One exact row 1,392.531 ms 5,636,096–5,742,592 bytes
One row, body only 1,360.279 ms 5,500,928–5,644,288 bytes

Selected hydration used roughly 59 times less peak RSS on this fixture but was roughly four times slower because RAW/zstd decoding still visits O(rows). The PR intentionally describes this as a bounded-memory fallback, not a query-latency improvement.

Why production zstd encoding is unchanged

The probe compared the existing streaming encoder with bulk zstd across repeated text, JSON numbers, and packed IDs.

  • Bulk output was 3–4 bytes larger in every measured shape.
  • Bulk was modestly faster on two shapes.
  • Bulk was materially slower on repeated text.

There was no universal win, so this branch retains zstd::encode_all. Current streaming frames may omit content size; the bound API fails closed for those frames.

Reproduction

From engine/:

cargo build --release -j 32 -p xerj-storage --example stored_projection_probe
probe=target/release/examples/stored_projection_probe
fixture=/workspace/.tmp/stored-decode-probe/finance-stream-final.zbs2
"$probe" generate "$fixture" 20000
sha256sum "$fixture"
for mode in full row projected; do
  for run in 1 2 3; do
    "$probe" "$mode" "$fixture" "/workspace/.tmp/stored-decode-probe/${mode}-stream-final-${run}.json"
  done
  sha256sum /workspace/.tmp/stored-decode-probe/${mode}-stream-final-*.json
done
for run in 1 2 3; do
  "$probe" zstd-compare 20
done

The raw outputs, environment, source identity, commands, and all hashes are committed in engine/reports/2026-07-27_bounded_stored_projection.md.

Verification

  • cargo test -p xerj-storage --lib: 122 passed / 0 failed;
  • cargo clippy -p xerj-storage --all-targets -- -D warnings: passed;
  • cargo fmt --all --check: passed;
  • diff check: passed;
  • current-main ancestry: passed;
  • independent Grok review of the final cancellation delta: approved.

Tradeoffs and non-claims

  • RAW/zstd selected reads still perform O(rows) work.
  • LZ4 selected reads decompress the complete column buffer before selection.
  • Cancellation cannot interrupt a single giant JSON cell or one decompressor call.
  • A selected giant value is not governed by a process-wide byte reservation in this storage-only API.
  • Current stream-zstd frames often provide no conservative predecode bound, so cache warming must skip rather than guess.
  • This branch does not change an engine route, server RSS, autoindex timing, retrieval quality, or FinanceBench results.

The dependent engine contribution routes exact point GET and publish-time predecode refusal through the shared process-wide cache budget. Semantic winner hydration and the 10K/FinanceBench end-to-end gates follow separately.

buger added 10 commits July 27, 2026 21:30
Exact kNN only needs the winning stored rows, but the canonical V2 decoder materializes every column value for every document. On vector-heavy segments that turns a small top-k result into O(segment rows) JSON allocation before source hydration.

Add a storage-only row-selective decoder for V2 sections. RAW JSON streams unselected rows through IgnoredAny, dictionary columns validate every packed id while materializing only selected entries, constants clone only selected values, and current DICT-backed CROSS_DEP columns reconstruct selected values. LZ4 remains selectively materialized after its required full-buffer decompression, which is reported explicitly.

Unsupported historical dependency shapes return a typed compatibility outcome only after their complete CrossDep payload is validated. Framing, JSON trailing data, bit widths, exact packed lengths, padding, ids, exception ordinals, and row selections are validated. Empty selections avoid payload decoding.

The exposed counters deliberately separate logical values from decompressed bytes; they are instrumentation dimensions and must not be summed as allocator memory.

Proof: 103 xerj-storage tests pass; targeted codec parity includes adaptive encoder output, RAW, LZ4, DICT, CONSTANT, and CROSS_DEP exceptions/nulls. A fixed three-row selection reports identical selected-value and output-clone work at 128 and 1024 rows. cargo clippy -p xerj-storage --all-targets -- -D warnings and package formatting pass.
Exact kNN projection and winner hydration previously exposed only synchronous
whole-operation APIs. A request deadline could be checked around a segment,
but not while RAW JSON rows, dictionary IDs, or CROSS_DEP exceptions were
being consumed. One large stored section could therefore overrun cancellation
by a complete multi-column decode.

Add controlled projection and hydration entry points with a distinct
`Cancelled` control outcome. Callers receive deterministic before-column,
128-row, and after-column checkpoints; cancellation is neither corruption nor
a compatibility fallback. Preserve the existing APIs as non-cancelling
compatibility wrappers.

Stream RAW typed vector and chunk rows directly from zstd through serde
visitors. Thread checkpoints through hydration JSON visitors, all-ID
dictionary validation, and CROSS_DEP exception scans. Identity columns reuse
the controlled row-codec path. Cancellation before a later corrupt column
drops partial state without observing unvisited corruption.

Adversarial tests place an unsupported or corrupt value at row 129 for pooled
vectors, chunk vectors, and hydration. Cancellation at the real row-128
visitor callback stops before row 129; a non-cancelling run reaches the
expected unsupported outcome or hard error.

Known limits are explicit. LZ decompression remains one indivisible buffer
before cancellable row parsing. Controlled identity decoding currently uses an
8*N-byte ordinal selector to reuse the proven selective codec. This change
therefore improves cancellation granularity but does not claim fully bounded
projection memory.

Validation:

- `cargo test -p xerj-storage`: 108 passed
- doc tests: 1 passed, 1 ignored
- `cargo clippy -p xerj-storage --all-targets -- -D warnings`
- `cargo fmt -p xerj-storage -- --check`
- `git diff --check`
ZBS2 previously exposed only a whole-section compatibility decoder. A caller selecting one row still had to materialize every column as Vec<Value>, rebuild every source object, and serialize the complete canonical JSON array.

Add cancellable selected-row and top-level field projection APIs, preserve identity/sequence metadata and legacy fallback, and harden malformed dictionary, packed-id, cross-dependency, framing, and varint handling. New zstd raw/dictionary/id frames carry content sizes so cache callers can conservatively preflight canonical bytes plus row offsets without decoding. Historical frames without sizes remain readable and fail closed only for admission estimates.

The on-disk version does not change: bulk and streaming compression both emit standard zstd frames consumed by the same decoder. Decoders accept historical unknown-size single frames, reject concatenated/trailing data, bound exact-size packed IDs, and bound cross-dependency expansion from format invariants.

A deterministic 20,000-row fixture encoded to 139,062 bytes and decoded to 86,832,275 bytes. The selected full-row bytes matched the compatibility decode (hash 7945261355329323226), as did the body-only projection (hash 17421003361823546720). Across three fresh-process runs, full decode median peak RSS was 334,942,208 bytes and sampled allocated memory 240,674,768 bytes; one-row hydration used 5,906,432 bytes RSS and 2,728,048 allocated; body-only projection used 5,718,016 bytes RSS and 453,488 allocated.

This is a memory primitive with an explicit CPU trade-off. Median wall time was 0.709 s full versus 2.016 s one-row and 2.050 s projected because RAW_JSON still streams O(rows). It is not yet wired into semantic-search late hydration or the shared CacheResident budget.

Tests cover every codec, sparse/nested sources, ordinals and field projections, full-decode parity, cancellation, malformed lengths/varints/dependencies/cardinality, unknown and concatenated zstd frames, decompression bounds, and conservative V1/V2/plain retained estimates.
Make cancellation checkpoints cover every consumed typed-vector row, including unsupported and non-finite values. Decode constant vector and chunk columns through controlled row iteration rather than allocating repeated rows eagerly.

Reject impossible V2 directory cardinalities before allocation and use fallible reserves for directory metadata and identity ordinals. Add adversarial cancellation and malformed-header coverage plus a reproducible SHA-256-friendly projection probe.

Keep the production zstd encoder unchanged: measured bulk compression was shape-dependent, slightly larger, and materially slower for repeated text. Existing frames without content sizes continue to fail closed for retained-size admission.
Record the exact source commit, environment, commands, immutable fixture and output SHA-256 digests, and all three raw runs for full and selective decode.

Include a three-shape encode_all-versus-bulk zstd comparison. The results support deferring production frame changes until an end-to-end cache-admission consumer exists, while preserving the RAW O(N), LZ4 whole-decompression, and giant-cell caveats.
Complete the num_docs allocation audit by replacing infallible full-row, constant-column, cross-dependency row, and exception allocations with checked reserves. This keeps malformed or resource-exhausting stored headers on the error path instead of an allocator panic path.
Point the reproducibility report at the final allocation-audited implementation commit. The probe and encoded output are unchanged by the fallible-reserve hardening.
The bounded stored-projection history was rewritten onto upstream main 16d6df0, but its reproducibility report still named the pre-rebase implementation and base commits. That made the measurements look detached from the reviewed source even though the probe file, fixture hash, output hash, and behavior were unchanged.

Point the report at rewritten implementation commit 0d28f07453e206516c7390c019c1dec45c7d52be and upstream base 16d6df0. A fresh 20,000-row run reproduced fixture SHA-256 71aef08c71077cec61a2482a3d8089571007f5167e7fa6a0b1d89b6b6f25e76d and output SHA-256 d505b2317c26eec30cf7ff40604d37a5b54986b652bb7348730c5d7ce5d2b35d for all nine full, row, and projected outputs.
Selective V2 row hydration cloned an entire selected constant column with vec![value; len]. Unlike RAW, LZ4, DICT, CROSS_DEP, and typed-vector paths, that eager clone had no 128-row control point and used an infallible capacity allocation. A large constant selection could therefore continue allocating and cloning after its caller requested cancellation.

Build constant outputs incrementally after try_reserve_exact, report allocation failure with the requested count, and run the common Rows checkpoint every 128 emitted values. Cancellation now returns the same distinct StoredDecodeRun::Cancelled outcome as the other selective codecs.

Add a 256-row constant fixture proving cancellation at row 128, non-cancelling parity and existing ordinal normalization, exact constant payloads, and deterministic capacity-overflow handling. Full xerj-storage tests pass 122/122.
Update the bounded stored-projection report to name the final implementation commit after constant-column hydration gained fallible allocation and cooperative cancellation. The committed probe itself and its deterministic fixture/output hashes are unchanged.

Record the expanded storage gate at 122 passed, zero failed, zero ignored.
@xerj-org
xerj-org merged commit ac74db8 into xerj-org:main Jul 27, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants