Skip to content

Fix CASE evaluation for custom column expressions - #24484

Merged
kosiew merged 4 commits into
apache:mainfrom
Hasnaathussain:fix/21231-case-custom-column-projection
Sep 3, 2026
Merged

kosiew merged 4 commits into
apache:mainfrom
Hasnaathussain:fix/21231-case-custom-column-projection

Conversation

@Hasnaathussain

@Hasnaathussain Hasnaathussain commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR address?

Rationale for this change

CaseBody::project discovers input columns by downcasting expression-tree nodes to DataFusion's concrete Column or LambdaVariable. A third-party column-like PhysicalExpr can evaluate an input column without exposing either node. The projection then omits that dependency and CASE evaluates the expression against a batch with the wrong schema.

What changes are included in this PR?

CASE projection now falls back to the original input batch when it encounters an unknown leaf expression. Built-in ScalarFunctionExpr leaves remain eligible for projection because they receive the row count and do not read input columns through the batch. The projection decision is centralized in ProjectedCaseBody::projection_for, with the invariant documented next to the representation.

The regression coverage exercises searched CASE, base-expression CASE, and the single-WHEN expression path with a custom column-like expression. It also checks that a nullary random() scalar function keeps the projection fast path.

This is a conservative correctness fix for the custom-leaf case. The broader dependency-reporting design discussed in #21231 remains separate work.

Are these changes tested?

  • cargo fmt --all -- --check
  • cargo clippy -p datafusion-physical-expr --all-targets --all-features -- -D warnings
  • cargo test -p datafusion-physical-expr expressions::case::tests
  • cargo test -p datafusion-physical-expr

Are there any user-facing changes?

Custom physical expressions can now be evaluated correctly inside searched and base-expression CASE expressions. There is no public API change.

@github-actions github-actions Bot added the physical-expr Changes to the physical-expr crates label Aug 19, 2026
@Hasnaathussain
Hasnaathussain marked this pull request as ready for review August 19, 2026 08:48
Copilot AI lite review requested due to automatic review settings August 19, 2026 08:48

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codecov-commenter

codecov-commenter commented Aug 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.37500% with 49 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.58%. Comparing base (739974b) to head (204806c).
⚠️ Report is 15 commits behind head on main.

Files with missing lines Patch % Lines
datafusion/physical-expr/src/expressions/case.rs 69.37% 30 Missing and 19 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #24484      +/-   ##
==========================================
- Coverage   81.59%   81.58%   -0.01%     
==========================================
  Files        1123     1123              
  Lines      406811   406947     +136     
  Branches   406811   406947     +136     
==========================================
+ Hits       331923   331999      +76     
- Misses      55451    55489      +38     
- Partials    19437    19459      +22     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

@Hasnaathussain,

Thanks for working on this. I tested the CASE expression changes locally and the fix looks correct for the reported custom PhysicalExpr failure. The new regression test also fails as expected if the projection guards are removed, so it is exercising the actual bug.

I left a few non-blocking suggestions below. The main thing I would like us to be explicit about is that this is a conservative fix for the custom leaf case, rather than a complete solution to the broader scope-aware dependency problem discussed in #21231.

Overall, the fallback to evaluating the original CASE body against the full batch looks correct to me.

expr.downcast_ref::<LambdaVariable>()
{
used_column_indices.insert(lambda_variable.index());
} else if expr.downcast_ref::<Literal>().is_none()

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.

I think this check is a little broader than the specific hazard we are trying to protect against. It will also disable CASE projection for some in-tree leaf expressions that cannot actually read the input batch positionally.

For example, a nullary ScalarFunctionExpr such as random() has no children, is not a Literal, and is not const-folded because it is volatile. That means an expression like CASE WHEN random() < 0.5 THEN a ELSE b END would now lose the projection fast path on a wide batch.

Would it make sense to keep known-safe DataFusion leaves projection-enabled, and reserve this fallback for expressions whose dependencies we genuinely cannot determine? If we prefer the conservative behavior for now, I think it would be worth calling out the performance tradeoff in the PR description.

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.

Addressed in 48b72e44c: known-safe ScalarFunctionExpr leaves remain projectable, while unknown leaves use the full-batch path. The regression suite also covers a nullary random() expression.

}

#[test]
fn case_without_expr_with_custom_column() -> Result<()> {

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.

Nice regression test. One thing I noticed is that this currently covers only the NoExpression evaluation path, while the PR adds the same projection guard to case_when_with_expr and expr_or_expr as well.

Could we add small tests for those two shapes too? A base-expression CASE using CustomColumn, plus a single-WHEN-with-ELSE case, would make sure all three guarded paths stay covered if this code is refactored later.

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 coverage for the three guarded evaluation shapes: searched CASE, base-expression CASE, and the single-WHEN-with-ELSE path, each using a column-like custom expression.

struct ProjectedCaseBody {
projection: Vec<usize>,
body: CaseBody,
supports_projection: bool,

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.

Could we document the invariant on supports_projection here? When this is false, the derived projection and rewritten body must not be used, and evaluation needs to fall back to the original CASE body on the full batch.

That relationship is important enough that a short field comment, plus a sentence in the ProjectedCaseBody docs, would make the representation much easier to reason about.

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.

Documented the supports_projection invariant next to the representation and in the ProjectedCaseBody docs: when it is false, evaluation must use the original CASE body against the full batch.

} else if expr.downcast_ref::<Literal>().is_none()
&& expr.children().is_empty()
{
// Unknown leaves may read input columns without exposing a Column child.

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.

This looks like a reasonable conservative fix for the reported crash. One process point though: the discussion in #21231 was also considering a PhysicalExpr dependency-reporting extension point, or a dedicated opt-in/opt-out mechanism for projection.

This PR takes a narrower approach by treating unknown leaves as unsafe. I think that is fine as a targeted fix, but it would be helpful to say so explicitly in the PR description and use wording like Part of #21231 rather than closing the issue entirely. The separate scope-aware traversal problem still remains.

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.

The PR description now calls this Part of #21231 and states the scope explicitly. The dependency-reporting extension discussed in the issue remains separate work.

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.

Small cleanup suggestion: the logic that filters the projection to valid batch indices, checks whether projection is supported, and decides whether projection is worthwhile is now repeated in three places.

A helper on ProjectedCaseBody, something like projection_for(&self, batch) -> Option<Vec<usize>>, could centralize that decision. That would also make it harder for a future CASE evaluation path to accidentally forget the supports_projection check.

@Hasnaathussain
Hasnaathussain force-pushed the fix/21231-case-custom-column-projection branch from d4a8b0f to 48b72e4 Compare August 29, 2026 09:10
@Hasnaathussain

Copy link
Copy Markdown
Contributor Author

Addressed in 48b72e4 and rebased onto current main (0f55902).

  • Centralized the projection decision in ProjectedCaseBody::projection_for and documented the supports_projection invariant.
  • Kept DataFusion ScalarFunctionExpr leaves projectable; unknown leaf expressions still use the full batch.
  • Added regression coverage for searched CASE, base-expression CASE, and the single-WHEN expression path with a column-like custom PhysicalExpr, plus a nullary random() fast-path check.
  • Updated the description to mark this as a conservative part of CaseWhen does not work with custom implemented column expression #21231; the broader dependency-reporting design remains separate work.

The branch is conflict-free after the rebase and the repository checks are running on the new head.

@github-actions github-actions Bot added the auto detected api change Auto detected API change label Aug 29, 2026
@Hasnaathussain

Copy link
Copy Markdown
Contributor Author

The first post-rebase run caught two fixture typos and the clippy-required branch shape. I corrected the ELSE expectations to 40, reordered the imports, and collapsed the nested else-if in 6c1847f. Rust 1.97 validation in WSL passes rustfmt, strict physical-expr clippy, all three custom-column regressions, and the full physical-expr library suite (1,629 passed, 2 ignored). Fresh workflows are currently waiting for the repository approval gate.

@Hasnaathussain
Hasnaathussain force-pushed the fix/21231-case-custom-column-projection branch from 6c1847f to f48c0e9 Compare August 30, 2026 14:42
@Hasnaathussain

Copy link
Copy Markdown
Contributor Author

I refreshed the branch onto current main (74f4dd6fe) after the semver report showed unrelated RangePartitioning API drift between the PR base and current main.

The functional diff is still limited to datafusion/physical-expr/src/expressions/case.rs; the three contributor commits remain DCO-signed. The clean-base validation passes: cargo fmt --all -- --check, the 39 CASE tests, and strict physical-expr clippy with -D warnings. The reviewer’s four live suggestions are addressed in the current source and the PR description, and I replied inline to each thread. The rebased head is f48c0e990.

@github-actions github-actions Bot removed the auto detected api change Auto detected API change label Sep 1, 2026

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

@Hasnaathussain, thanks for addressing the earlier review feedback. I took another pass through the follow-up changes and everything looks good to me.

The conservative CASE projection fallback is now limited appropriately, while known-safe Literal and ScalarFunctionExpr leaves keep the projection fast path. The nullary random() regression test confirms that behavior.

The added tests also cover all three evaluation shapes that use the projection decision: searched CASE, CASE with a base expression, and the single-WHEN-with-ELSE path. The supports_projection invariant is now documented, and ProjectedCaseBody::projection_for gives the evaluation paths a single place to make the projection decision.

I don't see any new correctness, schema, or regression issues in these changes. Looks good to me. Thanks!

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

Thank you for opening this pull request!

Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch).

Details
     Cloning apache/main
    Building datafusion-physical-expr v55.0.0 (current)
       Built [  39.123s] (current)
     Parsing datafusion-physical-expr v55.0.0 (current)
      Parsed [   0.050s] (current)
    Building datafusion-physical-expr v55.0.0 (baseline)
       Built [  31.195s] (baseline)
     Parsing datafusion-physical-expr v55.0.0 (baseline)
      Parsed [   0.050s] (baseline)
    Checking datafusion-physical-expr v55.0.0 -> v55.0.0 (no change; assume patch)
     Checked [   0.341s] 223 checks: 220 pass, 3 fail, 0 warn, 31 skip

--- failure derive_trait_impl_removed: built-in derived trait no longer implemented ---

Description:
A public type has stopped deriving one or more traits. This can break downstream code that depends on those types implementing those traits.
        ref: https://doc.rust-lang.org/reference/attributes/derive.html#derive
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/derive_trait_impl_removed.ron

Failed in:
  type TryCastExpr no longer derives Clone, in /home/runner/work/datafusion/datafusion/datafusion/physical-expr/src/expressions/try_cast.rs:34

--- failure function_missing: pub fn removed or renamed ---

Description:
A publicly-visible function cannot be imported by its prior path. A `pub use` may have been removed, or the function itself may have been renamed or removed entirely.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/function_missing.ron

Failed in:
  function datafusion_physical_expr::expressions::try_cast_with_target_field, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/465c32999df67c977c2c553cdde2196f0ff0adac/datafusion/physical-expr/src/expressions/try_cast.rs:298

--- failure inherent_method_missing: pub method removed or renamed ---

Description:
A publicly-visible method or associated fn is no longer available under its prior name. It may have been renamed or removed entirely.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/inherent_method_missing.ron

Failed in:
  CastExpr::target_metadata, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/465c32999df67c977c2c553cdde2196f0ff0adac/datafusion/physical-expr/src/expressions/cast.rs:171
  CastExpr::target_nullable, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/465c32999df67c977c2c553cdde2196f0ff0adac/datafusion/physical-expr/src/expressions/cast.rs:176
  CastExpr::has_explicit_metadata, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/465c32999df67c977c2c553cdde2196f0ff0adac/datafusion/physical-expr/src/expressions/cast.rs:204
  CastExpr::has_explicit_nullability, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/465c32999df67c977c2c553cdde2196f0ff0adac/datafusion/physical-expr/src/expressions/cast.rs:209
  TryCastExpr::new_with_target_field, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/465c32999df67c977c2c553cdde2196f0ff0adac/datafusion/physical-expr/src/expressions/try_cast.rs:105
  TryCastExpr::target_metadata, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/465c32999df67c977c2c553cdde2196f0ff0adac/datafusion/physical-expr/src/expressions/try_cast.rs:127
  TryCastExpr::target_field, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/465c32999df67c977c2c553cdde2196f0ff0adac/datafusion/physical-expr/src/expressions/try_cast.rs:136

     Summary semver requires new major version: 3 major and 0 minor checks failed
    Finished [  71.846s] datafusion-physical-expr

@github-actions github-actions Bot added the auto detected api change Auto detected API change label Sep 2, 2026
@kosiew
kosiew added this pull request to the merge queue Sep 3, 2026
@kosiew

kosiew commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🚀
@Hasnaathussain
Thank you for your contribution.

Merged via the queue into apache:main with commit 69a4c72 Sep 3, 2026
41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto detected api change Auto detected API change physical-expr Changes to the physical-expr crates v56.0.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants