Skip to content

perf: batch multi_get + PinnableSlice + WriteBatch#214

Merged
polaz merged 46 commits into
mainfrom
feat/#143-perf-readwrite-path-optimization--batch-multiget
Apr 6, 2026
Merged

perf: batch multi_get + PinnableSlice + WriteBatch#214
polaz merged 46 commits into
mainfrom
feat/#143-perf-readwrite-path-optimization--batch-multiget

Conversation

@polaz
Copy link
Copy Markdown
Member

@polaz polaz commented Apr 5, 2026

Summary

  • Batch multi_get: deferred sort+hash after memtable phase, sorted key access for SSTs, L0 seqno ceiling skip, BlobTree batch path with RT suppression
  • PinnableSlice: zero-copy get_pinned() API — Pinned variant keeps decompressed block buffer alive via refcounted Slice/ByteView, Owned for memtable/merge/blob values
  • WriteBatch: apply_batch(batch, seqno) -> Result<(u64, u64)> — shared seqno, single lock, Error::MixedOperationBatch rejects mixed-op duplicates unconditionally
  • Table::get seqno fix: returns global seqnos (was table-local for ingested tables)
  • Deps: update `structured-zstd 0.0.1 → 0.0.7

Technical Details

Batch multi_get

  • Phase 1 (memtable): unsorted scan — memtable lookup is O(log n) regardless of order
  • Phase 2 (SST): sort remaining keys + compute bloom hashes only if memtable misses exist (memtable-only batches skip entirely)
  • L0: Vec bitmap tracks keys at seqno ceiling (entry_seqno + 1 == read_seqno), skips them in subsequent runs
  • L1+: sorted key access for sequential I/O
  • BlobTree: batch path with sorted keys, bloom hashes, RT suppression (was naive per-key loop)
  • Small batches (≤2 keys) use simple per-key path
  • Monomorphized find_in_tables<T: TablePointLookup> — zero-cost generic for get (no Block overhead) and get_pinned (retains Block)

PinnableSlice

  • Pinned { _block: Block, value: Slice } — block buffer alive via refcount, does NOT prevent cache eviction
  • resolve_pinned_entry helper — single source of truth for tombstone/RT/merge resolution, used by both get_pinned and multi_get
  • Table::point_read_inner — shared block-index walk for point_read and point_read_with_block
  • BloomResult enum — DRY filter-loading with cfg(metrics) gating

WriteBatch

  • apply_batch returns ResultError::MixedOperationBatch if same user key has differing op types (insert + remove)
  • Repeated merge() on same key is safe (same value_type)
  • Memtable::insert_batch — single saturating_add for total size
  • Version-history read guard held for entire batch to prevent rotate_memtable race

Table::get global seqno

  • Table::get and get_with_block now add global_seqno back to returned InternalValue seqno
  • Fixes L0 best-selection and RT suppression for bulk-ingested tables with non-zero global_seqno
  • L0 fast-path: checked_add(1) == Some(seqno) (was dead code with == seqno)

Test Plan

  • 1229 tests pass (cargo nextest run --all-features)
  • 41 doc-tests pass (cargo test --doc)
  • Clippy clean (cargo clippy --all-targets --all-features -- -D warnings)
  • Criterion benchmarks: multi_get (10-500 keys), get_pinned vs get, write_batch vs individual inserts
  • MixedOperationBatch rejection test + repeated merge acceptance test

Related

Closes #143

Summary by CodeRabbit

  • New Features

    • Zero-copy pinned reads and a pinned-read API; WriteBatch for grouped atomic writes and a bulk memtable insert path.
  • Performance Improvements

    • Batched multi-get pipeline and optimized table point-lookup for large requests.
  • Bug Fixes

    • Rejected batches that mix conflicting operations on the same key (MixedOperationBatch).
  • Documentation

    • README clarifies batch-optimized multi_get, pinning, and atomic batch visibility.
  • Tests / Benchmarks

    • New integration tests and Criterion benchmarks for pinned reads, multi_get, and write batches.
  • Chores

    • Dependency version bumped and benchmark target added.

Phase 1 — Batch multi_get optimization:
- Pre-compute bloom filter hashes once for all keys
- Sort keys by comparator order for sequential I/O
- Check memtables first (active + sealed), batch remaining
- Batch table lookups through L0-L6 with sorted key access
- Small batches (≤2 keys) use simple per-key path

Phase 2 — PinnableSlice zero-copy reads:
- New PinnableSlice enum: Pinned (block cache) | Owned (memtable)
- New get_pinned() API on AbstractTree trait
- Pinned variant holds Arc<Block> reference preventing eviction
- Values from ByteView::slice() are already zero-copy sub-views
- Default trait impl wraps get() as Owned for BlobTree compat
- Tree overrides with full pinned path through tables

Phase 3 — WriteBatch atomic writes:
- New WriteBatch struct: insert/remove/remove_weak/merge ops
- apply_batch() on AbstractTree: single seqno for atomicity
- Memtable::insert_batch(): single fetch_add + single fetch_max
- Single version-history lock acquisition for entire batch

Dependencies:
- Update structured-zstd 0.0.1 → 0.0.6

Closes #143
Copilot AI review requested due to automatic review settings April 5, 2026 13:58
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 5, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cbbd7d7d-b579-4413-874e-a07f35545fa1

📥 Commits

Reviewing files that changed from the base of the PR and between 6e025fb and 05707d9.

📒 Files selected for processing (2)
  • src/error.rs
  • src/write_batch.rs

📝 Walkthrough

Walkthrough

Adds batched multi_get with bloom-batched SST probes, a PinnableSlice zero-copy read type and get_pinned API, a WriteBatch bulk-write API plus memtable bulk insert, table bloom/point-read refactors exposing block-backed reads, new tests and Criterion benchmarks, README tweak, and bumps optional structured-zstd.

Changes

Cohort / File(s) Summary
Dependency & Benchmarks
Cargo.toml, benches/batch_ops.rs, README.md
Bumped optional structured-zstd 0.0.1 → 0.0.7; added benches/batch_ops.rs and registered batch_ops benchmark; README updated to mention multi_get (batch-optimized), PinnableSlice, and WriteBatch.
Core traits & re-exports
src/abstract_tree.rs, src/lib.rs
Extended AbstractTree with get_pinned(...) -> Result<Option<PinnableSlice>> (default delegates to get) and required apply_batch(batch, seqno) -> Result<(u64,u64)>; crate root re-exports PinnableSlice and WriteBatch.
Pinned value type
src/pinnable_slice.rs
Added PinnableSlice enum with Pinned{_block, value} and Owned(UserValue) variants plus constructors, accessors, conversions, trait impls, and Clone.
Batch write API & error
src/write_batch.rs, src/error.rs
New WriteBatch type (insert/remove/remove_weak/merge), materialize(seqno) producing Vec<InternalValue> with validation rejecting mixed operation kinds per key via new Error::MixedOperationBatch.
Memtable bulk path
src/memtable/mod.rs
Added Memtable::insert_batch(items) -> (u64,u64) with empty fast-path, aggregated sizing, single atomic fetch_add, batched skiplist inserts, and highest-seqno update.
Tree: multi_get, pinned reads, batch apply
src/tree/mod.rs, src/blob_tree/mod.rs
Rewrote multi_get into a 3‑phase batched pipeline (memtable probe, sort remaining keys, batch table probe); added get_pinned, shared pinned-resolution helpers, append_batch and apply_batch; BlobTree delegates apply_batch and retains small-batch special-casing.
Table / block & bloom refactor
src/table/mod.rs
Introduced BloomResult + check_bloom; factored bloom/metrics out of point-read; added get_with_block(...)/point-read-with-block support and adjusted filter-metric incrementing logic.
Tests & benches
tests/*, benches/batch_ops.rs
Added integration tests for get_pinned/PinnableSlice, WriteBatch end-to-end tests, expanded multi_get tests, and benchmarks for multi_get, get_pinned, and write-batch performance.
Docs & CI guidance
.github/copilot-instructions.md
Documentation guidance added clarifying unit-structs are usable as values (doc-only).

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Tree as Tree
    participant Memtable as Memtable
    participant SST as Table/SST
    participant BlockCache as BlockCache

    Client->>Tree: get_pinned(key, seqno)
    Tree->>Memtable: probe active & sealed memtables
    alt Found in memtable
        Memtable-->>Tree: InternalValue (owned)
        Tree-->>Client: PinnableSlice::Owned
    else Not in memtable
        Tree->>SST: batched bloom checks & table probe (get_with_block)
        SST->>BlockCache: load & decompress block
        BlockCache-->>Tree: (InternalValue, Block)
        Tree-->>Client: PinnableSlice::Pinned
    end
Loading
sequenceDiagram
    participant Client as Client
    participant Batch as WriteBatch
    participant Tree as Tree
    participant MemtableInst as Memtable

    Client->>Batch: build batch (insert/remove/merge)
    Client->>Tree: apply_batch(batch, seqno)
    Tree->>Batch: materialize(seqno) -> Vec<InternalValue>
    Tree->>MemtableInst: insert_batch(items)
    MemtableInst-->>Tree: (bytes_added, new_size)
    Tree-->>Client: (bytes_added, new_size)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #143: perf: read/write path optimization — batch multi_get + PinnableSlice + batch insert — Implements the three objectives (batched multi_get, PinnableSlice/get_pinned, and batch insert API) described in the issue.
  • structured-world/coordinode-lsm-tree#223: Adds/requests a per-table batch point-read pipeline similar to this PR’s batch_get_from_tables / pinned table lookup helpers.

Possibly related PRs

  • structured-world/coordinode-lsm-tree#199 — Overlaps on table/block point-read refactors and error propagation; may conflict with get_with_block/point-read changes.
  • structured-world/coordinode-lsm-tree#175 — Touches benchmarks/Cargo bench targets and may overlap on bench files and Cargo.toml bench registration.
  • structured-world/coordinode-lsm-tree#166 — Also modifies src/error.rs enum variants and may conflict on Error additions.

Poem

🐇
I hop through blocks and cradle bytes so light,
I batch my tiny steps and make reads swift and bright,
I pin a warm block or hold an owned delight,
I bundle writes and march them through the night,
Small hops, big gains — the code-hare's bite.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'perf: batch multi_get + PinnableSlice + WriteBatch' accurately and concisely summarizes the three main performance-focused features introduced in the changeset.
Linked Issues check ✅ Passed All coding objectives from issue #143 are met: batch multi_get with memtable-first scan and sorted SST scanning (src/tree/mod.rs, src/blob_tree/mod.rs), PinnableSlice zero-copy API (src/pinnable_slice.rs, src/abstract_tree.rs, src/tree/mod.rs), and WriteBatch batch-insert API (src/write_batch.rs, src/memtable/mod.rs). Tests and benchmarks validate all three features comprehensively.
Out of Scope Changes check ✅ Passed All changes directly support the three core objectives: batch multi_get implementation, PinnableSlice zero-copy reads, and WriteBatch bulk inserts. Supporting infrastructure (BloomResult enum, TablePointLookup trait, error variant, benchmarks, tests) are necessary for these features and remain focused on the stated scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#143-perf-readwrite-path-optimization--batch-multiget

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 5, 2026

Codecov Report

❌ Patch coverage is 93.88715% with 39 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/blob_tree/mod.rs 82.35% 15 Missing ⚠️
src/tree/mod.rs 95.16% 15 Missing ⚠️
src/memtable/mod.rs 86.66% 4 Missing ⚠️
src/pinnable_slice.rs 93.47% 3 Missing ⚠️
src/table/mod.rs 97.72% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces three major read/write-path optimizations for the LSM tree: (1) a batch-optimized multi_get() to reduce bloom/hash work and improve I/O locality, (2) a get_pinned() API via PinnableSlice to support zero-copy-ish value access for table reads, and (3) an atomic WriteBatch API to apply multiple writes under a shared seqno with reduced locking/accounting overhead.

Changes:

  • Implement batch multi_get() with key sorting, precomputed bloom hashes, memtable-first filtering, and batched SST lookups.
  • Add PinnableSlice + AbstractTree::get_pinned() and table APIs to return a value along with its containing block.
  • Add WriteBatch + AbstractTree::apply_batch() and memtable bulk insert (insert_batch) for more efficient batched writes; bump structured-zstd to 0.0.6.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/write_batch.rs Adds coverage for batch insert/remove semantics and flush survivability.
tests/pinnable_slice.rs Adds coverage for get_pinned() behavior across memtable vs disk and basic PinnableSlice APIs.
src/write_batch.rs Implements WriteBatch builder + materialization into InternalValues.
src/tree/mod.rs Implements get_pinned(), rewrites multi_get() to a batched path, and adds append_batch().
src/table/mod.rs Adds get_with_block() / point_read_with_block() to return the block alongside point-read results.
src/pinnable_slice.rs Introduces PinnableSlice enum and its public API surface.
src/memtable/mod.rs Adds Memtable::insert_batch() with single-pass size/seqno accounting.
src/lib.rs Wires new modules and re-exports PinnableSlice and WriteBatch.
src/blob_tree/mod.rs Implements apply_batch() for BlobTree by delegating to the index tree.
src/abstract_tree.rs Extends the trait with get_pinned() (default impl) and required apply_batch().
README.md Updates feature list to mention batch multi_get, PinnableSlice, and WriteBatch.
Cargo.toml Updates structured-zstd dependency version to 0.0.6.

Comment thread src/pinnable_slice.rs Outdated
Comment thread src/memtable/mod.rs
Comment thread src/tree/mod.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Cargo.toml`:
- Line 38: The Cargo.toml dependency line for structured-zstd references a
non-existent version (0.0.6); update the structured-zstd dependency entry to use
a published version (e.g., set version = "0.0.3") so the crate resolves
correctly—look for the structured-zstd dependency declaration in Cargo.toml and
change its version value accordingly.

In `@src/memtable/mod.rs`:
- Around line 230-233: The loop currently consumes items with `for item in
items` which prevents reuse; change it to iterate by reference (e.g., `for item
in &items` or `for item in items.iter()`) in the block where you build
`InternalKey` and call `self.items.insert(&key, &item.value)` so you don't move
out of `items`; adjust any field accesses (e.g., `item.key` -> `&item.key` or
use references) or clone only the minimal fields if needed to satisfy ownership
for `InternalKey::new` and `self.items.insert` while keeping `items` intact for
later use.

In `@src/table/mod.rs`:
- Around line 325-409: get_with_block duplicates the filter-loading and
bloom-check logic present in get; extract that shared logic into a private
helper (suggest name check_bloom_filter or load_filter_block) which performs the
pinned/index/unpinned resolution using load_block + FilterBlock::new and returns
Option<Cow<'_, FilterBlock>> or a bool signalling whether to proceed; replace
the duplicated block in both get() and get_with_block() with a call to this
helper and ensure metrics updates and maybe_contains_hash calls are moved into
the helper while get_with_block continues to call
point_read_with_block/point_read as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 95ee76a8-d157-4e3d-b0b5-1fa4a8c9f32a

📥 Commits

Reviewing files that changed from the base of the PR and between cf5d5bd and fcedd42.

📒 Files selected for processing (12)
  • Cargo.toml
  • README.md
  • src/abstract_tree.rs
  • src/blob_tree/mod.rs
  • src/lib.rs
  • src/memtable/mod.rs
  • src/pinnable_slice.rs
  • src/table/mod.rs
  • src/tree/mod.rs
  • src/write_batch.rs
  • tests/pinnable_slice.rs
  • tests/write_batch.rs

Comment thread Cargo.toml Outdated
Comment thread src/memtable/mod.rs
Comment thread src/table/mod.rs
polaz added 2 commits April 5, 2026 17:17
WriteBatch:
- blob tree KV separation roundtrip
- merge operand concatenation
- remove_weak tombstone
- multi_get after batch spanning disk + memtable

PinnableSlice:
- blob tree returns Owned (not Pinned)
- range tombstone suppression
- after compaction returns Pinned (L1+ data)
- sealed memtable returns Owned

multi_get:
- range tombstone suppression
- multi-level (memtable + L0 + L1) with shadowing
- large batch (500 keys) reverse order from disk
- Extract check_bloom() in Table to DRY filter-loading logic
  shared between get() and get_with_block()
- Use saturating_add for batch size accumulation in insert_batch
- Drop version_history read lock before insert_batch to reduce
  lock contention during large batches
- Reword PinnableSlice docs: Block keeps buffer alive via
  refcounted Slice, does not prevent cache eviction
- Add integration tests: WriteBatch with blob tree, merge ops,
  remove_weak; PinnableSlice with blob tree, range tombstones,
  compaction, sealed memtable; multi_get with range tombstones,
  multi-level shadowing, 500-key reverse-order batch
Copy link
Copy Markdown

@sw-release-bot sw-release-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'lsm-tree db_bench'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.15.

Benchmark suite Current: a2c1f56 Previous: cf5d5bd Ratio
fillseq 839387.4601232084 ops/sec (normalized) 1148537.5894635925 ops/sec (normalized) 1.37
fillrandom 463627.8850158264 ops/sec (normalized) 663695.9309610558 ops/sec (normalized) 1.43
readrandom 239327.4587837826 ops/sec (normalized) 311914.1436192461 ops/sec (normalized) 1.30
seekrandom 170547.86670630827 ops/sec (normalized) 212174.6412340675 ops/sec (normalized) 1.24
overwrite 481982.997678611 ops/sec (normalized) 687074.7425792207 ops/sec (normalized) 1.43
mergerandom 355798.78013906063 ops/sec (normalized) 414073.5132640846 ops/sec (normalized) 1.16
readwhilewriting 196864.7817348204 ops/sec (normalized) 283433.1104358807 ops/sec (normalized) 1.44

This comment was automatically generated by workflow using github-action-benchmark.

CC: @polaz

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.

Comment thread src/tree/mod.rs Outdated
Comment thread src/pinnable_slice.rs
Comment thread src/table/mod.rs Outdated
Comment thread src/abstract_tree.rs
Comment thread src/abstract_tree.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/table/mod.rs (1)

245-260: ⚠️ Potential issue | 🟡 Minor

Remove the seqno parameter inconsistency or document it.

Line 247 passes iter.seek(key, seqno) while line 1068 uses iter.seek(key, crate::seqno::MAX_SEQNO). However, both paths are functionally equivalent because all filter partitions are written with hardcoded seqno=0, making the seqno parameter irrelevant to partition selection. For clarity, unify these calls to use MAX_SEQNO consistently (or document why snapshot semantics apply here).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/table/mod.rs` around lines 245 - 260, The call to iter.seek in the pinned
filter path uses a dynamic seqno (iter.seek(key, seqno)) while elsewhere the
code uses crate::seqno::MAX_SEQNO; unify these to avoid confusion by changing
the pinned_filter_index path to call iter.seek(key, crate::seqno::MAX_SEQNO)
(or, if you prefer not to change behavior, add a comment in the
pinned_filter_index block explaining that filter partitions are written with
seqno=0 and seqno is irrelevant so MAX_SEQNO is used for snapshot semantics);
update references in the pinned_filter_index handling around iter.seek,
filter_idx.iter, and related materialize/seek logic to reflect the unified
usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/tree/mod.rs`:
- Around line 1733-1745: append_batch currently clones active_memtable and drops
the version_history read guard before calling insert_batch, allowing
rotate_memtable to seal the memtable mid-batch; change append_batch to hold the
version_history read guard for the duration of the insert by keeping the guard
value (the RwLockReadGuard from self.version_history.read()) alive while calling
insert_batch on the cloned active_memtable (mirror the pattern used in
append_entry), so that the read lock prevents rotate_memtable from moving the
memtable into sealed_memtables until the batch insert finishes.

---

Outside diff comments:
In `@src/table/mod.rs`:
- Around line 245-260: The call to iter.seek in the pinned filter path uses a
dynamic seqno (iter.seek(key, seqno)) while elsewhere the code uses
crate::seqno::MAX_SEQNO; unify these to avoid confusion by changing the
pinned_filter_index path to call iter.seek(key, crate::seqno::MAX_SEQNO) (or, if
you prefer not to change behavior, add a comment in the pinned_filter_index
block explaining that filter partitions are written with seqno=0 and seqno is
irrelevant so MAX_SEQNO is used for snapshot semantics); update references in
the pinned_filter_index handling around iter.seek, filter_idx.iter, and related
materialize/seek logic to reflect the unified usage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6c045502-6822-4165-9573-4f9e6e935b3b

📥 Commits

Reviewing files that changed from the base of the PR and between fcedd42 and 6ef0bbe.

📒 Files selected for processing (7)
  • src/memtable/mod.rs
  • src/pinnable_slice.rs
  • src/table/mod.rs
  • src/tree/mod.rs
  • tests/multi_get.rs
  • tests/pinnable_slice.rs
  • tests/write_batch.rs

Comment thread src/tree/mod.rs Outdated
- Hold version_history read guard during append_batch to prevent
  rotate_memtable from sealing mid-batch (data loss race)
- Reword PinnableSlice field/method docs: block keeps buffer alive
  via refcount, does not prevent cache eviction
- Fix get_pinned/apply_batch doctests: pass &folder not folder
- Unify filter partition seek to MAX_SEQNO (seqno is irrelevant
  for partition selection, all partitions written with seqno=0)
- Add code comment explaining L0 seqno fast-path in pinned path
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pinnable_slice.rs`:
- Around line 24-33: The docs currently describe the `Pinned` variant as a
"block cache" signal which implies a cache hit; update the comments for
`PinnableSlice` (including the `Pinned` variant description and any doc blocks
around `is_pinned()` and `PinnableSlice::as_ref`) to instead explain that
`Pinned` means the slice shares/pins the underlying decompressed block buffer
(holds a `Block` clone with a refcounted `Slice`) and therefore keeps that
buffer alive for the `PinnableSlice` lifetime; mention `Tree::get_pinned()` and
`Table::get_with_block()` semantics so callers understand `Pinned` can be
created even when the block was just loaded from disk, and ensure the wording
focuses on sharing/pinning and lifetime/refcount behavior rather than cache-hit
semantics.

In `@src/table/mod.rs`:
- Around line 245-265: The code path that checks pinned_filter_index currently
falls through when iter.next() returns None, causing an unnecessary point lookup
and clearing has_filter; instead treat a missing filter partition as a definite
miss: inside bloom_may_contain_key (the function containing the
pinned_filter_index handling), after iter.seek(...) if iter.next() is None, set
has_filter true (so filter metrics are recorded) and immediately return
Ok(false) to short-circuit the lookup; apply the same change to the second
identical pinned_filter_index branch elsewhere in this file.

In `@src/tree/mod.rs`:
- Around line 1730-1749: append_batch currently calls Memtable::insert_batch
which inserts entries one-by-one and can be observed partially by concurrent
get(seqno) readers; fix by making the batch atomic: in append_batch /
Memtable::insert_batch add a publish barrier (e.g., write all items into a
private buffer or mark them as uncommitted, then atomically flip a committed
flag or advance a visible_seqno) so readers (get) only see the new entries after
the commit flip; locate append_batch, Memtable::insert_batch, get, and any
seqno/visible_seqno state and implement the two-phase insert+commit (or document
that callers must call apply_batch/publish after append_batch) so a racing get
cannot observe a prefix of the batch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3c6dc9ab-4a09-47db-9a88-4c0314188e6d

📥 Commits

Reviewing files that changed from the base of the PR and between 6ef0bbe and c34bca7.

📒 Files selected for processing (4)
  • src/abstract_tree.rs
  • src/pinnable_slice.rs
  • src/table/mod.rs
  • src/tree/mod.rs

Comment thread src/pinnable_slice.rs Outdated
Comment thread src/table/mod.rs
Comment thread src/tree/mod.rs
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/table/mod.rs:266

  • In check_bloom, when pinned_filter_index is present and iter.next() returns None (key is beyond all partition end-keys), the key cannot exist in this table (same logic as bloom_may_contain_key). Currently this falls through with filter_block = None, causing get()/get_with_block() to proceed to point_read and also report has_filter = false. Consider short-circuiting to (should_proceed = false, has_filter = true) in this case, and computing has_filter based on filter availability (index/full filter), not only on whether a filter block was loaded.
    fn check_bloom(&self, key: &[u8], key_hash: u64) -> crate::Result<(bool, bool)> {
        // Returns (should_proceed, has_filter)
        let filter_block = if let Some(block) = &self.pinned_filter_block {
            Some(Cow::Borrowed(block))
        } else if let Some(filter_idx) = &self.pinned_filter_index {
            let mut iter = filter_idx.iter(self.comparator.clone());
            // Filter partitions are written with seqno=0, making the seqno
            // parameter irrelevant to partition selection. Use MAX_SEQNO
            // consistently to match the index-block seek in Table::range().
            iter.seek(key, crate::seqno::MAX_SEQNO);

            if let Some(filter_block_handle) = iter.next() {
                let filter_block_handle = filter_block_handle.materialize(filter_idx.as_slice());

                let block = self.load_block(
                    &filter_block_handle.into_inner(),
                    BlockType::Filter,
                    CompressionType::None,
                    #[cfg(zstd_any)]
                    None,
                )?;
                Some(Cow::Owned(FilterBlock::new(block)))
            } else {
                None
            }
        } else if let Some(_filter_tli_handle) = &self.regions.filter_tli {

- Remove all "block cache" wording from PinnableSlice — Pinned means
  sharing decompressed block buffer, not a cache-hit indicator
- Return (false, true) when filter partition seek yields None — key
  sorts past last partition, definite miss instead of fallthrough
- Clarify WriteBatch visibility contract: entries visible one-at-a-time,
  caller must publish seqno after apply_batch for atomic visibility
Comment thread src/tree/mod.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/abstract_tree.rs (1)

559-563: ⚠️ Potential issue | 🟡 Minor

Clarify Pinned docs to avoid cache-hit implication.

Line 559 still frames PinnableSlice::Pinned as tied to “found in the block cache,” which can be read as a cache-hit signal. Prefer wording that focuses on shared backing-memory lifetime.

✏️ Suggested doc tweak
-    /// When the value is found in the block cache, the returned
-    /// [`PinnableSlice::Pinned`] variant holds a reference to the cached
-    /// block, avoiding a data copy. Memtable and blob-resolved values use
+    /// When the value is backed by an on-disk data block, the returned
+    /// [`PinnableSlice::Pinned`] variant holds a reference to that block's
+    /// decompressed buffer, avoiding a data copy. Memtable and blob-resolved values use
     /// the [`PinnableSlice::Owned`] variant.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/abstract_tree.rs` around lines 559 - 563, The docs for
PinnableSlice::Pinned currently imply it signals a block-cache hit; update the
comment to remove any cache-hit implication and instead describe that
PinnableSlice::Pinned indicates the returned slice shares backing memory whose
lifetime is tied to the pinned handle (i.e., no data copy), whereas
PinnableSlice::Owned contains an owned copy (used for memtable/blob-resolved
values). Edit the comment surrounding PinnableSlice::Pinned in abstract_tree.rs
to emphasize shared backing-memory lifetime and ownership semantics rather than
referencing a cache hit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/write_batch.rs`:
- Line 40: The documentation for WriteBatch incorrectly says batches are
“applied atomically”; update the doc comment on the WriteBatch type (the
WriteBatch struct/docs) to state that entries are applied with a shared seqno
but become visible one-by-one unless the caller publishes visibility (e.g., via
visible_seqno.fetch_max(seqno + 1)) and that atomic visibility must be performed
by the caller after apply_batch/Tree::append_batch. Mention
Tree::append_batch/apply_batch as the locations that intentionally do not
provide internal visibility gating.

---

Duplicate comments:
In `@src/abstract_tree.rs`:
- Around line 559-563: The docs for PinnableSlice::Pinned currently imply it
signals a block-cache hit; update the comment to remove any cache-hit
implication and instead describe that PinnableSlice::Pinned indicates the
returned slice shares backing memory whose lifetime is tied to the pinned handle
(i.e., no data copy), whereas PinnableSlice::Owned contains an owned copy (used
for memtable/blob-resolved values). Edit the comment surrounding
PinnableSlice::Pinned in abstract_tree.rs to emphasize shared backing-memory
lifetime and ownership semantics rather than referencing a cache hit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9198c93b-2c6b-4d79-8d90-15ed451c784a

📥 Commits

Reviewing files that changed from the base of the PR and between c34bca7 and 06e216e.

📒 Files selected for processing (4)
  • src/abstract_tree.rs
  • src/pinnable_slice.rs
  • src/table/mod.rs
  • src/write_batch.rs

Comment thread src/write_batch.rs Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Comment thread src/tree/mod.rs Outdated
Comment thread src/write_batch.rs Outdated
polaz added 2 commits April 5, 2026 18:46
refactor(table): extract BloomResult enum from check_bloom

- PinnableSlice Pinned variant: len, is_empty, value, as_ref, clone,
  into_value, From impl, PartialEq, Debug — all exercised on disk values
- PinnableSlice: empty value, tombstone on disk, RT suppression of disk
  values, merge operands in memtable/sealed/disk paths
- multi_get: merge operand resolution (memtable + disk), L0 tombstones
- BloomResult enum replaces (bool, bool) tuple — cleanly gates
  has_filter behind cfg(metrics) without underscore prefix hacks
…ract

- WriteBatch: remove "atomically" from struct doc (visibility is
  per-entry, not atomic without caller seqno publish)
- get_pinned: replace "block cache" with "on-disk data block" in
  AbstractTree trait doc
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

@polaz polaz requested a review from Copilot April 6, 2026 10:24
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 6, 2026

@polaz Sure! Resuming automatic reviews and kicking off a full review of PR #214 now.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 214
File: src/tree/mod.rs:1047-1065
Timestamp: 2026-04-05T17:27:55.945Z
Learning: In `src/table/mod.rs` (structured-world/coordinode-lsm-tree), `Table::get` and `Table::get_with_block` both convert table-local seqnos back to global seqno coordinates before returning `InternalValue` (fixed in PR `#214`, commit 431e9759, lines 346-351). All callers — including `get_internal_entry_from_tables`, `get_internal_entry_with_block_from_tables`, and `batch_get_from_tables` — therefore receive already-normalized global seqnos. Do not re-flag the absence of caller-side seqno normalization for these APIs in future reviews of PR `#214` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 214
File: src/tree/mod.rs:845-875
Timestamp: 2026-04-05T19:15:56.939Z
Learning: In `src/tree/mod.rs` (structured-world/coordinode-lsm-tree, PR `#214`), `multi_get` Phase 1 cannot pre-prune keys by active/sealed memtable range tombstones before SST lookup. RT suppression requires `key_seqno < rt.seqno`, but the entry seqno for disk-resident keys is not known until Phase 2 (SST lookup via `batch_get_from_tables`) completes. Pre-pruning would risk incorrectly suppressing valid entries. RT suppression is correctly and safely applied in Phase 3 via `resolve_entry` → `is_suppressed_by_range_tombstones` after the entry seqno is available. Do not re-raise early RT pre-pruning in `multi_get` Phase 1 in future reviews of PR `#214` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 214
File: src/tree/mod.rs:1754-1773
Timestamp: 2026-04-05T23:36:17.125Z
Learning: In `src/memtable/mod.rs` (structured-world/coordinode-lsm-tree, PR `#214`), `Memtable::insert_batch` returns `(total_size, size_before + total_size)` where `size_before` is the pre-add value from `fetch_add` (which returns before-add semantics). The second element is therefore the post-insert memtable size. This matches the `Memtable::insert()` pattern. Do not flag `append_batch` forwarding `insert_batch` as returning a pre-insert size — the returned tuple already contains the post-insert size.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 214
File: src/tree/mod.rs:1730-1749
Timestamp: 2026-04-05T15:32:09.162Z
Learning: In `src/tree/mod.rs` (structured-world/coordinode-lsm-tree), `Tree::append_batch` / `apply_batch` inserts entries into the memtable one-at-a-time and does NOT provide internal atomic visibility. Atomic batch visibility is a caller responsibility: after `apply_batch` returns, the caller must call `visible_seqno.fetch_max(seqno + 1)` to gate readers. This contract was documented in PR `#214` (WriteBatch and apply_batch docs). Do not flag the absence of an internal commit barrier in `append_batch` / `Memtable::insert_batch` in future reviews — visibility is intentionally delegated to the caller's seqno management.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T20:34:35.867Z
Learning: In `src/vlog/mod.rs` (structured-world/coordinode-lsm-tree), `recover_blob_files` defers all `insert_for_blob_file` calls into a `pending_cache_inserts` buffer and commits them only after the missing-file guard (`blob_files.len() < ids.len()`) passes. This was fixed in PR `#208` to prevent leaking FDs into the descriptor-table cache on partial/failed recovery. Do not flag the absence of this deferral as a gap in future reviews of PR `#208` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 214
File: src/write_batch.rs:77-103
Timestamp: 2026-04-05T16:20:39.105Z
Learning: In `src/write_batch.rs` (structured-world/coordinode-lsm-tree PR `#214`), `WriteBatch` deliberately allows duplicate keys but documents that all entries share the same seqno, so ordering for repeated keys is determined by skiplist ordering on `(user_key, seqno, value_type)` — not by insertion order. The maintainer chose documentation-only guidance over API-level enforcement or canonicalization. Do not re-raise the duplicate-key ordering concern as a blocking issue in future reviews of PR `#214` or later code; at most suggest a follow-up debug_assert in `materialize`.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-30T22:48:06.079Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), the "binary search trusts semantically-corrupted binary index" concern (lines 245-299) is explicitly out of scope for PR `#182`. The scenario requires a corrupted binary index that passes the block checksum (encoder bug or checksum collision). Adding O(n) monotonicity validation per seek call would regress the hot path for a case already caught by checksums. This hardening is tracked in issue `#184` (fallible decoder constructor). Do not flag the absence of monotonicity validation on the binary-index seek path in reviews of PR `#182` or earlier code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 182
File: src/table/block/decoder.rs:236-243
Timestamp: 2026-03-30T17:47:54.258Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), `get_key_at` calls `Item::parse_full` on every binary-search probe inside `partition_point` / `partition_point_2` as a result of the bounds-hardening in PR `#182` (which replaced the previous unsafe `get_unchecked` + `parse_restart_key` path). Threading `entries_end` into `parse_restart_key()` to restore a lighter bounded fast path is tracked in issue `#190`. Do not flag the full-item decode in `get_key_at` as an unaddressed gap in reviews of PR `#182` or later code; it is a known, tracked follow-up.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 214
File: src/blob_tree/mod.rs:770-781
Timestamp: 2026-04-05T23:14:52.959Z
Learning: In `src/blob_tree/mod.rs` (structured-world/coordinode-lsm-tree), BlobTree merge operands are always stored inline in the index tree — they are never written as blob indirections (they bypass KV separation). Therefore, calling `Tree::resolve_merge_via_pipeline` on a BlobTree merge operand in the `multi_get` batched path (n > 2) is correct and safe: the pipeline result is always a plain `UserValue` with no value-log dereference needed. Do not flag `resolve_merge_via_pipeline` usage for merge operands in `BlobTree::multi_get` as missing blob-context in future reviews of PR `#214` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-30T04:38:24.388Z
Learning: In `src/table/index_block/block_handle.rs` (structured-world/coordinode-lsm-tree), `parse_full` bounds hardening, `#[must_use]` annotations on parser methods, and remaining `value_type` unwrap paths in `parse_full` are pre-existing code outside the diff of PR `#182`. The broader hardening (including a fallible decoder constructor) is tracked in issue `#184`. Do not flag these as unaddressed gaps in reviews of PR `#182`; they are known, tracked follow-ups.
✅ Actions performed

Reviews resumed.


Full review triggered.

find_in_tables (monomorphized get/get_pinned path) continued scanning
L1+ runs after the covering run returned None. L1+ runs have non-
overlapping key ranges — break immediately after covering table miss.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.

Comment thread src/tree/mod.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/tree/mod.rs (1)

1535-1541: ⚠️ Potential issue | 🟠 Major

Stop after the covering L1+ run misses.

Once run.get_for_key_cmp() returns a table, no later run in the same level can contain key. Continuing after T::lookup(...) returns None makes both get() and get_pinned() pay redundant same-level bloom/index probes, even though batch_get_from_tables() already has the covered-miss fast path.

♻️ Minimal fix
             } else {
                 for run in level.iter() {
-                    if let Some(table) = run.get_for_key_cmp(key, comparator)
-                        && let Some(item) = T::lookup(table, key, seqno, key_hash)?
-                    {
-                        return Ok(item.filter_tombstone());
+                    if let Some(table) = run.get_for_key_cmp(key, comparator) {
+                        if let Some(item) = T::lookup(table, key, seqno, key_hash)? {
+                            return Ok(item.filter_tombstone());
+                        }
+                        break;
                     }
                 }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tree/mod.rs` around lines 1535 - 1541, The loop currently continues
probing later runs after run.get_for_key_cmp(key, comparator) returns
Some(table) but T::lookup(table, key, seqno, key_hash) returns None, causing
redundant same-level probes; change the logic so that when get_for_key_cmp
yields a table and T::lookup returns None you immediately short-circuit the
search for that key in this level (return Ok(None) or otherwise stop iterating
this level) instead of continuing to later runs—adjust the code around
run.get_for_key_cmp and T::lookup to return the covered-miss result, mirroring
the fast-path behavior used by batch_get_from_tables(), and keep get() and
get_pinned() calling this updated path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/tree/mod.rs`:
- Around line 1456-1479: The merge step currently does
still_remaining.extend(covered_miss) which can break the comparator sort order
and ruin sequential SST access; instead merge the two sorted partitions
(still_remaining and covered_miss) by comparing keys via keys[idx].as_ref() with
comparator so the resulting vector preserves the comparator order before
descending to lower levels—replace the extend with a proper two-way merge (or
re-sort using the comparator on the combined entries) so functions like
run.get_for_key_cmp, level.iter(), and the vectors still_remaining/covered_miss
maintain the required order for subsequent probes.

---

Duplicate comments:
In `@src/tree/mod.rs`:
- Around line 1535-1541: The loop currently continues probing later runs after
run.get_for_key_cmp(key, comparator) returns Some(table) but T::lookup(table,
key, seqno, key_hash) returns None, causing redundant same-level probes; change
the logic so that when get_for_key_cmp yields a table and T::lookup returns None
you immediately short-circuit the search for that key in this level (return
Ok(None) or otherwise stop iterating this level) instead of continuing to later
runs—adjust the code around run.get_for_key_cmp and T::lookup to return the
covered-miss result, mirroring the fast-path behavior used by
batch_get_from_tables(), and keep get() and get_pinned() calling this updated
path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a266cd29-953d-4641-9fbd-5bd9ec32786f

📥 Commits

Reviewing files that changed from the base of the PR and between 4a9d067 and 5ee39fa.

📒 Files selected for processing (2)
  • src/error.rs
  • src/tree/mod.rs

Comment thread src/tree/mod.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
Cargo.toml (1)

38-38: ⚠️ Potential issue | 🔴 Critical

structured-zstd version may be non-resolvable (build blocker).

At Line 38, please verify that structured-zstd version 0.0.6 exists on crates.io; if it does not, dependency resolution will fail and block CI/build.

#!/bin/bash
# Read-only verification: list published versions for structured-zstd
curl -s https://crates.io/api/v1/crates/structured-zstd/versions \
  | jq -r '.versions[].num' \
  | head -50
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` at line 38, The Cargo.toml lists the dependency structured-zstd =
"0.0.6" which may not exist on crates.io; verify whether version 0.0.6 is
published and, if it is missing, update the dependency entry (the
structured-zstd dependency in Cargo.toml) to a published version (e.g. bump to
an existing semver) or point it to a valid git/path source; ensure the
dependency string for structured-zstd matches a resolvable crate version or
valid alternative source so CI/build dependency resolution succeeds.
src/tree/mod.rs (2)

1535-1540: ⚠️ Potential issue | 🟠 Major

Stop the L1+ run scan after the covering run misses.

Once run.get_for_key_cmp(...) returns a table for this key, later runs in the same level cannot contain it. If T::lookup(...) then returns None, continuing the loop turns get and get_pinned back into an O(runs_in_level) walk on sparse levels.

Fix sketch
-                for run in level.iter() {
-                    if let Some(table) = run.get_for_key_cmp(key, comparator)
-                        && let Some(item) = T::lookup(table, key, seqno, key_hash)?
-                    {
-                        return Ok(item.filter_tombstone());
-                    }
-                }
+                for run in level.iter() {
+                    if let Some(table) = run.get_for_key_cmp(key, comparator) {
+                        if let Some(item) = T::lookup(table, key, seqno, key_hash)? {
+                            return Ok(item.filter_tombstone());
+                        }
+                        break;
+                    }
+                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tree/mod.rs` around lines 1535 - 1540, When scanning runs in a level you
must stop the L1+ run scan once the covering run has been checked and
T::lookup(...) returns None to avoid turning get/get_pinned into an
O(runs_in_level) walk; modify the loop that iterates runs (the code calling
run.get_for_key_cmp(...) and then T::lookup(...)) to track whether a covering
run/table was observed for the key and, if so, break out of the loop when lookup
returns None so later runs in the same level are not scanned. Ensure the change
affects both get and get_pinned code paths and uses the existing
run.get_for_key_cmp, T::lookup, and runs_in_level logic to decide when to stop
scanning.

1456-1479: ⚠️ Potential issue | 🟠 Major

Preserve comparator order when re-adding covered_miss.

Line 1479 concatenates two individually sorted partitions. Once a level has both uncovered keys and earlier covered misses, the next level sees keys out of comparator order, which throws away the sequential SST access pattern this path is trying to preserve.

Fix sketch
-                still_remaining.extend(covered_miss);
+                still_remaining.extend(covered_miss);
+                still_remaining.sort_by(|&(a, _), &(b, _)| {
+                    comparator.compare(keys[a].as_ref(), keys[b].as_ref())
+                });

At minimum, re-sort here; a two-way merge is even better if you want to avoid the extra sort.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tree/mod.rs` around lines 1456 - 1479, The code currently concatenates
the already-sorted `covered_miss` partition with the other sorted partition
causing comparator order to be broken downstream; instead, preserve comparator
order by either performing a two-way merge of the two sorted slices (preferred)
or by re-sorting the concatenated vector with the same comparator used elsewhere
(e.g., the comparator/`cmp`/`compare` function) before returning or passing the
combined keys; locate the concatenation site referencing `covered_miss` and the
other partition and replace the simple concat with a stable merge routine that
compares elements with the same comparator (or call sort_by on the combined
collection) so the final sequence is globally ordered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/pinnable_slice.rs`:
- Around line 10-15: Tests repeat the Config::new(...).open()? pattern; create a
shared helper (e.g., a private test helper function like init_tree or
make_config_in_tests) that constructs the Config, calls open(), and returns the
opened object or Result so tests call that helper instead of duplicating
Config::new(...).open() throughout; update all test usages in pinnable_slice.rs
to call the helper and adjust imports/return types accordingly so future
constructor/config changes are centralized.
- Around line 417-431: Multiple tests re-declare the same ConcatMerge fixture;
hoist a single ConcatMerge definition to module scope and have each test
reference that one to avoid duplication. Create a single ConcatMerge definition
(matching the existing local definitions) at the top of the test module (make it
pub(crate) or non-private as needed), remove the three local redefinitions
currently inside the tests at lines around 417/458/500, and update the tests
that previously declared ConcatMerge to simply use the module-scoped ConcatMerge
symbol (ensuring any helper impls or trait impls such as Display/From/Default
stay with the single definition). Ensure the module-scoped fixture compiles with
all tests that previously used the duplicated versions.

---

Duplicate comments:
In `@Cargo.toml`:
- Line 38: The Cargo.toml lists the dependency structured-zstd = "0.0.6" which
may not exist on crates.io; verify whether version 0.0.6 is published and, if it
is missing, update the dependency entry (the structured-zstd dependency in
Cargo.toml) to a published version (e.g. bump to an existing semver) or point it
to a valid git/path source; ensure the dependency string for structured-zstd
matches a resolvable crate version or valid alternative source so CI/build
dependency resolution succeeds.

In `@src/tree/mod.rs`:
- Around line 1535-1540: When scanning runs in a level you must stop the L1+ run
scan once the covering run has been checked and T::lookup(...) returns None to
avoid turning get/get_pinned into an O(runs_in_level) walk; modify the loop that
iterates runs (the code calling run.get_for_key_cmp(...) and then
T::lookup(...)) to track whether a covering run/table was observed for the key
and, if so, break out of the loop when lookup returns None so later runs in the
same level are not scanned. Ensure the change affects both get and get_pinned
code paths and uses the existing run.get_for_key_cmp, T::lookup, and
runs_in_level logic to decide when to stop scanning.
- Around line 1456-1479: The code currently concatenates the already-sorted
`covered_miss` partition with the other sorted partition causing comparator
order to be broken downstream; instead, preserve comparator order by either
performing a two-way merge of the two sorted slices (preferred) or by re-sorting
the concatenated vector with the same comparator used elsewhere (e.g., the
comparator/`cmp`/`compare` function) before returning or passing the combined
keys; locate the concatenation site referencing `covered_miss` and the other
partition and replace the simple concat with a stable merge routine that
compares elements with the same comparator (or call sort_by on the combined
collection) so the final sequence is globally ordered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a266cd29-953d-4641-9fbd-5bd9ec32786f

📥 Commits

Reviewing files that changed from the base of the PR and between cf5d5bd and 5ee39fa.

📒 Files selected for processing (16)
  • .github/copilot-instructions.md
  • Cargo.toml
  • README.md
  • benches/batch_ops.rs
  • src/abstract_tree.rs
  • src/blob_tree/mod.rs
  • src/error.rs
  • src/lib.rs
  • src/memtable/mod.rs
  • src/pinnable_slice.rs
  • src/table/mod.rs
  • src/tree/mod.rs
  • src/write_batch.rs
  • tests/multi_get.rs
  • tests/pinnable_slice.rs
  • tests/write_batch.rs

Comment thread tests/pinnable_slice.rs
Comment thread tests/pinnable_slice.rs Outdated
- Re-sort still_remaining after extending with covered_miss to
  preserve comparator order for next level's sequential scan
- Extract setup_tree() and module-scope ConcatMerge in pinnable_slice
  tests (was duplicated 20+ / 3 times respectively)
- Bump structured-zstd 0.0.6 → 0.0.7
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 6, 2026

@coderabbitai Re: structured-zstd version duplicate — updated to 0.0.7 (latest). Version 0.0.6 was always valid (cargo search confirmed), 0.0.7 released since. Re: L1+ covering miss duplicates — both fixed: find_in_tables breaks after covering table miss, batch path re-sorts after covered_miss merge.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 6, 2026

Tip

For best results, initiate chat on the files or code changes.

@polaz Thanks for the updates — all three changes are noted:

  1. structured-zstd → 0.0.7: Acknowledged. 0.0.6 was always a valid published version; 0.0.7 is now the latest, and the bump is captured.

  2. find_in_tables covers-miss break: The early break after a covering-table miss in the L1+ walk correctly prevents probing lower-priority runs for a key that is already definitively resolved (present or tombstoned) by a covering SST. Good fix.

  3. Batch path re-sort after covered_miss merge: Re-sorting the remaining key list after merging covered_miss entries back in ensures that the next level's sorted-sequential probing invariant is maintained. Good fix.


✏️ Learnings added
Learnt from: polaz
URL: https://github.com/structured-world/coordinode-lsm-tree/pull/214

Timestamp: 2026-04-06T10:54:29.135Z
Learning: In `src/tree/mod.rs` (structured-world/coordinode-lsm-tree, PR `#214`), `find_in_tables` now breaks out of the L1+ run-walk loop immediately after a covering-table miss. This prevents stale or duplicate results from lower-priority runs for a key already definitively resolved by a covering SST. Do not re-flag the absence of this break in future reviews of PR `#214` or later code.

Learnt from: polaz
URL: https://github.com/structured-world/coordinode-lsm-tree/pull/214

Timestamp: 2026-04-06T10:54:29.135Z
Learning: In `src/tree/mod.rs` (structured-world/coordinode-lsm-tree, PR `#214`), `batch_get_from_tables` re-sorts the remaining keys after merging `covered_miss` entries back into the active key set. This preserves the sorted-sequential probing invariant required for correct L1+ level traversal. Do not re-flag the absence of a re-sort after covered_miss merge in future reviews of PR `#214` or later code.

Learnt from: polaz
URL: https://github.com/structured-world/coordinode-lsm-tree/pull/214

Timestamp: 2026-04-06T10:54:29.135Z
Learning: In `Cargo.toml` (structured-world/coordinode-lsm-tree, PR `#214`), the `structured-zstd` dependency was updated to version `0.0.7` (latest as of the update). Version `0.0.6` (used earlier in the PR) was always a valid published version. Do not flag either version as invalid in reviews of PR `#214`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 214
File: src/tree/mod.rs:1047-1065
Timestamp: 2026-04-05T17:27:55.945Z
Learning: In `src/table/mod.rs` (structured-world/coordinode-lsm-tree), `Table::get` and `Table::get_with_block` both convert table-local seqnos back to global seqno coordinates before returning `InternalValue` (fixed in PR `#214`, commit 431e9759, lines 346-351). All callers — including `get_internal_entry_from_tables`, `get_internal_entry_with_block_from_tables`, and `batch_get_from_tables` — therefore receive already-normalized global seqnos. Do not re-flag the absence of caller-side seqno normalization for these APIs in future reviews of PR `#214` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-30T04:38:24.388Z
Learning: In `src/table/index_block/block_handle.rs` (structured-world/coordinode-lsm-tree), `parse_full` bounds hardening, `#[must_use]` annotations on parser methods, and remaining `value_type` unwrap paths in `parse_full` are pre-existing code outside the diff of PR `#182`. The broader hardening (including a fallible decoder constructor) is tracked in issue `#184`. Do not flag these as unaddressed gaps in reviews of PR `#182`; they are known, tracked follow-ups.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T20:34:35.867Z
Learning: In `src/vlog/mod.rs` (structured-world/coordinode-lsm-tree), `recover_blob_files` defers all `insert_for_blob_file` calls into a `pending_cache_inserts` buffer and commits them only after the missing-file guard (`blob_files.len() < ids.len()`) passes. This was fixed in PR `#208` to prevent leaking FDs into the descriptor-table cache on partial/failed recovery. Do not flag the absence of this deferral as a gap in future reviews of PR `#208` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-30T22:48:06.079Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), the "binary search trusts semantically-corrupted binary index" concern (lines 245-299) is explicitly out of scope for PR `#182`. The scenario requires a corrupted binary index that passes the block checksum (encoder bug or checksum collision). Adding O(n) monotonicity validation per seek call would regress the hot path for a case already caught by checksums. This hardening is tracked in issue `#184` (fallible decoder constructor). Do not flag the absence of monotonicity validation on the binary-index seek path in reviews of PR `#182` or earlier code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 202
File: src/table/tests.rs:2113-2118
Timestamp: 2026-04-02T23:53:51.122Z
Learning: In `src/table/block_index/iter.rs` and `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), `trim_back_to_upper_bound` contains an unconditional restore (the `else { true }` branch at lines ~787-788 of decoder.rs) that always re-pushes a covering entry onto `hi_scanner.stack` when the stack would otherwise empty. This means `seek_upper_bound_cursor` always returns `true` for well-formed block data, regardless of `restart_interval`. Consequently, the `OwnedIndexBlockIter::from_block_with_bounds` returning `Ok(None)` (empty-child path) cannot be forced from test configuration (e.g., `use_index_block_restart_interval(4)`) on non-corrupt data — it is only reachable via `fill_stack`/`advance_upper_restart_interval` decode failure. Regression tests for the two-level index empty-child skip behavior (PR `#202`, issue `#194`) validate overall iteration correctness defensively, not the exact `Ok(None)` corruption path. Do not suggest forcing this path via `restart_interval` changes in reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-27T07:10:44.958Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), the restart-key span validation is intentionally split: strict bounds hardening (buffer/offset checks) was implemented in PR `#182`, while full semantic restart-key span validation (which requires passing the restart-key span through decoder state) is tracked separately in issue `#183`. Do not flag the absence of semantic-span validation in decoder.rs as an unaddressed gap in reviews of PR `#182` or earlier code; it is a known, tracked follow-up.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-28T13:41:55.184Z
Learning: In `src/table/index_block/block_handle.rs` (structured-world/coordinode-lsm-tree), the `parse_truncated` function now validates `shared_prefix_len` against both the entry-start boundary AND the restart-key span (`base_key_end`). This was fixed in PR `#182` commit `f64f90e` (originally tracked as follow-up issue `#186`). The function signature was updated to accept a `base_key_end: usize` parameter, and the prefix-end check was changed from `prefix_end > offset` to `prefix_end > base_key_end`. Do not flag the absence of this restart-key span check in reviews of PR `#182` or later code — it is fully implemented.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-05T10:11:17.013Z
Learning: In `src/version/recovery.rs` (structured-world/coordinode-lsm-tree), the `table_count` and `blob_file_count` corruption unit tests were updated in PR `#212` (commit 49a23065) to assert `matches!(err, crate::Error::Unrecoverable)` instead of generic `is_err()`. This pins the exact error variant returned by the bounds-check path. Do not flag the absence of this precise assertion in future reviews of PR `#212` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 182
File: src/table/block/decoder.rs:236-243
Timestamp: 2026-03-30T17:47:54.258Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), `get_key_at` calls `Item::parse_full` on every binary-search probe inside `partition_point` / `partition_point_2` as a result of the bounds-hardening in PR `#182` (which replaced the previous unsafe `get_unchecked` + `parse_restart_key` path). Threading `entries_end` into `parse_restart_key()` to restore a lighter bounded fast path is tracked in issue `#190`. Do not flag the full-item decode in `get_key_at` as an unaddressed gap in reviews of PR `#182` or later code; it is a known, tracked follow-up.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 214
File: src/table/mod.rs:352-356
Timestamp: 2026-04-05T22:29:27.475Z
Learning: In `src/table/mod.rs` (structured-world/coordinode-lsm-tree, PR `#214`), the seqno rehydration additions in `Table::get` and `Table::get_with_block` (i.e. `iv.key.seqno += global_seqno`) were changed to `saturating_add` in the fix commit to prevent u64 wrap-around when `global_seqno` is large or corrupt. `saturating_add` clamps to `u64::MAX`, which is safe because it is far above any valid seqno and will not collide with real seqno comparisons. Do not re-flag these additions as unchecked overflow in future reviews; the saturating semantics are intentional.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 166
File: src/tree/mod.rs:1628-1659
Timestamp: 2026-03-24T20:01:06.401Z
Learning: In `structured-world/coordinode-lsm-tree` `src/tree/mod.rs`, the `RouteMismatch` check in `recover_levels` is intentionally a best-effort heuristic: it uses `config.level_routes` level-coverage to distinguish a removed route (config error) from a deleted SST (data corruption), but does NOT persist route provenance per-table. Persisting route provenance would require a manifest format change and is deferred to a future PR. Do not re-flag this as a misclassification bug; the limitation is documented in the code comment above the `all_missing_uncovered` logic.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T07:48:17.153Z
Learning: In `src/config/mod.rs` (structured-world/coordinode-lsm-tree), `all_tables_folders` deduplicates by path only. This is correct: the same filesystem path always refers to the same on-disk directory, so path identity is sufficient for dedup. Routing the same path through different Fs backends is a configuration error that is already prevented upstream by `level_routes` validation (overlapping key ranges are rejected). Do not flag the absence of Fs-backend-aware deduplication in `all_tables_folders` in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-05T01:09:33.237Z
Learning: In `src/version/super_version.rs` (structured-world/coordinode-lsm-tree), `SuperVersions::maintenance` GC loop was fixed in PR `#212` (commit ecfa47c8) to eliminate the TOCTOU between `fs.exists()` and `fs.remove_file()`. The canonical fix calls `fs.remove_file()` directly and treats `ErrorKind::NotFound` as success. Do not flag the absence of a preceding `exists()` guard in future reviews of PR `#212` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-28T13:41:14.854Z
Learning: In `src/table/index_block/block_handle.rs` (structured-world/coordinode-lsm-tree), the `parse_truncated` function now validates `shared_prefix_len` against the actual restart-key span (`base_key_end`) in addition to the entry-start boundary. This was implemented in PR `#182` commit `f64f90e1`, closing issue `#186`. The function signature now includes `base_key_end: usize` to carry the restart-key end offset through the decoder parse path.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 214
File: src/tree/mod.rs:845-875
Timestamp: 2026-04-05T19:15:56.939Z
Learning: In `src/tree/mod.rs` (structured-world/coordinode-lsm-tree, PR `#214`), `multi_get` Phase 1 cannot pre-prune keys by active/sealed memtable range tombstones before SST lookup. RT suppression requires `key_seqno < rt.seqno`, but the entry seqno for disk-resident keys is not known until Phase 2 (SST lookup via `batch_get_from_tables`) completes. Pre-pruning would risk incorrectly suppressing valid entries. RT suppression is correctly and safely applied in Phase 3 via `resolve_entry` → `is_suppressed_by_range_tombstones` after the entry seqno is available. Do not re-raise early RT pre-pruning in `multi_get` Phase 1 in future reviews of PR `#214` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 214
File: src/tree/mod.rs:1730-1749
Timestamp: 2026-04-05T15:32:09.162Z
Learning: In `src/tree/mod.rs` (structured-world/coordinode-lsm-tree), `Tree::append_batch` / `apply_batch` inserts entries into the memtable one-at-a-time and does NOT provide internal atomic visibility. Atomic batch visibility is a caller responsibility: after `apply_batch` returns, the caller must call `visible_seqno.fetch_max(seqno + 1)` to gate readers. This contract was documented in PR `#214` (WriteBatch and apply_batch docs). Do not flag the absence of an internal commit barrier in `append_batch` / `Memtable::insert_batch` in future reviews — visibility is intentionally delegated to the caller's seqno management.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 214
File: src/write_batch.rs:77-103
Timestamp: 2026-04-05T16:20:39.105Z
Learning: In `src/write_batch.rs` (structured-world/coordinode-lsm-tree PR `#214`), `WriteBatch` deliberately allows duplicate keys but documents that all entries share the same seqno, so ordering for repeated keys is determined by skiplist ordering on `(user_key, seqno, value_type)` — not by insertion order. The maintainer chose documentation-only guidance over API-level enforcement or canonicalization. Do not re-raise the duplicate-key ordering concern as a blocking issue in future reviews of PR `#214` or later code; at most suggest a follow-up debug_assert in `materialize`.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 191
File: src/table/iter.rs:92-97
Timestamp: 2026-04-02T16:28:24.715Z
Learning: In `src/table/iter.rs` and `src/table/util.rs` (structured-world/coordinode-lsm-tree), the `cache.get_block(...)` fast path inside `load_block` (util.rs:57-76) returns cached blocks without validating `block_type`. This is a pre-existing pattern not introduced by PR `#191`; the `create_data_block_reader` helper in `iter.rs` inherits the same gap. Block-type validation on the cache-hit path is tracked in issue `#198`. Do not re-flag the absence of `BlockType::Data` validation on the cache-hit path in reviews of PR `#191` or later code.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.

Comment thread Cargo.toml
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/pinnable_slice.rs`:
- Around line 425-427: Test currently flushes after the merge so both the base
insert and merge operand end up in the same SST; move the flush to occur
immediately after tree.insert("k", "D", 0) to force the base value into a
different SST, then perform tree.merge("k", "E", 1) (and optionally flush again)
so merge resolution exercises the cross‑SST disk lookup path—adjust calls to
tree.insert, tree.flush_active_memtable(0) and tree.merge accordingly and
preserve the ? error propagation.
- Around line 8-16: The helper setup_tree currently calls .open().unwrap(),
causing panics on I/O errors; change its signature to return
lsm_tree::Result<(AnyTree, tempfile::TempDir)> and replace the unwrap with the ?
operator so the error from Config::new(...).open() is propagated to callers;
update any callers in tests to handle Result (using ? in test functions or
map_err) so failures surface as Results instead of panics.
- Around line 346-348: The test currently applies a range tombstone via
tree.remove_range("a", "c", 2) but never flushes it, so it only exercises
memtable RT suppression; after calling tree.remove_range(...) add a flush of the
memtable so the range tombstone is written to an SST (e.g., call tree.flush() or
the project's memtable flush helper) so get_pinned() must handle SST-vs-SST
suppression; ensure the flush happens before subsequent reads/assertions that
rely on disk-resident RT behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 03560bd8-175c-46fd-aa37-cf4667c4c880

📥 Commits

Reviewing files that changed from the base of the PR and between 5ee39fa and 1422860.

📒 Files selected for processing (3)
  • Cargo.toml
  • src/tree/mod.rs
  • tests/pinnable_slice.rs

Comment thread tests/pinnable_slice.rs Outdated
Comment thread tests/pinnable_slice.rs Outdated
Comment thread tests/pinnable_slice.rs
polaz added 2 commits April 6, 2026 14:52
- setup_tree() returns Result instead of unwrap on I/O path
- RT suppression test flushes tombstone to disk (SST-vs-SST path)
- merge-on-disk test splits base and operand across separate SSTs
- RT suppresses blob-resolved values in batch path
- Merge operand resolution with and without merge operator
- Memtable hits bypass SST phase for blob tree
- Merge without operator returns raw operand value
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Comment thread src/write_batch.rs
Comment thread src/error.rs Outdated
InternalKey ordering ignores value_type, making equal-key entries
with different op types ambiguous to reads/merges — not skiplist
overwrites.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.

@polaz polaz merged commit 6f86575 into main Apr 6, 2026
21 checks passed
@polaz polaz deleted the feat/#143-perf-readwrite-path-optimization--batch-multiget branch April 6, 2026 12:45
@sw-release-bot sw-release-bot Bot mentioned this pull request Apr 6, 2026
polaz pushed a commit that referenced this pull request Apr 6, 2026
## 🤖 New release

* `coordinode-lsm-tree`: 4.3.0 -> 4.3.1

<details><summary><i><b>Changelog</b></i></summary><p>

<blockquote>

##
[4.3.1](v4.3.0...v4.3.1)
- 2026-04-06

### Performance

- *(compression)* use numeric zstd levels in pure Rust backend
([#226](#226))
- batch multi_get + PinnableSlice + WriteBatch
([#214](#214))
</blockquote>


</p></details>

---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).

Co-authored-by: sw-release-bot[bot] <255865126+sw-release-bot[bot]@users.noreply.github.com>
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.

perf: read/write path optimization — batch multi_get + PinnableSlice + batch insert

2 participants