Skip to content

feat: compress RLE child buffers#7663

Open
Xuanwo wants to merge 4 commits into
mainfrom
xuanwo/issue-7329-rle-child-compression
Open

feat: compress RLE child buffers#7663
Xuanwo wants to merge 4 commits into
mainfrom
xuanwo/issue-7329-rle-child-compression

Conversation

@Xuanwo

@Xuanwo Xuanwo commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes #7329.

RLE miniblock compression currently stores run values and run lengths as raw Flat child buffers, so repeated or low-entropy child buffers can dominate the encoded page even after RLE chooses a better shape than raw fixed-width data.

This change lets RLE child buffers choose the smallest valid representation among Flat, OutOfLineBitpacking(Flat), and General(Flat) when a general compression scheme is configured. The decoder validates the supported nested child encodings and preserves the existing flat path, while rejecting combinations where both children require the run count.

The strategy automatically enables RLE child bitpacking and reuses explicit field compression settings for LZ4/Zstd child candidates, so configured general compression can reduce the values and run-length payload without adding a new public compression knob.

Performance data from a local release microbenchmark is below. Payload bytes exclude metadata; shape is values/run_lengths.

Scenario Plain RLE Auto child bitpack LZ4 configured Zstd configured
Long constant runs 384 B 384 B (flat/flat) 384 B (flat/flat) 384 B (flat/flat)
Short runs, small values 327,680 B 90,112 B (bitpack/flat) 5,120 B (general/general) 5,440 B (general/general)
Short runs, monotonic values 327,680 B 196,608 B (bitpack/flat) 132,224 B (bitpack/general) 132,800 B (bitpack/general)
Short runs, random values 327,680 B 286,720 B (flat/bitpack) 263,296 B (flat/general) 263,872 B (flat/general)
Variable U16 run lengths 26,226 B 26,226 B (flat/flat) 26,226 B (flat/flat) 26,226 B (flat/flat)

The encoder falls back to Flat when a child encoding does not shrink the payload. In the high-benefit cases, child bitpacking keeps encode/decode time in the same order of magnitude as plain RLE, while LZ4/Zstd trade more CPU for substantially smaller payloads. For example, short runs with small values measured about 512/237 us encode/decode for plain RLE, 517/248 us with child bitpacking, 585/265 us with LZ4, and 633/294 us with Zstd.

Summary by CodeRabbit

  • New Features

    • Improved support for newer file versions with more flexible compression choices for encoded data.
    • Added smarter selection between compact encoding options to reduce stored size when possible.
  • Bug Fixes

    • Fixed decoding issues for data using newer compression layouts.
    • Improved compatibility across older and newer encoded files, including mixed compression settings.

@github-actions github-actions Bot added A-encoding Encoding, IO, file reader/writer enhancement New feature or request labels Jul 7, 2026
@Xuanwo Xuanwo marked this pull request as ready for review July 7, 2026 10:30
@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.97335% with 213 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
rust/lance-encoding/src/encodings/physical/rle.rs 82.21% 95 Missing and 37 partials ⚠️
rust/lance-encoding/src/compression.rs 70.11% 59 Missing and 22 partials ⚠️

📢 Thoughts on this report? Let us know!

@BubbleCal BubbleCal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two concerns from review.

Comment thread rust/lance-encoding/src/compression.rs Outdated
Comment thread rust/lance-encoding/src/compression.rs Outdated
@Xuanwo Xuanwo requested a review from BubbleCal July 8, 2026 15:29
Comment thread rust/lance-encoding/src/compression.rs Outdated

if rle_bytes < raw_bytes {
#[cfg(feature = "bitpacking")]
let use_child_encodings = version.resolve() >= LanceFileVersion::V2_2;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This enables new RLE child encodings for V2.2, but released V2.2 readers only accept flat RLE children. Files written this way may be unreadable by older V2.2 readers, so this likely needs a new format-version gate.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds support for secondary (child) compression of RLE values and run-lengths buffers in the Lance encoding module. Introduces candidate generation for flat, general, and bitpacked encodings, version-gated selection logic (V2.3+), new decompressor abstractions to reconstruct compressed child buffers, and expanded validation and tests.

Changes

RLE child buffer secondary compression

Layer / File(s) Summary
Encoder child-compression config
rust/lance-encoding/src/encodings/physical/rle.rs
RleEncoder gains values_compression, run_lengths_compression, and use_child_bitpacking fields plus with_child_encoding constructor and a new RleChildCandidate struct.
Candidate generation and selection
rust/lance-encoding/src/encodings/physical/rle.rs
Implements flat/general/bitpacked candidate generation, pair selection by size with run-count constraints, and integrates the chosen pair into MiniBlockCompressor output.
Decoder child abstraction
rust/lance-encoding/src/encodings/physical/rle.rs
Adds RleChildDecompressor (Flat/Block variants), with_child_decompressors, reworked decode_data, and new decode_child_buffers/num_child_values helpers to reconstruct values from decompressed child buffers.
Version-aware selection and decompression wiring
rust/lance-encoding/src/compression.rs
try_rle_for_mini_block now takes the file version, derives child encoding config, and estimates payload size; build_fixed_width_compressor passes the version through; create_rle_decompressor and validation helpers replace the old flat-only validate_rle_compression for both mini-block and block decompression paths.
Tests and helpers
rust/lance-encoding/src/compression.rs, rust/lance-encoding/src/encodings/physical/rle.rs
Adds expect_rle_encoding helper and tests covering flat-child preservation on older versions, bitpacked/general child selection on newer versions, values/run-length compression combinations, and async round-trip verification with explicit compression settings.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding compression for RLE child buffers.
Linked Issues check ✅ Passed The changes implement supported secondary compression for RLE child buffers, validate invalid nested cases, and add tests for values and run lengths.
Out of Scope Changes check ✅ Passed The modified code and tests stay focused on RLE child-buffer compression and decoder validation, with no clear unrelated scope creep.
✨ 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 xuanwo/issue-7329-rle-child-compression

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
rust/lance-encoding/src/compression.rs (1)

1187-1338: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Prefer Error::corrupt_file for malformed persisted encodings on the read path.

create_rle_decompressor, create_rle_child_decompressor, validate_rle_child_compression, and validate_rle_block_child_inner are reading encodings deserialized from stored files. Missing sub-encodings (e.g. missing values/run-lengths/inner compression) and unsupported nested child combinations are format/integrity problems, not caller-supplied input, so Error::corrupt_file classifies the root cause more accurately and helps operators distinguish corruption from API misuse. Note the surrounding code already uses invalid_input for similar cases, so consider whether to align both.

As per coding guidelines: "Match the Error variant to the root cause: use Error::invalid_input for caller data issues, Error::corrupt_file for format or integrity issues".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-encoding/src/compression.rs` around lines 1187 - 1338, The RLE
read-path validation in create_rle_decompressor, create_rle_child_decompressor,
validate_rle_child_compression, and validate_rle_block_child_inner is treating
malformed persisted encodings as invalid input; switch the missing child
encoding/compression and unsupported nested-combination error returns to
Error::corrupt_file so these deserialization integrity issues are classified
correctly. Keep the existing function structure and update the relevant
Error::invalid_input calls in these helpers to use the corruption variant where
the data comes from stored file metadata rather than caller-provided arguments.

Source: Coding guidelines

🧹 Nitpick comments (2)
rust/lance-encoding/src/encodings/physical/rle.rs (2)

902-932: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid bare .unwrap() in library code.

Inside required_bits, the value.try_into().unwrap() calls are in non-test library code. Although chunks_exact(N) guarantees the slice length so the conversion is infallible in practice, the project guideline prohibits bare .unwrap() here.

♻️ Use `.expect(...)` (or destructure the array) to document the invariant
             16 => buffer
                 .as_ref()
                 .chunks_exact(2)
-                .map(|value| u16::from_le_bytes(value.try_into().unwrap()) as u64)
+                .map(|value| {
+                    u16::from_le_bytes(
+                        value.try_into().expect("chunks_exact(2) yields 2-byte slices"),
+                    ) as u64
+                })
                 .max(),

As per coding guidelines: "Never use .unwrap() ... in library code for fallible operations" and "If unavoidable, use .expect("reason")."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-encoding/src/encodings/physical/rle.rs` around lines 902 - 932,
The `required_bits` helper in `rle.rs` uses bare `unwrap()` on
`value.try_into()` in the 16/32/64-bit `chunks_exact` branches, which violates
the library code guideline. Replace those `unwrap()` calls with an explicit
invariant-documenting approach, such as `expect(...)` with a clear message or
destructuring into fixed-size arrays, while keeping the logic inside
`required_bits` unchanged.

Source: Coding guidelines


994-1020: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Avoid recomputing RLE child candidates on the write path
try_rle_for_mini_block calls selected_payload_size before returning the RLE compressor, so compress() repeats encode_data() + child_candidates() for the same block. With child compression enabled, that doubles the expensive RLE/LZ4/Zstd work. Cache the selected candidates or encoded buffers during the estimate and reuse them in compress() instead of recomputing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-encoding/src/encodings/physical/rle.rs` around lines 994 - 1020,
selected_payload_size currently recomputes encode_data and child_candidates for
the same block that try_rle_for_mini_block has already inspected, causing
duplicate work on the write path. Update the RLE flow in rle.rs so the result of
the sizing/selection step is cached and reused by compress()—for example by
threading the encoded buffers or selected child candidates through the
compressor state—rather than calling encode_data() and child_candidates() again
in selected_payload_size and compress().
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@rust/lance-encoding/src/compression.rs`:
- Around line 1187-1338: The RLE read-path validation in
create_rle_decompressor, create_rle_child_decompressor,
validate_rle_child_compression, and validate_rle_block_child_inner is treating
malformed persisted encodings as invalid input; switch the missing child
encoding/compression and unsupported nested-combination error returns to
Error::corrupt_file so these deserialization integrity issues are classified
correctly. Keep the existing function structure and update the relevant
Error::invalid_input calls in these helpers to use the corruption variant where
the data comes from stored file metadata rather than caller-provided arguments.

---

Nitpick comments:
In `@rust/lance-encoding/src/encodings/physical/rle.rs`:
- Around line 902-932: The `required_bits` helper in `rle.rs` uses bare
`unwrap()` on `value.try_into()` in the 16/32/64-bit `chunks_exact` branches,
which violates the library code guideline. Replace those `unwrap()` calls with
an explicit invariant-documenting approach, such as `expect(...)` with a clear
message or destructuring into fixed-size arrays, while keeping the logic inside
`required_bits` unchanged.
- Around line 994-1020: selected_payload_size currently recomputes encode_data
and child_candidates for the same block that try_rle_for_mini_block has already
inspected, causing duplicate work on the write path. Update the RLE flow in
rle.rs so the result of the sizing/selection step is cached and reused by
compress()—for example by threading the encoded buffers or selected child
candidates through the compressor state—rather than calling encode_data() and
child_candidates() again in selected_payload_size and compress().

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: afccec72-953c-4004-a5c8-6a4880e10bd0

📥 Commits

Reviewing files that changed from the base of the PR and between 5e9c196 and 94d6c18.

📒 Files selected for processing (2)
  • rust/lance-encoding/src/compression.rs
  • rust/lance-encoding/src/encodings/physical/rle.rs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-encoding Encoding, IO, file reader/writer enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support secondary compression for RLE child buffers

2 participants