Skip to content

fix: keep a literal out of scan equivalence classes for complex_expr = literal filters - #27288

Open
cetra3 wants to merge 1 commit into
apache:mainfrom
pydantic:fix-scan-filter-literal-equivalence
Open

cetra3 wants to merge 1 commit into
apache:mainfrom
pydantic:fix-scan-filter-literal-equivalence

Conversation

@cetra3

@cetra3 cetra3 commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

Posted by Claude (an AI assistant) on behalf of cetra3.

Which issue does this PR close?

Rationale for this change

With datafusion.execution.parquet.pushdown_filters = true, a query that groups by a CASE key and pins it with HAVING fails planning with a SanityCheckPlan error. See #27287 for a datafusion-cli reproduction.

The cause is in FileScanConfig::add_filter_equivalence_info. It turns every x = y conjunct of the source's exact filter into an equivalence class, including complex_expr = literal. That puts the literal in a class. normalize_expr rewrites bottom-up, so every occurrence of the literal, including the one inside complex_expr itself, becomes the canonical expression of the class. In #27287, b = 'y' inside the CASE becomes b = <the CASE>. The partial aggregate requires its input ordered by the group key, because is_expr_constant finds the key constant. But the normalized requirement no longer matches the class, so ordering_satisfy rejects it.

FilterExec already avoids this. collect_columns_from_predicate_inner keeps only pairs with a column on one side, with a comment that names this exact failure. FilterExec then records complex_expr = literal as a constant through ConstExpr::collect_predicate_constants. The scan path, from #25780, does not have the same guard. Since #26747, ordering checks also consult equivalent expressions, which is where the bisect in #27287 points.

What changes are included in this PR?

add_filter_equivalence_info (datafusion/datasource/src/file_scan_config/mod.rs) follows FilterExec:

  • An equality with a column on one side becomes an equivalence class, as before.
  • Any other equality goes through ConstExpr::collect_predicate_constants, so complex_expr = literal makes complex_expr a constant without putting the literal in a class.

Only the second kind of conjunct changes. I first added constants for every conjunct, as FilterExec does. That changed one expected plan in push_down_filter_regression.slt (group_clustering_mode=Partial([1]) disappeared), so this PR leaves column equalities as they were.

What is the testing strategy for this PR?

Are there any user-facing changes?

The query shape in #27287 plans and runs. No public API changes.

🤖 Generated with Claude Code

…= literal filters

`FileScanConfig::add_filter_equivalence_info` turned every equality of the
source's exact filter into an equivalence class. For `complex_expr = literal`
that put the literal in a class, and normalization then replaced the literal
inside `complex_expr` with the class's canonical expression. A partial
aggregate that relied on the constant group key then failed `SanityCheckPlan`.

Follow `FilterExec`: only an equality with a column on one side becomes an
equivalence class, and any other equality is recorded as a constant.

Closes apache#27287.

Claude-Session: https://claude.ai/code/session_01USkuh48jV15LVKgyqXmCod
@github-actions github-actions Bot added sqllogictest SQL Logic Tests (.slt) datasource Changes to the datasource crate labels Nov 10, 2026
cetra3 added a commit to pydantic/datafusion that referenced this pull request Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

datasource Changes to the datasource crate sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

REGRESSION: SanityCheckPlan error for GROUP BY ... HAVING on a CASE key with parquet filter pushdown

1 participant