From 3ceda40a9dcf46b34bc02c1f6c821c5a6a5ce615 Mon Sep 17 00:00:00 2001 From: nuno-faria Date: Sun, 20 Sep 2026 15:45:05 +0100 Subject: [PATCH] fix: Prevent SubqueryAlias to be pushed past FilterExec when unparsing --- datafusion/core/tests/sql/unparser.rs | 27 ++++++++++++++++++++++++++- datafusion/sql/src/unparser/plan.rs | 6 +++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/datafusion/core/tests/sql/unparser.rs b/datafusion/core/tests/sql/unparser.rs index 3982c60dbc7d9..b77f53cb46932 100644 --- a/datafusion/core/tests/sql/unparser.rs +++ b/datafusion/core/tests/sql/unparser.rs @@ -51,8 +51,8 @@ use datafusion_catalog::memory::MemorySchemaProvider; use datafusion_catalog::{CatalogProvider, MemoryCatalogProvider, SchemaProvider}; use datafusion_common::Column; use datafusion_expr::Expr; -use datafusion_sql::unparser::Unparser; use datafusion_sql::unparser::dialect::{DefaultDialect, DuckDBDialect}; +use datafusion_sql::unparser::{Unparser, plan_to_sql}; use itertools::Itertools; use recursive::{set_minimum_stack_size, set_stack_allocation_size}; @@ -747,6 +747,31 @@ async fn optimized_duckdb_unparse_top_level_sort_over_agg_uses_select_alias() -> Ok(()) } +#[tokio::test] +async fn optimized_filter_with_subquery_alias() -> Result<()> { + let ctx = SessionContext::new(); + ctx.sql("create table t (a int)").await?.collect().await?; + let df = ctx + .sql( + " + select * + from ( + select a + from t + ) t2 + where a = 1 + ", + ) + .await?; + let plan = df.into_optimized_plan()?; + let sql = plan_to_sql(&plan)?.to_string(); + assert_eq!( + sql, + "SELECT * FROM (SELECT t.a FROM t WHERE (t.a = 1)) AS t2" + ); + Ok(()) +} + /// The outcome of running a single roundtrip test. /// /// A successful test produces [`TestCaseResult::Success`]. diff --git a/datafusion/sql/src/unparser/plan.rs b/datafusion/sql/src/unparser/plan.rs index 18af08fc18361..69babae4e0ffe 100644 --- a/datafusion/sql/src/unparser/plan.rs +++ b/datafusion/sql/src/unparser/plan.rs @@ -1632,7 +1632,11 @@ impl Unparser<'_> { // we must emit a derived subquery: (SELECT ...) AS alias. // Without this, the recursive handler would merge those clauses // into the outer SELECT, losing the subquery structure entirely. - if unparsed_table_scan.is_none() && Self::requires_derived_subquery(plan) + // Also, do not add a table alias past a Filter, as otherwise the predicates might + // refer to invalid tables. + if (unparsed_table_scan.is_none() + && Self::requires_derived_subquery(plan)) + || matches!(plan, LogicalPlan::Filter(_)) { // When the dialect does not support column aliases in // table aliases (e.g. SQLite), inject the aliases into