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
if binary.op() != &Operator::Eq {
continue;
}
// Follow `FilterExec`: only an equality with a column on one side

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 put this sort of logic in a shared place so that we can avoid duplicating diverging bits of code?

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.

Agree with @adriangb. I think the simplest way to avoid two diverging copies is to drop this one: #26967 (approved) fixes the root cause in EquivalenceGroup, and with only #26967 on this PR's base, the #27287 query returns y 1 and your new slt test passes unchanged.

Keeping it also costs a sort: with pushdown_filters = true, equalities with no column on either side no longer form a class at the scan, so WHERE a = 'x' AND upper(a) = upper(b) ORDER BY upper(b) gains a SortExec that main doesn't need. Could you drop the mod.rs change, keep the slt test, and land it after #26967?

Plans that gain a sort with this PR (all have none, or fewer keys, on main and on main + #26967)
SET datafusion.execution.parquet.pushdown_filters = true;
-- t: parquet with VARCHAR columns a, b
EXPLAIN SELECT a, b FROM t WHERE a = 'x' AND upper(a) = upper(b) ORDER BY upper(b);         -- SortExec added
EXPLAIN SELECT a, b FROM t WHERE upper(a) = 'X' AND upper(a) = upper(b) ORDER BY upper(b);  -- SortExec + SPM added
EXPLAIN SELECT a, b FROM t WHERE upper(a) = upper(b) ORDER BY upper(a), upper(b);           -- sorts on 2 keys instead of 1

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

@cetra3 thanks for the fix and the clear write-up. Requesting changes for one reason, explained in the reply on @adriangb's thread: #26967 fixes #27287 at the root, so this PR could keep only the slt test.

Arc::clone(binary.right()),
)?;
} else {
constants.extend(ConstExpr::collect_predicate_constants(

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.

If this scan change stays (for example if #26967 stalls), nothing tests this branch: with it removed, every test in this PR still passes. This plan has no SortExec only because of the branch, so could you add it as an EXPLAIN test?

EXPLAIN SELECT a, b FROM t WHERE upper(a) = 'X' ORDER BY upper(a);

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

3 participants