Skip to content

feat(filter): keep the scalar index for comparisons of a column cast to a float - #10820

Merged
Xuanwo merged 6 commits into
lance-format:mainfrom
jonasdedden:lance-predicate-pushdown-m0bezu-cast-unwrap
Nov 14, 2026
Merged

Xuanwo merged 6 commits into
lance-format:mainfrom
jonasdedden:lance-predicate-pushdown-m0bezu-cast-unwrap

Conversation

@jonasdedden

@jonasdedden jonasdedden commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #9333, which left explicit column casts open. A comparison of a column cast to a float now plans on the column itself, so its scalar index is used. DataFusion's own cast unwrapping doesn't cover float targets.

CAST(i AS double) > 2.5                -- i >= 3
TRY_CAST(i AS double) = 3.0            -- i = 3
CAST(i AS double) = 9007199254740992.0 -- i >= 9007199254740992 AND i < 9007199254740994
CAST(i AS double) IN (1.0, 2.5, 3.0)   -- i IN (1, 3)
CAST(u8 AS double) < -1.0              -- never true; NULL stays NULL
CAST(f32 AS double) > 0.1              -- f32 > 0.099999994 (largest f32 below 0.1)
f32 > CAST(0.1 AS double)              -- same: coercion casts the column here

The result keeps the semantics of the cast that was written, unlike #9333's exact comparison of a bare i > 2.5:

  • An integer is compared after rounding to the float type. Rounding is monotonic, so each comparison selects one interval of integers, found by binary search. Above 2^53, = 2^53 therefore also matches 2^53 + 1, as the cast does.
  • A Float32 widens exactly. A bound Float32 can't represent becomes the nearest Float32 below it, and = against it becomes constant.
  • NaN keeps its place above every number. A Float32 compared with NaN is left alone.

BETWEEN, IN and NOT forms are covered. An IN list stays one flat list on the column: where several integers round to a value (past 2^53, or 2^24 for Float32), up to 16 of them are listed individually, so CAST(i AS double) IN (2^53) becomes i IN (2^53, 2^53 + 1). Only wider intervals, near ±2^63, become ranges, OR'd as a balanced tree so long lists don't build deep expressions. The rewrite runs after coercion and before the float comparison rewrite.

Tests compare every operator row by row against the rounded or widened value. They cover Int8/Int32/Int64/UInt64 and Float32 against boundary literals (±2^53, ±2^63, 2^64, ±inf, ±NaN, values Float32 can't represent) and assert that no cast is left. A BTREE dataset test compares indexed and unindexed scans.

Validation on the merged tree (main e0aacd774): the PR selectors pass (widened_ 39, float_literal_on_integer_key 4, test_btree_nullable_filters_match_unindexed_scan 1), as do the current parse_filter_with_schema, session-planner, and optimizing_twice_changes_nothing (14) tests. cargo fmt --all and full clippy with -D warnings are clean.

The author-reported interval-heavy probe (1,000 IN values above 2^53 over 32,768 Int64 rows) evaluates in 0.11–0.12 ms after e7a6674. Intervals wider than 16 integers still stay scan-only, so this is not a universal speedup. DataFusion's nested casts for CAST(i AS float) against an untyped Float64 fractional literal remain correct but unoptimized.

…to a float

`CAST(i AS double) > 2.5`, `TRY_CAST(i AS double) = 3.0` and
`CAST(f32 AS double) > 0.1` now plan on the column instead of on the cast,
so its scalar index is used. DataFusion's own cast unwrapping does not cover
float targets. This is the follow-up lance-format#9333 left open.

The rewrite keeps the semantics of the cast that was written:

- An integer is compared after rounding it to the float type. Rounding is
  monotonic, so every comparison selects one interval of integers, found by
  binary search: `CAST(i AS double) = 9007199254740992.0` becomes
  `i >= 9007199254740992 AND i < 9007199254740994`.
- A Float32 widens exactly. A bound it cannot represent becomes the nearest
  Float32 below it, and `=` / `!=` against it become constant (NULL rows stay
  NULL). This also covers `f32 > CAST(0.1 AS double)`, which coercion turns
  into a cast of the column.
- NaN keeps its place above every number; a Float32 compared with a NaN is
  left alone, since the comparison also orders NaN payloads.

`BETWEEN` and `IN` are handled too. The rewrite runs after coercion and
before the float comparison rewrite.
`IN` becomes an `OR` of the values' equalities, which the simplifier
gathers back into a list, `!=` negates `=`, and the integer interval
helpers are one function.

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@github-actions github-actions Bot added the enhancement New feature or request label Nov 7, 2026
lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added the K-changes Latest Gatekeeper recommendation requests changes. label Nov 7, 2026
`CAST(i AS double) IN (...)` was rewritten into one `OR` level per list
value, so a 10,000-value list overflowed the stack in the passes that
walk the filter afterwards. Values that a single integer rounds to now
stay an `IN` list on the column, and the intervals that several integers
round to are OR'd as a balanced tree.
`tenant = 1.5` is coerced to `CAST(tenant AS double) = 1.5`, which the
widening cast rewrite now turns back into a comparison on the key: an
empty range here, and a plain equality for `tenant = 2.0`.
@github-actions github-actions Bot added the A-index Vector index, linalg, tokenizer label Nov 7, 2026
@lance-gatekeeper lance-gatekeeper Bot removed the K-changes Latest Gatekeeper recommendation requests changes. label Nov 7, 2026
lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added K-approved Latest Gatekeeper recommendation permits acceptance. K-risk Latest Gatekeeper recommendation includes a non-blocking risk. labels Nov 7, 2026
Past 2^53 several integers round to one double, and each such value of
`CAST(i AS double) IN (...)` became a range of its own, evaluated as
separate comparisons. The integers are now listed when at most 16 round
to the value, which covers doubles up to 2^57 and Float32 up to 2^28,
so the filter stays one hash lookup and one index search. Wider
intervals keep the balanced ranges.
@lance-gatekeeper lance-gatekeeper Bot removed K-risk Latest Gatekeeper recommendation includes a non-blocking risk. K-approved Latest Gatekeeper recommendation permits acceptance. labels Nov 7, 2026
lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added K-approved Latest Gatekeeper recommendation permits acceptance. K-risk Latest Gatekeeper recommendation includes a non-blocking risk. labels Nov 7, 2026
@lance-gatekeeper lance-gatekeeper Bot removed K-risk Latest Gatekeeper recommendation includes a non-blocking risk. K-approved Latest Gatekeeper recommendation permits acceptance. labels Nov 14, 2026

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

⚠️ Gate recommendation: approve with a non-blocking risk.

The merged revision preserves cast rounding and NULL behavior while exposing scalar-index predicates. The accepted trade-off remains: rounding intervals containing more than 16 integers use balanced range predicates, so long lists can cost more on scans without an index.

@lance-gatekeeper lance-gatekeeper Bot added K-approved Latest Gatekeeper recommendation permits acceptance. K-risk Latest Gatekeeper recommendation includes a non-blocking risk. labels Nov 14, 2026
@Xuanwo
Xuanwo merged commit d27fd01 into lance-format:main Nov 14, 2026
41 of 43 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-index Vector index, linalg, tokenizer enhancement New feature or request K-approved Latest Gatekeeper recommendation permits acceptance. K-risk Latest Gatekeeper recommendation includes a non-blocking risk.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants