Repository navigation
fix(optimizer): error on correlated subqueries with ambiguous duplicate columns across relations - #27244
fix(optimizer): error on correlated subqueries with ambiguous duplicate columns across relations#27244Awesome06 wants to merge 5 commits into
Conversation
…te columns across relations Fail fast with an explicit error when a correlated subquery references the outer query from multiple relations that share identical column names, preventing silent wrong results caused by column collision during decorrelation. - Motivation & Intent: When a correlated subquery joins multiple inner relations that expose columns with identical unqualified names (e.g., `one.k` and `two.k`), `SubqueryAlias` disambiguates them in the schema as `k` and `k:1`. However, decorrelation rewrote join filters by bare column name to `<alias>.<col>`, collapsing both references into `__correlated_sq_N.k`. This caused conditions intended for one relation to be evaluated against the other, silently producing incorrect results. Per maintainer guidelines (apache#5492, apache#26672), rather than introducing fragile heuristic alias rewrites, DataFusion must fail fast with a plan error. - Key Changes: - `datafusion/optimizer/src/decorrelate.rs`: Added `subquery_schema: &DFSchema` parameter to `build_join_filter`. Checks whether any correlated column has duplicate fields in `subquery_schema.fields()`. Returns an explicit `plan_err!` if duplicates exist. Added duplicate field error test in `test_build_join_filter`. - `datafusion/optimizer/src/decorrelate_predicate_subquery.rs`: Passed `new_plan.schema()` into `build_join_filter`. - `datafusion/optimizer/src/scalar_subquery_to_join.rs`: Passed `decorrelated_subquery.schema()` into `build_join_filter`. - `datafusion/sqllogictest/test_files/subquery.slt`: Added regression test suite covering `EXISTS`, `NOT EXISTS`, `IN`, and scalar subqueries asserting `Unsupported correlated subquery` error, along with a positive control query ensuring single-relation correlation continues to succeed. - Verification Evidence: - `cargo test -p datafusion-sqllogictest --test sqllogictests -- subquery` (100% passed) - `cargo test -p datafusion-optimizer` (975 unit tests + 29 integration tests + 5 doctests passed) - `cargo fmt --all -- --check` (clean) - `cargo clippy -p datafusion-optimizer -p datafusion-sqllogictest --all-targets -- -D warnings` (0 warnings) Closes apache#26234
|
@alamb could you review this bug fix |
jayzhan211
left a comment
There was a problem hiding this comment.
@Awesome06 thanks for the PR. In decorrelate_lateral_join.rs around line 402, requalify_filter does the same name-based mapping, so LATERAL still returns wrong rows with this PR. Expected (1, 1), (2, 0), actual (1, 0), (2, 0):
SELECT t_outer.k, s.c FROM t26234 t_outer, LATERAL (
SELECT count(*) AS c FROM t26234 one, t26234 two
WHERE one.k = t_outer.k AND two.k = t_outer.k + 1
) s;Could you call the same check there, so every place that maps correlated columns by name uses it, and add this query as a statement error test?
if let Expr::Column(col) = &expr
&& inner_schema.has_column(col)
{
+ check_alias_keeps_column_name(inner_schema, col)?;
let new_col = Column::new(Some(alias.clone()), col.name.clone());|
Thanks for the catch and the helpful pointers, @jayzhan211! Pushed an update with your suggestions:
|
jayzhan211
left a comment
There was a problem hiding this comment.
@Awesome06 thanks for the update. A derived table inside the subquery still returns wrong rows with no error. This query returns no rows; the expected result is 1 (the NOT EXISTS form returns 1, 2; expected 2):
SELECT t_outer.k FROM t26234 t_outer WHERE EXISTS (
SELECT 1 FROM (
SELECT * FROM t26234 one, t26234 two WHERE one.k = t_outer.k AND two.k = t_outer.k + 1
) d
);In decorrelate.rs around line 711, the SubqueryAlias arm of PullUpCorrelatedExpr maps one.k and two.k to d.k by name, which is the third place that does this. Could you call the same check there?
for col in local_correlated_cols.iter() {
+ check_alias_keeps_column_name(alias.input.schema(), col)?;
new_correlated_cols
.insert(Column::new(Some(alias.alias.clone()), col.name.clone()));Tests to add before DROP TABLE t26234: the query above and its NOT EXISTS form as errors, and a one-relation derived table that keeps working
# Same-named columns of a derived table inside the subquery
statement error Unsupported correlated subquery: correlated column two.k has the same name as an earlier column of the subquery output
SELECT t_outer.k FROM t26234 t_outer WHERE EXISTS (
SELECT 1 FROM (
SELECT * FROM t26234 one, t26234 two WHERE one.k = t_outer.k AND two.k = t_outer.k + 1
) d
);
statement error Unsupported correlated subquery: correlated column two.k has the same name as an earlier column of the subquery output
SELECT t_outer.k FROM t26234 t_outer WHERE NOT EXISTS (
SELECT 1 FROM (
SELECT * FROM t26234 one, t26234 two WHERE one.k = t_outer.k AND two.k = t_outer.k + 1
) d
);
# A derived table with one relation still works
query I
SELECT t_outer.k FROM t26234 t_outer WHERE EXISTS (
SELECT 1 FROM (SELECT * FROM t26234 one WHERE one.k = t_outer.k + 1) d
) ORDER BY t_outer.k;
----
1
With this applied, the full sqllogictest suite and cargo test -p datafusion-optimizer --lib pass, and cargo clippy -p datafusion-optimizer --all-targets -- -D warnings is clean. Without the new line, the two error tests return rows.
Optional cleanup: the PR description still quotes the old error text (inner relation column '{col}' has ambiguous duplicate references across relations) and doesn't mention the LATERAL check. Could you update it to the current message (correlated column two.k has the same name as an earlier column of the subquery output) and add decorrelate_lateral_join.rs?
…rrelated subqueries Fail fast with an explicit error when a correlated subquery contains a derived table referencing multiple inner relations with identical column names, preventing silent wrong results caused by duplicate column collapse into the derived table alias. Closes apache#26234
|
Thanks for catching this, @jayzhan211! Updated in
|
… correlated subqueries Use unique_field_aliases in check_alias_keeps_column_name to mirror SubqueryAlias renaming logic and prevent collisions with synthetically aliased columns.
Which issue does this PR close?
Closes #26234
Rationale for this change
When a correlated subquery joins multiple inner relations that expose columns with identical unqualified names (e.g.,
one.kandtwo.k), DataFusion'sSubqueryAliasnaming contract keeps the first field with a given name (k) and disambiguates subsequent ones (k:1,k:2).Previously, subquery decorrelation (for
EXISTS,NOT EXISTS,IN, scalar subqueries,LATERALjoins, and inner derived tables) requalified inner column references by unqualified column name into<alias>.<col>. This collapsed references from distinct inner tables into the same aliased column (e.g.__correlated_sq_N.kord.k), producing contradictory join filters (k = outer.k AND k = outer.k + 1) that silently returned wrong results (empty rows) without an error.Per DataFusion's maintainer doctrine ("Fail-Fast / Error Over Wrong Results"), rather than introducing speculative alias heuristics, decorrelation must fail fast with an explicit planning error when a correlated column is not the primary 0-indexed column retained under the alias.
What changes are included in this PR?
datafusion/optimizer/src/decorrelate.rs:check_alias_keeps_column_name(schema: &DFSchema, col: &Column) -> Result<()>: Validates whethercolis the first field with its unqualified name inschema. If not, raises:Unsupported correlated subquery: correlated column {col} has the same name as an earlier column of the subquery outputcheck_alias_keeps_column_namecall insidebuild_join_filterto guard top-level subquery aliasing.check_alias_keeps_column_name(alias.input.schema(), col)?;to theLogicalPlan::SubqueryAliasarm ofPullUpCorrelatedExpr, guarding intermediate derived tables (FROM (SELECT ... FROM one, two) d) inside subqueries.test_build_join_filterunit tests to verify that unambiguous first-column correlations succeed while subsequent ambiguous correlations fail.datafusion/optimizer/src/decorrelate_lateral_join.rs:check_alias_keeps_column_namecheck insiderequalify_filter, guarding lateral join filter requalification against ambiguous duplicate column collision.datafusion/optimizer/src/decorrelate_predicate_subquery.rs&datafusion/optimizer/src/scalar_subquery_to_join.rs:new_plan.schema()anddecorrelated_subquery.schema()intobuild_join_filter.datafusion/sqllogictest/test_files/subquery.slt:EXISTS,NOT EXISTS,IN, scalar subqueries, andLATERALjoins referencing ambiguous second columns (two.k), asserting the exactUnsupported correlated subqueryerror.EXISTSandNOT EXISTS), asserting the exact error.Are these changes tested?
Yes:
cargo test -p datafusion-optimizer --lib(975 unit tests passed)cargo test -p datafusion-sqllogictest --test sqllogictests -- subquery(100% passed)cargo clippy -p datafusion-optimizer --all-targets -- -D warnings(0 warnings)cargo fmt --all -- --check(clean)Are there any user-facing changes?
Queries with correlated subqueries that reference ambiguous duplicate columns across multiple inner relations (or within derived tables inside subqueries) now fail fast with an explicit
DataFusionError::Planinstead of silently returning incorrect results. Unambiguous references (referencing only the first relation) continue to succeed.