Repository navigation
fix: match functional dependencies by column position, not by name - #26150
jayzhan211 wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The new passthrough helper can incorrectly resolve ambiguous unqualified columns to the first matching field.
1 open finding
What changed in this PR
Fixes functional-dependency matching by using field positions rather than potentially colliding expression names.
Changes:
- Adds index-based dependency helpers and passthrough-column resolution.
- Updates aggregate, projection, GROUP BY, and ORDER BY handling.
- Adds regression tests and migration documentation.
| File | Description |
|---|---|
docs/source/library-user-guide/upgrading/56.0.0.md |
Documents the API migration. |
datafusion/sqllogictest/test_files/functional_dependencies.slt |
Adds CAST regression coverage. |
datafusion/optimizer/src/optimize_projections/mod.rs |
Uses field indices for GROUP BY pruning. |
datafusion/optimizer/src/eliminate_duplicated_expr.rs |
Uses field indices for sort pruning. |
datafusion/expr/src/utils.rs |
Adds passthrough-field resolution. |
datafusion/expr/src/logical_plan/plan.rs |
Updates aggregate and projection dependencies. |
datafusion/expr/src/logical_plan/builder.rs |
Updates implicit GROUP BY expansion. |
datafusion/common/src/functional_dependencies.rs |
Converts dependency helpers to index-based APIs. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| /// expressions can have the same name, e.g. `CAST(t.a AS INT)` is named `t.a`. | ||
| pub fn passthrough_field_index(expr: &Expr, schema: &DFSchema) -> Option<usize> { | ||
| match expr { | ||
| Expr::Column(col) => schema.maybe_index_of_column(col), |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #26150 +/- ##
==========================================
- Coverage 82.76% 82.76% -0.01%
==========================================
Files 1147 1147
Lines 450580 450563 -17
Branches 450580 450563 -17
==========================================
- Hits 372944 372921 -23
- Misses 54929 54932 +3
- Partials 22707 22710 +3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
kosiew
left a comment
There was a problem hiding this comment.
Thanks for working on this. The functional dependency fix looks sound, and the new regression tests cover the reported CAST issue. I have one optional suggestion for additional test coverage, but it is not blocking. Approving this change.
|
|
||
| # 6.4 `y` is not determined by the GROUP BY expression, so it can't be selected. | ||
| query error DataFusion error: Error during planning: Column in SELECT must be in GROUP BY or an aggregate function | ||
| SELECT y, count(*) FROM t_cast GROUP BY CAST(x AS INT); |
There was a problem hiding this comment.
Could we also add a regression test for TRY_CAST over a UNIQUE NOT NULL key? For example, ('bad-a', 'b') and ('bad-b', 'a') both produce NULL when cast to INT, so the test could verify that ORDER BY retains y, GROUP BY preserves both groups, and selecting y when grouping only by the cast fails planning. This is optional coverage since the new helper already handles TRY_CAST conservatively.

Which issue does this PR close?
Rationale for this change
If
xis a primary key, the optimizer treatsCAST(x AS ...)asx. It then drops the other ORDER BY or GROUP BY keys thatxdetermines, so the query returns wrong results:CAST(x AS INT)is namedt.x, because casts are left out of expression names. The functional dependency helpers matched GROUP BY and ORDER BY expressions to key columns by comparing names, so the cast matched the keyt.x.The same happens with
TRY_CAST, with aUNIQUE NOT NULLkey, and with a key that comes from an inner GROUP BY. The ORDER BY case comes from the sort key pruning added in #21362 (54.0.0).What changes are included in this PR?
functional_dependencies.rsnow take, for each GROUP BY or ORDER BY expression, the index of the input field it references (Option<usize>) instead of its name. A computed expression has no index, so it never matches a key. The newdatafusion_expr::utils::passthrough_field_indexreturns the index for a column reference, aliased or not.Aggregate's output use the GROUP BY list that its schema is built from (grouping_set_to_exprlist). Before, they used a list de-duplicated by name, which mergedCAST(x AS INT)andx.DFSchema::index_of_column_by_nameinstead of comparing"qualifier.name"strings. The unit testprojection_duplicate_flattened_name_uses_first_input_indexasserted the old string behaviour: a column named"orders.id"got the dependency of the different fieldorders.id. It is renamed and now asserts that each column keeps its own dependency.What is the testing strategy for this PR?
New tests. Section 6 of
functional_dependencies.slthas one query for each user of the helpers:Aggregateoutput dependencies;On
main, each of these returns a wrong result or accepts an invalid query.Existing tests are unchanged, apart from the rewritten unit test.
Planning time. Planning is not slower. The
sql_plannerTPC-H and TPC-DS benchmarks, whose tables have primary keys, are about 5% faster, because the new code no longer renders expression names.Benchmark numbers (3 interleaved runs per side)
mainphysical_plan_tpch_allphysical_plan_tpcds_allAre there any user-facing changes?
Results. The queries above return correct results.
API change. Four
pubfunctions indatafusion_commontake&[Option<usize>]instead of&[String]:aggregate_functional_dependenciesget_target_functional_dependenciesget_required_group_by_exprs_indicesget_required_sort_exprs_indicesThe upgrade guide has a migration note.