Skip to content

fix(index): align scalar index field type support#7966

Open
ddupg wants to merge 1 commit into
lance-format:mainfrom
ddupg:fix/ddu-317-pylance-scalar-index-type-parity
Open

fix(index): align scalar index field type support#7966
ddupg wants to merge 1 commit into
lance-format:mainfrom
ddupg:fix/ddu-317-pylance-scalar-index-type-parity

Conversation

@ddupg

@ddupg ddupg commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Allow PyLance BTree, Bitmap, and ZoneMap indices on LargeBinary, Decimal128, Decimal256, and duration fields.
  • Coerce typed LargeBinary, decimal, and duration literals in Rust so eligible predicates are planned as scalar index queries.
  • Add Decimal128/Decimal256 extrema support for ZoneMap planning.
  • Extend the existing scalar-index type test to cover index planning, query results, and uncommitted segment creation.

Root cause

Python validation was narrower than the Core index capabilities. In addition, some accepted types could create an index but their typed query literals were not coerced by the planner, causing filters to fall back to LanceRead.

Non-goals

Decimal32 and Decimal64 are not included because Lance does not yet support them as dataset types. Their support is tracked separately in #5174.

Testing

  • cargo test -p lance-datafusion expr::tests
  • cargo test -p lance-arrow-stats test_rstest_primitives
  • uv run --python 3.11 pytest python/tests/test_scalar_index.py::test_scalar_index_types
  • uv run make lint
  • cargo fmt --all --check
  • cargo clippy --all --tests --benches -- -D warnings

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Scalar-index validation now accepts binary, large binary, decimal, and duration-related types. Decimal extrema calculation and DataFusion scalar coercion support were added, with expanded Python and Rust test coverage.

Changes

Scalar index type support

Layer / File(s) Summary
Decimal extrema support
rust/arrow-stats/src/lib.rs
Decimal128 and Decimal256 min/max indices now use primitive extrema logic, with corresponding assertions.
Scalar literal coercion
rust/lance-datafusion/src/expr.rs
LargeBinary, matching decimal variants, and duration literals are supported by scalar coercion and tested.
Python scalar-index validation
python/python/lance/dataset.py, python/python/tests/test_scalar_index.py
Scalar-index validation and documentation include binary, decimal, and duration-related types, with parametrized coverage across index types and values.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested labels: enhancement, A-index

Suggested reviewers: xuanwo, westonpace

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly reflects the main change: broadening scalar index type support.
Description check ✅ Passed The description matches the changeset and accurately summarizes the type-support updates and tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 added A-python Python bindings bug Something isn't working labels Jul 24, 2026
@ddupg
ddupg marked this pull request as ready for review July 24, 2026 06:23

@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: 1

Note

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

🟡 Other comments (1)
python/python/lance/dataset.py-3248-3252 (1)

3248-3252: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the rejection error match the expanded validation.

Unsupported columns still receive a split, stale TypeError that omits binary/decimal support and the actual field_type. Raise one message that includes column and field_type.

Proposed fix
-                    raise TypeError(
-                        f"BTREE/BITMAP/ZONEMAP index column {column} must be int",
-                        ", float, bool, str, large_str, fixed-size-binary, or temporal",
-                    )
+                    raise TypeError(
+                        "BTREE/BITMAP/ZONEMAP index column "
+                        f"{column!r} must be boolean, integer, floating, string, "
+                        "binary, fixed-size-binary, decimal, or temporal; "
+                        f"got field_type={field_type}"
+                    )

As per coding guidelines, “Include full error context, including variable names, values, sizes, and types.”

🤖 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 `@python/python/lance/dataset.py` around lines 3248 - 3252, Update the
rejection TypeError in the validation surrounding the field-type checks to use
one message covering all unsupported types, including the column name and actual
field_type value and type. Remove the stale split error wording while preserving
the expanded validation conditions.

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.

Inline comments:
In `@rust/lance-datafusion/src/expr.rs`:
- Around line 451-469: The public safe_coerce_scalar function needs Rustdoc
documenting its LargeBinary, decimal, and duration coercion behavior. Add
examples using its actual signature, with links to ScalarValue, DataType, and
the relevant casting method, and keep the examples synchronized with supported
inputs and return behavior.

---

Other comments:
In `@python/python/lance/dataset.py`:
- Around line 3248-3252: Update the rejection TypeError in the validation
surrounding the field-type checks to use one message covering all unsupported
types, including the column name and actual field_type value and type. Remove
the stale split error wording while preserving the expanded validation
conditions.
🪄 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: QUIET

Plan: Pro Plus

Run ID: a4b45219-5565-4eb2-a8a3-896ca03dcb95

📥 Commits

Reviewing files that changed from the base of the PR and between 71b4515 and ef7a57e.

📒 Files selected for processing (4)
  • python/python/lance/dataset.py
  • python/python/tests/test_scalar_index.py
  • rust/arrow-stats/src/lib.rs
  • rust/lance-datafusion/src/expr.rs

Comment thread rust/lance-datafusion/src/expr.rs
@ddupg
ddupg force-pushed the fix/ddu-317-pylance-scalar-index-type-parity branch 2 times, most recently from 8dd1a15 to 7f0575d Compare July 24, 2026 06:42

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

🧹 Nitpick comments (1)
rust/arrow-stats/src/lib.rs (1)

753-755: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use direct Vec construction for Decimal256Array.

Decimal128Array::from(Vec) is already used here, and Arrow Rust 58 supports Decimal256Array::from(Vec<i256>), so this can match the existing primitive-array style:

Decimal256Array::from(vec![300_i128, 100, 200].into_iter().map(Into::into))
🤖 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/arrow-stats/src/lib.rs` around lines 753 - 755, Update the
Decimal256Array construction in the affected test or example to use
Decimal256Array::from with a Vec<i128> iterator, matching the existing
Decimal128Array construction style, and remove the from_iter_values call while
preserving the values and conversion to i256.

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.

Nitpick comments:
In `@rust/arrow-stats/src/lib.rs`:
- Around line 753-755: Update the Decimal256Array construction in the affected
test or example to use Decimal256Array::from with a Vec<i128> iterator, matching
the existing Decimal128Array construction style, and remove the from_iter_values
call while preserving the values and conversion to i256.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: c05718f6-d085-4bc6-8f41-34fbb46f3c65

📥 Commits

Reviewing files that changed from the base of the PR and between 8dd1a15 and 7f0575d.

📒 Files selected for processing (4)
  • python/python/lance/dataset.py
  • python/python/tests/test_scalar_index.py
  • rust/arrow-stats/src/lib.rs
  • rust/lance-datafusion/src/expr.rs

@ddupg

ddupg commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Could please @Xuanwo help review this? thanks!

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.23529% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
rust/lance-datafusion/src/expr.rs 87.50% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@ddupg
ddupg force-pushed the fix/ddu-317-pylance-scalar-index-type-parity branch from 7f0575d to daa8c63 Compare July 24, 2026 10:07

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

🟡 Other comments (1)
python/python/tests/test_scalar_index.py-758-758 (1)

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

Add scalar-column coverage for RTREE via the uncommitted index route.

create_scalar_index(..., index_type="RTREE") is accepted, but RTREE is routed through create_index_uncommitted(...), and test_scalar_index.py only covers BTREE/BITMAP/ZONEMAP here. Add a valid scalar-column RTREE test calling the segmented uncommitted path, like the BTREE/BITMAP/ZONEMAP cases.

🤖 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 `@python/python/tests/test_scalar_index.py` at line 758, Extend the index_type
coverage loop in test_scalar_index.py to include RTREE and add the corresponding
valid scalar-column case through create_index_uncommitted, matching the existing
BTREE, BITMAP, and ZONEMAP test flow. Ensure the RTREE case invokes the
segmented uncommitted path and preserves the current assertions.

Source: Coding guidelines

🧹 Nitpick comments (1)
rust/arrow-stats/src/lib.rs (1)

753-755: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Construct the primitive array with From<Vec<i256>>.

Use Decimal256Array::from(vec![300_i64.into(), 100_i64.into(), 200_i64.into()]) to avoid the iterator from_iter_values path.

🤖 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/arrow-stats/src/lib.rs` around lines 753 - 755, Replace the
Decimal256Array construction using from_iter_values in the affected test with
the From<Vec<i256>> form, explicitly building a vector of 300_i64, 100_i64, and
200_i64 converted into i256 values. Preserve the existing array contents and
ordering.

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.

Other comments:
In `@python/python/tests/test_scalar_index.py`:
- Line 758: Extend the index_type coverage loop in test_scalar_index.py to
include RTREE and add the corresponding valid scalar-column case through
create_index_uncommitted, matching the existing BTREE, BITMAP, and ZONEMAP test
flow. Ensure the RTREE case invokes the segmented uncommitted path and preserves
the current assertions.

---

Nitpick comments:
In `@rust/arrow-stats/src/lib.rs`:
- Around line 753-755: Replace the Decimal256Array construction using
from_iter_values in the affected test with the From<Vec<i256>> form, explicitly
building a vector of 300_i64, 100_i64, and 200_i64 converted into i256 values.
Preserve the existing array contents and ordering.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: febe9cc3-9682-43b8-86e2-366ddaf6d8fc

📥 Commits

Reviewing files that changed from the base of the PR and between 7f0575d and daa8c63.

📒 Files selected for processing (4)
  • python/python/lance/dataset.py
  • python/python/tests/test_scalar_index.py
  • rust/arrow-stats/src/lib.rs
  • rust/lance-datafusion/src/expr.rs

@Xuanwo Xuanwo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Legacy Decimal ZoneMap indexes can return false negatives after this change.

Before this PR, Rust and Python IndexConfig could create a Decimal ZoneMap even though arrow-stats returned no extrema for Decimal, so non-null zones were persisted with typed-null min/max. This PR makes typed Decimal predicates reach ScalarIndexQuery, while evaluate_zone_against_query treats a missing finite minimum as no match and ZONEMAP_INDEX_VERSION remains 0.

I reproduced this on the current head with a pre-PR index containing [1.00, 2.00, 3.00]: value = arrow_cast(2.00, 'Decimal128(10, 2)') returns 0 rows through ScalarIndexQuery, while the same filter with scalar indexes disabled returns 1 row. The old writer behavior is also present in released v8.0.0.

Could we conservatively include zones whose bounds are null but whose null_count shows non-null values, or otherwise reject/rebuild legacy Decimal v0 indexes? A historical fixture comparing indexed and flat reads for equality/range would cover this compatibility path.

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

Labels

A-python Python bindings bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants