Skip to content

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

Merged
Xuanwo merged 6 commits into
lance-format:mainfrom
morales-t-netflix:tmorales/compression_fixed_length_structs
Aug 17, 2026
Merged

feat: per value support for fixed-length packed structs#7714
Xuanwo merged 6 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

Adds #5021

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 prior to this). 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 😉

@github-actions github-actions Bot added A-encoding Encoding, IO, file reader/writer enhancement New feature or request labels 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
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 053e28ec-2955-4019-93af-206b8e0138df

📥 Commits

Reviewing files that changed from the base of the PR and between cf9f028 and 32c8c8c.

📒 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

📝 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, version-gating, and rejection tests.

Changes

Packed-Struct Compression

Layer / File(s) Summary
Packed-field validation and accumulation
rust/lance-encoding/src/encodings/physical/packed.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, rust/lance-encoding/src/encodings/logical/primitive.rs
Implements PackedStructFixedPerValueEncoder, emits row-major packed metadata, selects the encoder for fixed-width struct blocks, removes the Full-zip rejection, and gates encoding on Lance V2.3.
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, full-zip, version-gating, and variable-child rejection tests.

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

Sequence Diagram(s)

sequenceDiagram
  participant CompressionStrategy
  participant PackedStructFixedPerValueEncoder
  participant FixedWidthDataBlock
  participant DecompressionStrategy
  participant PackedStructFixedPerValueDecompressor
  CompressionStrategy->>PackedStructFixedPerValueEncoder: select fixed packed-struct encoding
  PackedStructFixedPerValueEncoder->>FixedWidthDataBlock: write row-major packed bytes and metadata
  DecompressionStrategy->>PackedStructFixedPerValueDecompressor: create PackedStruct decoder
  PackedStructFixedPerValueDecompressor->>PackedStructFixedPerValueDecompressor: unzip rows into child buffers
  PackedStructFixedPerValueDecompressor-->>DecompressionStrategy: return reconstructed struct
Loading

Suggested labels: A-format

Suggested reviewers: xuanwo

🚥 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
Title check ✅ Passed The title clearly summarizes the main change: adding per-value support for fixed-length packed structs.
Description check ✅ Passed The description is directly related to the PR and correctly describes adding full-zip support for fixed-length 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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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
Contributor

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
Contributor

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
Contributor

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 78.99160% with 75 lines in your changes missing coverage. Please review.

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

📢 Thoughts on this report? Let us know!

@Xuanwo Xuanwo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The fixed full-zip path makes sense for wide packed structs. My main concern is the file-version contract: create_per_value enables this fixed PackedStruct full-zip representation for V2.1 and V2.2, while existing readers for those stable versions only recognize PackedStruct in the mini-block dispatcher. For example, the released v9.0.0-beta.19 reader falls through to the fixed-per-value todo! for this metadata, so a file written as V2.2 by this PR cannot be read after rolling back to that released reader.

Could we only enable writing this combination for V2.3+ and keep V2.1/V2.2 returning NotSupported? It would also be worth covering V2.1/V2.2 rejection and V2.3 round-trip in the versioned tests. The reader side can remain permissive.

@morales-t-netflix

Copy link
Copy Markdown
Contributor Author

The fixed full-zip path makes sense for wide packed structs. My main concern is the file-version contract: create_per_value enables this fixed PackedStruct full-zip representation for V2.1 and V2.2, while existing readers for those stable versions only recognize PackedStruct in the mini-block dispatcher. For example, the released v9.0.0-beta.19 reader falls through to the fixed-per-value todo! for this metadata, so a file written as V2.2 by this PR cannot be read after rolling back to that released reader.

Could we only enable writing this combination for V2.3+ and keep V2.1/V2.2 returning NotSupported? It would also be worth covering V2.1/V2.2 rejection and V2.3 round-trip in the versioned tests. The reader side can remain permissive.

Fair point! Went ahead and switched this to be V2.3 only - and also added a test to verify failure pre V2.3 (and updated the new tests to point to V2.3 for round trip conversion and all that jazz)

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

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

Caution

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

⚠️ Outside diff range comments (4)
rust/lance-encoding/src/encodings/physical/packed.rs (4)

294-320: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject or preserve struct validity before packing.

check_struct_validity accepts StructDataBlock values with a validity bitmap, but the packed representation and decompressor return validity: None. Nullable structs can therefore round-trip with their null mask silently discarded.

🤖 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 294 - 320,
Update check_struct_validity to reject StructDataBlock inputs with a non-None
validity bitmap before packing, since packed structs cannot preserve it. Return
an appropriate invalid-input error while retaining the existing child-count and
value-count validation for structs without validity.

Source: Coding guidelines


246-282: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject descending offsets before subtraction and slicing.

Both branches need to validate start <= end (and the next offset index) before computing end - start; malformed offsets can otherwise panic instead of returning a validation error.

As per coding guidelines, validate API-boundary inputs and include row, start, end, and buffer-size context in the error.

🤖 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 246 - 282,
Update both 32-bit and 64-bit branches in the packed variable-child decoding
logic to validate the next offset index and require start <= end before
subtracting or slicing. Return an invalid-input error for malformed offsets,
including row, start, end, and buffer length context, while preserving the
existing bounds validation for end.

Source: Coding guidelines


692-774: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make packed-row cursor arithmetic fallible.

cursor + ... and cursor + len can overflow before the bounds checks, and try_into().expect(...) can panic while decoding malformed data. Use checked_add and fallible byte conversion; report persisted-format failures as corruption errors.

As per coding guidelines, library decoding must not use expect() for fallible operations and must use checked arithmetic with contextual errors.

🤖 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 692 - 774,
Update the packed-row decoding logic around the FieldAccumulator fixed and
variable branches to use checked_add for every cursor/end and cursor/length
calculation, returning contextual corruption errors on overflow before slicing
or bounds checks. Replace the u32/u64 byte-slice try_into().expect(...)
conversions with fallible handling that reports persisted-format corruption
errors, and apply the same safeguards to both Variable32 and Variable64 paths.

Source: Coding guidelines


467-474: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Validate child alignment before the row loop.

For an empty struct block, append_row_bytes is never called, so non-byte-aligned children bypass validation while bits_per_row / 8 truncates the width. This can emit metadata that the decoder rejects.

🤖 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 467 - 474,
Validate every child’s byte alignment before entering the row loop in the
surrounding encoding function, including when num_values is zero. Reuse the same
alignment condition enforced by append_row_bytes, return the existing validation
error for any non-byte-aligned child, and only then compute bytes_per_row and
append row data.

Source: Coding guidelines

🟡 Other comments (1)
rust/lance-encoding/src/encodings/physical/packed.rs-1574-1607 (1)

1574-1607: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the version-gating error message too.

The test should verify both Error::NotSupported and that the message identifies the Lance V2.3 requirement.

As per coding guidelines, tests must assert both the error variant and message content.

🤖 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 1574 -
1607, Update fixed_per_value_packed_struct_requires_v23 to inspect the returned
Error::NotSupported value and assert its message states that Lance V2.3 is
required, while preserving the existing variant assertion.

Source: Coding guidelines

🤖 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/encodings/physical/packed.rs`:
- Around line 294-320: Update check_struct_validity to reject StructDataBlock
inputs with a non-None validity bitmap before packing, since packed structs
cannot preserve it. Return an appropriate invalid-input error while retaining
the existing child-count and value-count validation for structs without
validity.
- Around line 246-282: Update both 32-bit and 64-bit branches in the packed
variable-child decoding logic to validate the next offset index and require
start <= end before subtracting or slicing. Return an invalid-input error for
malformed offsets, including row, start, end, and buffer length context, while
preserving the existing bounds validation for end.
- Around line 692-774: Update the packed-row decoding logic around the
FieldAccumulator fixed and variable branches to use checked_add for every
cursor/end and cursor/length calculation, returning contextual corruption errors
on overflow before slicing or bounds checks. Replace the u32/u64 byte-slice
try_into().expect(...) conversions with fallible handling that reports
persisted-format corruption errors, and apply the same safeguards to both
Variable32 and Variable64 paths.
- Around line 467-474: Validate every child’s byte alignment before entering the
row loop in the surrounding encoding function, including when num_values is
zero. Reuse the same alignment condition enforced by append_row_bytes, return
the existing validation error for any non-byte-aligned child, and only then
compute bytes_per_row and append row data.

---

Other comments:
In `@rust/lance-encoding/src/encodings/physical/packed.rs`:
- Around line 1574-1607: Update fixed_per_value_packed_struct_requires_v23 to
inspect the returned Error::NotSupported value and assert its message states
that Lance V2.3 is required, while preserving the existing variant assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 4d133d86-f09a-43a8-b4df-c895487d70b9

📥 Commits

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

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

@morales-t-netflix

Copy link
Copy Markdown
Contributor Author

Updated! Seeing tests fail - but think this is because main has this issue - seeing this across many other PRs :sweat-smile:

@morales-t-netflix

morales-t-netflix commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Howdy @Xuanwo!

Know there was some innovation in the enocder space through this stack of PRs. Wanted to check your thoughts on me adding onto that, allowing this encoding in V2.3?

(Pending feedback - will integrate those changes, and fix the build)

@Xuanwo

Xuanwo commented Aug 5, 2026

Copy link
Copy Markdown
Member

Hi, @morales-t-netflix, Sorry for the late. Now this PR has been merged, feel free to just rebase. Thank you!

@morales-t-netflix
morales-t-netflix force-pushed the tmorales/compression_fixed_length_structs branch from 5129a81 to 8b18afc Compare August 5, 2026 15:32
@morales-t-netflix

Copy link
Copy Markdown
Contributor Author

Thanks @Xuanwo! Just rebased and tests are 🟢

@morales-t-netflix

Copy link
Copy Markdown
Contributor Author

Howdy! Wanted to give this one a friendly bump, as this would be very helpful for some work in my world 😄. Thank you again for all your help! @Xuanwo

@morales-t-netflix

Copy link
Copy Markdown
Contributor Author

Another friendly bump on this one 😄

@morales-t-netflix
morales-t-netflix force-pushed the tmorales/compression_fixed_length_structs branch from 8b18afc to 022e013 Compare August 12, 2026 13:52
@Xuanwo
Xuanwo merged commit c409302 into lance-format:main Aug 17, 2026
38 of 39 checks passed
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.

3 participants