Skip to content

feat: per value support for fixed-length packed structs#7714

Open
morales-t-netflix wants to merge 3 commits into
lance-format:mainfrom
morales-t-netflix:tmorales/compression_fixed_length_structs
Open

feat: per value support for fixed-length packed structs#7714
morales-t-netflix wants to merge 3 commits into
lance-format:mainfrom
morales-t-netflix:tmorales/compression_fixed_length_structs

Conversation

@morales-t-netflix

@morales-t-netflix morales-t-netflix commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

In line with the suggestion from this issue, this added support for full-zip encoding for fixed-length structs.

Please note - this is a change from current behavior (per-value Fixed-length packed structs would error out). Please let me know if that warrants marking this feat as a breaking change.

Will note - I'm fairly new to rust, so any and all feedback is appreciated 😉

Summary by CodeRabbit

  • New Features
    • Added fixed-per-value packed struct encoding/decoding for fixed-width children, with flat row-major packed metadata.
    • Enhanced packed struct handling to better support fixed layouts alongside existing variable packed structs.
  • Bug Fixes
    • Fixed per-value packed struct selection when no variable-width child is present.
    • Added stricter validation for fixed packed struct decompression (byte alignment, layout/width constraints, bounds checks).
  • Tests
    • Added fixed packed-struct round-trip tests and full writer/reader wiring, including rejection coverage for unsupported variable-width children in the fixed encoder.

@github-actions github-actions Bot added the A-encoding Encoding, IO, file reader/writer label Jul 9, 2026
@github-actions github-actions Bot added the enhancement New feature or request label Jul 9, 2026
@morales-t-netflix morales-t-netflix force-pushed the tmorales/compression_fixed_length_structs branch from b6636bf to 8a991b1 Compare July 9, 2026 21:03
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 999ded29-6e71-4dc2-8586-4e1a75b2682e

📥 Commits

Reviewing files that changed from the base of the PR and between caf6b7c and 9b062c9.

📒 Files selected for processing (1)
  • rust/lance-encoding/src/encodings/physical/packed.rs

📝 Walkthrough

Walkthrough

Adds fixed-per-value packed-struct encoding and decoding, refactors shared packed-field handling, wires the new compression path into strategy selection, and adds direct, full-zip, and rejection tests.

Changes

Packed-Struct Compression

Layer / File(s) Summary
Packed-field validation and accumulation
rust/lance-encoding/src/encodings/physical/packed.rs, rust/lance-encoding/src/encodings/logical/primitive.rs
Adds fixed-field row handling, shared struct validation, and fixed-field accumulation and reconstruction for packed-struct decoding.
Fixed packed-struct encoding
rust/lance-encoding/src/encodings/physical/packed.rs, rust/lance-encoding/src/compression.rs
Implements PackedStructFixedPerValueEncoder, emits row-major packed metadata, and selects the encoder for fixed-width struct blocks.
Fixed packed-struct decoding and round trips
rust/lance-encoding/src/encodings/physical/packed.rs, rust/lance-encoding/src/compression.rs
Implements packed-buffer unzipping and child decompression, wires decoder creation, and adds round-trip and variable-child rejection tests.

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

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% 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 per-value support for fixed-length packed structs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

ACTION NEEDED
Lance follows the Conventional Commits specification for release automation.

The PR title and description are used as the merge commit message. Please update your PR title and description to match the specification.

For details on the error please inspect the "PR Title Check" action.

@morales-t-netflix morales-t-netflix changed the title feat: Per value support for fixed-length packed structs feat: per value support for fixed-length packed structs Jul 9, 2026

@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.

Actionable comments posted: 6

🤖 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.

Inline comments:
In `@rust/lance-encoding/src/encodings/physical/packed.rs`:
- Around line 190-197: Move the byte-alignment check and byte-width calculation
out of `append_row_bytes` and into `FixedPackedFieldData` construction so they
are validated once instead of per row. Update the
`FixedPackedFieldData`/`Packed` setup to reject non-byte-aligned
`bits_per_value` up front, store `bytes_per_value` on the struct, and have
`append_row_bytes` use the cached value without repeating the invariant check.
- Around line 440-462: The packed-width arithmetic in the fixed struct path can
overflow or wrap when accumulating child widths or sizing buffers. In the
`packed.rs` flow around `compress`/row allocation, replace `bits_per_row +=` and
any metadata-derived size math with `checked_add` and `checked_mul`, and reject
invalid totals as corrupted data rather than continuing. Store the validated
row-width in the decompressor/decoder state, and make the related paths at the
referenced `Packed*` helpers use the same checked arithmetic and
`Error::corrupt_file` reporting with the relevant sizes/values included.
- Around line 869-875: The decompressor type is exposing a public API surface
unnecessarily; make PackedStructFixedPerValueDecompressor crate-private to match
its pub(crate) constructor and internal-only usage. Update the struct
declaration in packed.rs so the type itself is no longer public, while keeping
PackedStructFixedPerValueDecompressor::new(crate) available for internal
callers.
- Around line 1554-1559: The test for PackedStructFixedPerValueEncoder::compress
only checks the error message text, so it can pass even if the wrong error
category is returned. Update the assertion to verify both the specific error
variant produced by compress and that its message still contains the expected
“fixed-width” text, using the existing encoder/compress/unwarp_err setup in this
test.
- Around line 198-208: The packed struct child slice in packed.rs computes the
end index with unchecked addition, which can wrap and bypass the bounds check.
Update the logic around the `Packed` row extraction path to use checked addition
for `end` (mirroring the existing `checked_mul` on `row_idx`) and return
`Error::invalid_input` on overflow before slicing `self.block.data`; keep the
existing `start`, `data`, and `output.extend_from_slice` flow in the same
method.
- Around line 414-422: The PackedStructFixedPerValueEncoder type is exposed as
public API even though it is only used internally by the compression strategy.
Change both the struct and its new constructor to crate-visible access in
PackedStructFixedPerValueEncoder so they remain internal to the crate, and keep
the encoder construction limited to the strategy that creates it.
🪄 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 Plus

Run ID: f958ff30-04b5-409c-9387-116180210930

📥 Commits

Reviewing files that changed from the base of the PR and between 0bdf0a1 and 8a991b1.

📒 Files selected for processing (3)
  • rust/lance-encoding/src/compression.rs
  • rust/lance-encoding/src/encodings/logical/primitive.rs
  • rust/lance-encoding/src/encodings/physical/packed.rs
💤 Files with no reviewable changes (1)
  • rust/lance-encoding/src/encodings/logical/primitive.rs

Comment on lines +190 to +197
fn append_row_bytes(&self, row_idx: usize, output: &mut Vec<u8>) -> Result<()> {
let bits_per_value = self.block.bits_per_value;
if !bits_per_value.is_multiple_of(8) {
return Err(Error::invalid_input(
"Packed struct encoding requires byte-aligned fixed-width children",
));
}
let bytes_per_value = (bits_per_value / 8) as usize;

@coderabbitai coderabbitai Bot Jul 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Hoist byte-alignment validation out of the row loop.

This method runs once per field per row, but alignment and byte width never change. Validate them when constructing FixedPackedFieldData and store bytes_per_value.

As per coding guidelines, “In performance-critical lance-encoding encoding/decoding paths, hoist loop-invariant conditionals out of hot loops.”

🤖 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/packed.rs` around lines 190 - 197,
Move the byte-alignment check and byte-width calculation out of
`append_row_bytes` and into `FixedPackedFieldData` construction so they are
validated once instead of per row. Update the `FixedPackedFieldData`/`Packed`
setup to reject non-byte-aligned `bits_per_value` up front, store
`bytes_per_value` on the struct, and have `append_row_bytes` use the cached
value without repeating the invariant check.

Source: Coding guidelines

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added all of AI's suggestions except this one. I think philosophically, the above sounds right to me, but this isn't the behavior of the code previously - so would be interested in input from a maintainer

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@morales-t-netflix, that's a fair point to flag for maintainer input. A couple of things that might help frame the discussion:

  • FixedPackedFieldData is new in this PR, so there isn't really "previous behavior" tied to this struct specifically — the per-call check pattern was likely copied for symmetry with VariablePackedFieldData::append_row_bytes, where the equivalent checks (bits_per_length alignment, 32/64-bit prefix) genuinely can't be hoisted because they gate a match on block.bits_per_offset, which is inspected fresh each call.
  • For the fixed case, though, bits_per_value is fixed at construction (self.block.bits_per_value never changes across rows), so hoisting is behavior-preserving in the sense that the same error is still returned for invalid input — it would just surface once during FixedPackedFieldData construction instead of on the first append_row_bytes call. No valid input path changes.

That said, I'll defer to the maintainer's call here — if there's a reason to keep validation lazy/per-call (e.g., to match error-reporting expectations elsewhere in the encoder), that's a legitimate tradeoff to weigh against the minor per-row overhead.

(๐•ᴗ•๐) still thinking this one over alongside you

Comment thread rust/lance-encoding/src/encodings/physical/packed.rs
Comment thread rust/lance-encoding/src/encodings/physical/packed.rs
Comment thread rust/lance-encoding/src/encodings/physical/packed.rs
Comment thread rust/lance-encoding/src/encodings/physical/packed.rs
Comment thread rust/lance-encoding/src/encodings/physical/packed.rs
@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.43902% with 74 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...st/lance-encoding/src/encodings/physical/packed.rs 77.39% 52 Missing and 21 partials ⚠️
rust/lance-encoding/src/compression.rs 80.00% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

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.

1 participant