Skip to content

chore: upgrade DataFusion to 54#7793

Merged
wjones127 merged 5 commits into
lance-format:mainfrom
wjones127:chore/upgrade-datafusion-54
Jul 15, 2026
Merged

chore: upgrade DataFusion to 54#7793
wjones127 merged 5 commits into
lance-format:mainfrom
wjones127:chore/upgrade-datafusion-54

Conversation

@wjones127

@wjones127 wjones127 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

geodatafusion 0.5.0 supports DataFusion 54, which was the last remaining blocker. This bumps DataFusion 53 → 54, and transitively geodatafusion 0.4 → 0.5, sqlparser 0.61 → 0.62, and substrait 0.62 → 0.63. Arrow stays at 58.

Migration for DataFusion 54 breaking changes

  • as_any was removed from the ExecutionPlan, PhysicalExpr, ScalarUDFImpl, TableProvider, SchemaProvider, CatalogProvider, and CatalogProviderList traits. The impls are dropped and downcasting now uses the inherent dyn Trait::downcast_ref/is (e.g. plan.downcast_ref::<T>() instead of plan.as_any().downcast_ref::<T>()).
  • ExecutionPlan::partition_statistics now returns Arc<Statistics>.
  • Expr::Cast/TryCast hold a FieldRef instead of a DataType.
  • AnalyzeExec::new gained a metric_categories argument and MetricType::SUMMARY was renamed to MetricType::Summary.
  • sqlparser 0.62 wraps the LIKE escape character in ValueWithSpan.
  • substrait 0.63 adds RexType::Lambda/LambdaInvocation variants and replaces extension URIs with URNs.
  • Migrated the deprecated SimplifyContext::default() builder to SimplifyContext::builder().build().

create_aggregate_expr_and_maybe_filter is retained under #[allow(deprecated)]; migrating to LoweredAggregateBuilder is left as follow-up.

Verification

  • cargo clippy --all --tests --benches -- -D warnings clean, plus -p lance-datafusion --features substrait.
  • lance-datafusion substrait tests pass (exercising the URN and RexType changes).
  • Python (pylance) and Java (lance-jni) bindings compile; all three lockfiles refreshed.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added support for DataFusion 54 and GeoDataFusion 0.5.
    • Enabled higher-order functions in SQL planning.
  • Bug Fixes
    • Improved compatibility with updated DataFusion casting and execution-plan/statistics behavior.
    • Updated Substrait conversion to use the current extension identifier format.
    • Added validation to reject unsupported lambda expressions in filter conditions.
  • Tests
    • Refreshed planning/explain and count-pushdown assertions to match the latest behavior.

geodatafusion 0.5.0 supports DataFusion 54, which was the last remaining
blocker. This bumps DataFusion 53 -> 54, and transitively geodatafusion
0.4 -> 0.5, sqlparser 0.61 -> 0.62, and substrait 0.62 -> 0.63. Arrow
stays at 58.

Migration for DataFusion 54 breaking changes:

- `as_any` was removed from the `ExecutionPlan`, `PhysicalExpr`,
  `ScalarUDFImpl`, `TableProvider`, `SchemaProvider`, `CatalogProvider`,
  and `CatalogProviderList` traits. Drop the impls and downcast via the
  inherent `dyn Trait::downcast_ref`/`is` instead of `.as_any().downcast_ref()`.
- `ExecutionPlan::partition_statistics` now returns `Arc<Statistics>`.
- `Expr::Cast`/`TryCast` hold a `FieldRef` instead of a `DataType`.
- `AnalyzeExec::new` gained a `metric_categories` argument and
  `MetricType::SUMMARY` was renamed to `MetricType::Summary`.
- sqlparser 0.62 wraps the LIKE escape character in `ValueWithSpan`.
- substrait 0.63 adds `RexType::Lambda`/`LambdaInvocation` and replaces
  extension URIs with URNs.
- Migrate the deprecated `SimplifyContext::default()` builder to
  `SimplifyContext::builder().build()`.

`create_aggregate_expr_and_maybe_filter` is kept under `#[allow(deprecated)]`;
migrating to `LoweredAggregateBuilder` is left as follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

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: 39da8bfd-0136-43bd-9d37-c716c3929550

📥 Commits

Reviewing files that changed from the base of the PR and between 2ebc588 and 7930bf0.

📒 Files selected for processing (3)
  • rust/lance/src/io/exec/count_from_mask.rs
  • rust/lance/src/io/exec/filtered_read.rs
  • rust/lance/src/io/exec/rowids.rs

📝 Walkthrough

Walkthrough

This PR upgrades DataFusion integrations from 53 to 54, updates planner and Substrait APIs, migrates execution-plan statistics and downcasting interfaces, and adjusts scalar expression handling, tests, and language bindings.

Changes

DataFusion 54 compatibility

Layer / File(s) Summary
Dependency version updates
Cargo.toml, java/lance-jni/Cargo.toml, python/Cargo.toml, python/pyproject.toml
DataFusion dependencies move to 54.0.0, with geodatafusion moving to 0.5.0.
Planner, expression, and Substrait updates
rust/lance-datafusion/src/planner.rs, rust/lance-datafusion/src/substrait.rs, rust/lance-index/src/scalar/*
Planner constructors, higher-order function metadata, simplification contexts, cast handling, Substrait URNs, lambda validation, and scalar expression tests are updated.
Execution-plan contract migration
rust/lance-datafusion/src/exec.rs, rust/lance-namespace-datafusion/src/*, rust/lance/src/datafusion/*, rust/lance/src/dataset/*, rust/lance/src/io/exec/*
Explicit as_any overrides are removed and partition statistics return Arc<Statistics> where required.
Downcast and validation updates
rust/lance/src/io/exec/*, rust/lance/src/dataset/write/merge_insert.rs, rust/lance/src/dataset/scanner.rs, rust/lance/tests/*
Physical-plan traversal uses direct downcast and type-check helpers, with related test assertions and deprecated API annotations updated.

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

Suggested reviewers: westonpace, xuanwo, wkalt

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.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: upgrading DataFusion to version 54.
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 added A-python Python bindings A-index Vector index, linalg, tokenizer A-java Java bindings + JNI A-deps Dependency updates A-namespace Namespace impls chore labels Jul 14, 2026
wjones127 and others added 3 commits July 14, 2026 16:41
The substrait 0.63 bump removed the extension-URI API
(`SimpleExtensionUri`, `Plan.extension_uris`,
`ExtensionFunction.extension_uri_reference`) in favor of URNs. This test
file was missed in the DF54 migration because it only compiles under the
`substrait` feature, which the default workspace clippy/build does not
enable.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…rder-independent

DataFusion 54's optimizer reorders the pushed-down HashJoinExec
projection in the full-schema delete-by-source case, moving
`__merge_source_sentinel` ahead of `_rowid` to match the consuming
ProjectionExec's column order. The plan is still correct (indices line
up), but the positional `projection=[_rowid` assertion no longer held.
Assert instead that the join keeps a pushed-down projection containing
`_rowid`, which is what the test actually cares about.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
pylance now exports a datafusion-ffi 54 TableProvider, but the PyPI
`datafusion` package was pinned to 53. The mismatched FFI struct layout
segfaulted `test_table_provider.py::test_table_loading` when
datafusion 53 consumed the v54 provider via `register_table`. Bump the
dependency (and the test-group pin) to 54 so both sides share the same
FFI ABI.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@wjones127
wjones127 marked this pull request as ready for review July 15, 2026 01:24
@wjones127
wjones127 requested a review from Xuanwo July 15, 2026 01:24

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

🤖 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/planner.rs`:
- Around line 797-800: Update the escape handling shared by SQLExpr::ILike and
SQLExpr::Like so a SingleQuotedString is accepted only when it contains exactly
one character; reject empty or multi-character strings with an input error
instead of using char.chars().next() and silently altering the predicate.

In `@rust/lance-datafusion/src/substrait.rs`:
- Around line 167-169: Ensure Lambda and LambdaInvocation expressions are
rejected on every expression-parse path, not only when schema references are
remapped. Update the parsing flow around remap_expr_references() to traverse and
validate expressions unconditionally while preserving remapping for pruned
schemas, and add a regression test using an unpruned schema that verifies the
invalid-input error.

In `@rust/lance/src/io/exec/count_from_mask.rs`:
- Around line 512-516: Move the create_aggregate_expr_and_maybe_filter import
out of count_star_expr and into the mod tests import block, keeping the
deprecation allowance applied to both the import and its use in the function.

In `@rust/lance/src/io/exec/filtered_read.rs`:
- Around line 2057-2060: Replace the unconditional Statistics clones at both
mutation sites with Arc::unwrap_or_clone so unique Arc allocations are reused:
update the partition_statistics handling in
rust/lance/src/io/exec/filtered_read.rs (lines 2057-2060) and the corresponding
.as_ref().clone() in rust/lance/src/io/exec/rowids.rs (line 291).
🪄 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: 9dceccfe-8778-492d-93b1-90a1d2522a23

📥 Commits

Reviewing files that changed from the base of the PR and between 45bc880 and 2ebc588.

⛔ Files ignored due to path filters (4)
  • Cargo.lock is excluded by !**/*.lock
  • java/lance-jni/Cargo.lock is excluded by !**/*.lock
  • python/Cargo.lock is excluded by !**/*.lock
  • python/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (46)
  • Cargo.toml
  • java/lance-jni/Cargo.toml
  • python/Cargo.toml
  • python/pyproject.toml
  • rust/lance-datafusion/src/exec.rs
  • rust/lance-datafusion/src/planner.rs
  • rust/lance-datafusion/src/substrait.rs
  • rust/lance-index/src/scalar/btree.rs
  • rust/lance-index/src/scalar/expression.rs
  • rust/lance-index/src/scalar/rtree/sort/hilbert_sort.rs
  • rust/lance-namespace-datafusion/src/catalog.rs
  • rust/lance-namespace-datafusion/src/schema.rs
  • rust/lance/src/datafusion/dataframe.rs
  • rust/lance/src/datafusion/logical_plan.rs
  • rust/lance/src/dataset/mem_wal/memtable/scanner/exec/brute_force_vector.rs
  • rust/lance/src/dataset/mem_wal/memtable/scanner/exec/btree.rs
  • rust/lance/src/dataset/mem_wal/memtable/scanner/exec/dedup_scan.rs
  • rust/lance/src/dataset/mem_wal/memtable/scanner/exec/fts.rs
  • rust/lance/src/dataset/mem_wal/memtable/scanner/exec/scan.rs
  • rust/lance/src/dataset/mem_wal/memtable/scanner/exec/vector.rs
  • rust/lance/src/dataset/mem_wal/scanner/exec/bloom_guard.rs
  • rust/lance/src/dataset/mem_wal/scanner/exec/coalesce_first.rs
  • rust/lance/src/dataset/mem_wal/scanner/exec/generation_tag.rs
  • rust/lance/src/dataset/mem_wal/scanner/exec/pk_block_filter.rs
  • rust/lance/src/dataset/scanner.rs
  • rust/lance/src/dataset/schema_evolution/optimize.rs
  • rust/lance/src/dataset/tests/dataset_aggregate.rs
  • rust/lance/src/dataset/udtf.rs
  • rust/lance/src/dataset/write/merge_insert.rs
  • rust/lance/src/dataset/write/merge_insert/exec/delete.rs
  • rust/lance/src/dataset/write/merge_insert/exec/write.rs
  • rust/lance/src/io/exec/count_from_mask.rs
  • rust/lance/src/io/exec/count_pushdown.rs
  • rust/lance/src/io/exec/filter.rs
  • rust/lance/src/io/exec/filtered_read.rs
  • rust/lance/src/io/exec/fts.rs
  • rust/lance/src/io/exec/knn.rs
  • rust/lance/src/io/exec/optimizer.rs
  • rust/lance/src/io/exec/pushdown_scan.rs
  • rust/lance/src/io/exec/rowids.rs
  • rust/lance/src/io/exec/scalar_index.rs
  • rust/lance/src/io/exec/scan.rs
  • rust/lance/src/io/exec/take.rs
  • rust/lance/src/io/exec/testing.rs
  • rust/lance/src/io/exec/utils.rs
  • rust/lance/tests/count_pushdown/mod.rs
💤 Files with no reviewable changes (12)
  • rust/lance/src/dataset/udtf.rs
  • rust/lance/src/dataset/mem_wal/scanner/exec/coalesce_first.rs
  • rust/lance/src/io/exec/testing.rs
  • rust/lance-index/src/scalar/rtree/sort/hilbert_sort.rs
  • rust/lance/src/dataset/write/merge_insert/exec/delete.rs
  • rust/lance/src/dataset/write/merge_insert/exec/write.rs
  • rust/lance-namespace-datafusion/src/catalog.rs
  • rust/lance/src/dataset/mem_wal/scanner/exec/pk_block_filter.rs
  • rust/lance/src/dataset/mem_wal/scanner/exec/bloom_guard.rs
  • rust/lance/src/dataset/mem_wal/scanner/exec/generation_tag.rs
  • rust/lance-namespace-datafusion/src/schema.rs
  • rust/lance/src/io/exec/utils.rs

Comment on lines +797 to +800
Some(ValueWithSpan {
value: Value::SingleQuotedString(char),
..
}) => char.chars().next(),

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file and inspect the relevant regions with line numbers.
wc -l rust/lance-datafusion/src/planner.rs
sed -n '760,840p' rust/lance-datafusion/src/planner.rs
sed -n '800,840p' rust/lance-datafusion/src/planner.rs
sed -n '1200,1245p' rust/lance-datafusion/src/planner.rs

# Search for LIKE/escape related helpers and tests.
rg -n "ESCAPE|escape|Like" rust/lance-datafusion/src/planner.rs rust/lance-datafusion -g '!**/target/**'

Repository: lance-format/lance

Length of output: 10362


🏁 Script executed:

#!/bin/bash
set -euo pipefail
wc -l rust/lance-datafusion/src/planner.rs
sed -n '760,840p' rust/lance-datafusion/src/planner.rs
sed -n '1200,1245p' rust/lance-datafusion/src/planner.rs
rg -n "ESCAPE|escape|Like" rust/lance-datafusion/src/planner.rs rust/lance-datafusion -g '!**/target/**'

Repository: lance-format/lance

Length of output: 8744


🏁 Script executed:

echo test

Repository: lance-format/lance

Length of output: 161


Reject invalid LIKE escape strings. Both SQLExpr::ILike and SQLExpr::Like should require exactly one escape character; char.chars().next() turns '' into None and truncates 'ab' to 'a', which changes predicate behavior instead of returning an input 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-datafusion/src/planner.rs` around lines 797 - 800, Update the
escape handling shared by SQLExpr::ILike and SQLExpr::Like so a
SingleQuotedString is accepted only when it contains exactly one character;
reject empty or multi-character strings with an input error instead of using
char.chars().next() and silently altering the predicate.

Source: Coding guidelines

Comment on lines +167 to +169
RexType::Lambda(_) | RexType::LambdaInvocation(_) => Err(Error::invalid_input(
"Lambda expressions not allowed in filter expression",
)),

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="rust/lance-datafusion/src/substrait.rs"

# Map the file structure first.
ast-grep outline "$FILE" --view expanded

echo
echo "----- relevant slices -----"
sed -n '120,240p' "$FILE"
echo
sed -n '520,620p' "$FILE"
echo
sed -n '840,940p' "$FILE"

Repository: lance-format/lance

Length of output: 15547


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for lambda-related validation and remapping call sites.
rg -n "Lambda|LambdaInvocation|remap_expr_references|parse_substrait|extension-type|schema length|filter" rust/lance-datafusion/src/substrait.rs

Repository: lance-format/lance

Length of output: 4326


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="rust/lance-datafusion/src/substrait.rs"

# Print only the parse_substrait function and the remap_expr_references helper.
python3 - <<'PY'
from pathlib import Path
path = Path("rust/lance-datafusion/src/substrait.rs")
lines = path.read_text().splitlines()
for start, end, label in [(1, 260, "top"), (520, 640, "middle"), (840, 960, "bottom")]:
    print(f"\n===== {label} {start}-{end} =====")
    for i in range(start, min(end, len(lines)) + 1):
        print(f"{i:4d}: {lines[i-1]}")
PY

Repository: lance-format/lance

Length of output: 23773


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '267,330p' rust/lance-datafusion/src/substrait.rs

Repository: lance-format/lance

Length of output: 2559


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
path = Path("rust/lance-datafusion/src/substrait.rs")
for i, line in enumerate(path.read_text().splitlines(), start=1):
    if 267 <= i <= 330:
        print(f"{i:4d}: {line}")
PY

Repository: lance-format/lance

Length of output: 2943


Reject lambda expressions on every parse path. Lambda and LambdaInvocation are only rejected inside remap_expr_references(), so plans that don’t drop any schema fields skip this check and can still reach the consumer. Split validation from remapping, or run the traversal unconditionally, and add a regression test with an unpruned schema.

🤖 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-datafusion/src/substrait.rs` around lines 167 - 169, Ensure Lambda
and LambdaInvocation expressions are rejected on every expression-parse path,
not only when schema references are remapped. Update the parsing flow around
remap_expr_references() to traverse and validate expressions unconditionally
while preserving remapping for pruned schemas, and add a regression test using
an unpruned schema that verifies the invalid-input error.

Source: Coding guidelines

Comment thread rust/lance/src/io/exec/count_from_mask.rs Outdated
Comment thread rust/lance/src/io/exec/filtered_read.rs Outdated
Comment on lines +2057 to +2060
let mut df_stats = df_filter_exec
.partition_statistics(partition)?
.as_ref()
.clone();

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 | 🔵 Trivial | ⚡ Quick win

Avoid unconditional deep copies of Arc-wrapped Statistics.

Use Arc::unwrap_or_clone at both mutation sites so unique allocations are reused. This aligns with the DataFusion 54 migration guidance. (datafusion.apache.org)

  • rust/lance/src/io/exec/filtered_read.rs#L2057-L2060: replace .as_ref().clone() with Arc::unwrap_or_clone(...).
  • rust/lance/src/io/exec/rowids.rs#L291-L291: replace .as_ref().clone() with Arc::unwrap_or_clone(...).
📍 Affects 2 files
  • rust/lance/src/io/exec/filtered_read.rs#L2057-L2060 (this comment)
  • rust/lance/src/io/exec/rowids.rs#L291-L291
🤖 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/src/io/exec/filtered_read.rs` around lines 2057 - 2060, Replace
the unconditional Statistics clones at both mutation sites with
Arc::unwrap_or_clone so unique Arc allocations are reused: update the
partition_statistics handling in rust/lance/src/io/exec/filtered_read.rs (lines
2057-2060) and the corresponding .as_ref().clone() in
rust/lance/src/io/exec/rowids.rs (line 291).

Source: MCP tools

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

Nice work, love it!

- Reuse unique Arc<Statistics> allocations via Arc::unwrap_or_clone in the
  partition_statistics overrides (filtered_read, rowids) instead of an
  unconditional deep copy, per the DataFusion 54 upgrade guidance.
- Move the deprecated create_aggregate_expr_and_maybe_filter import out of the
  count_star_expr test helper into the test module's import block.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-deps Dependency updates A-index Vector index, linalg, tokenizer A-java Java bindings + JNI A-namespace Namespace impls A-python Python bindings chore

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants