Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 39 additions & 6 deletions datafusion/sql/src/unparser/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@ pub struct DerivedRelationBuilder {
lateral: Option<bool>,
subquery: Option<Box<ast::Query>>,
alias: Option<ast::TableAlias>,
projection_names: Vec<ast::Ident>,
}

impl DerivedRelationBuilder {
Expand All @@ -692,18 +693,49 @@ impl DerivedRelationBuilder {
self.alias = value;
self
}
pub(super) fn projection_names(&mut self, value: Vec<ast::Ident>) -> &mut Self {
self.projection_names = value;
self
}
fn build(&self) -> Result<ast::TableFactor, BuilderError> {
let mut subquery = match self.subquery {
Some(ref value) => value.clone(),
None => {
return Err(Into::into(UninitializedFieldError::from("subquery")));
}
};
if self
.alias
.as_ref()
.is_none_or(|alias| alias.columns.is_empty())
&& let ast::SetExpr::Select(select) = subquery.body.as_mut()
&& select.projection.len() == self.projection_names.len()
{
for (item, alias) in select.projection.iter_mut().zip(&self.projection_names)
{
if let ast::SelectItem::UnnamedExpr(expr) = item {
let preserves_name = match expr {
ast::Expr::Identifier(name) => name.value == alias.value,
ast::Expr::CompoundIdentifier(names) => {
names.last().is_some_and(|name| name.value == alias.value)
}
_ => false,
};
if !preserves_name {
*item = ast::SelectItem::ExprWithAlias {
expr: expr.clone(),
alias: alias.clone(),
};
}
}
}
}
Ok(ast::TableFactor::Derived {
lateral: match self.lateral {
Some(ref value) => *value,
None => return Err(Into::into(UninitializedFieldError::from("lateral"))),
},
subquery: match self.subquery {
Some(ref value) => value.clone(),
None => {
return Err(Into::into(UninitializedFieldError::from("subquery")));
}
},
subquery,
alias: self.alias.clone(),
sample: None,
})
Expand All @@ -713,6 +745,7 @@ impl DerivedRelationBuilder {
lateral: Default::default(),
subquery: Default::default(),
alias: Default::default(),
projection_names: Default::default(),
}
}
}
Expand Down
30 changes: 20 additions & 10 deletions datafusion/sql/src/unparser/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,18 @@ impl Unparser<'_> {
alias: Option<ast::TableAlias>,
lateral: bool,
) -> Result<()> {
let preserve_names = matches!(plan, LogicalPlan::Projection(_))

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.

Output-name preservation should not depend on the derived table having a table alias. BigQueryDialect does not require one, so derive_with_dialect_alias passes None and this condition remains false. The inner computed expression is then anonymous while the outer projection references its encoded logical name, producing invalid GoogleSQL. Could we preserve names whenever no table-column alias list already provides them, and add a BigQuery or alias-less regression test?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 91ca248.

&& alias.as_ref().is_none_or(|alias| alias.columns.is_empty());
let mut derived_builder = DerivedRelationBuilder::default();
if preserve_names {
derived_builder.projection_names(
plan.schema()
.fields()
.iter()
.map(|field| self.column_alias_to_sql(field.name()))
.collect::<Result<Vec<_>>>()?,
);
}
derived_builder.lateral(lateral).alias(alias).subquery({
let inner_statement = self.plan_to_sql(plan)?;
if let ast::Statement::Query(inner_query) = inner_statement {
Expand Down Expand Up @@ -2689,18 +2700,9 @@ impl Unparser<'_> {
Expr::Alias(Alias { expr, name, .. }) => {
let inner = self.expr_to_sql(expr)?;

// Determine the alias name to use
let col_name = if let Some(rewritten_name) =
self.dialect.col_alias_overrides(name)?
{
rewritten_name.to_string()
} else {
name.to_string()
};

Ok(ast::SelectItem::ExprWithAlias {
expr: inner,
alias: self.new_ident_quoted_if_needs(col_name),
alias: self.column_alias_to_sql(name)?,
})
}
_ => {
Expand All @@ -2711,6 +2713,14 @@ impl Unparser<'_> {
}
}

fn column_alias_to_sql(&self, name: &str) -> Result<Ident> {
let name = self
.dialect
.col_alias_overrides(name)?
.unwrap_or_else(|| name.to_string());
Ok(self.new_ident_quoted_if_needs(name))
}

fn sorts_to_sql(&self, sort_exprs: &[SortExpr]) -> Result<OrderByKind> {
Ok(OrderByKind::Expressions(
sort_exprs
Expand Down
57 changes: 56 additions & 1 deletion datafusion/sql/tests/cases/plan_to_sql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

use arrow::datatypes::{DataType, Field, Schema};

use datafusion_common::tree_node::{Transformed, TransformedResult};
use datafusion_common::{
Column, DFSchema, DFSchemaRef, DataFusionError, Result, TableReference,
assert_contains,
Expand Down Expand Up @@ -111,6 +112,25 @@ fn roundtrip_expr(table: TableReference, sql: &str) -> Result<String> {
Ok(ast.to_string())
}

fn remove_column_self_aliases(plan: LogicalPlan) -> Result<LogicalPlan> {
plan.transform_up_with_subqueries(|plan| {
plan.map_expressions(|expr| {
if let Expr::Alias(alias) = &expr
&& alias.relation.is_none()
&& alias.metadata.is_none()
&& let Expr::Column(column) = alias.expr.as_ref()
&& column.relation.is_none()
&& column.name == alias.name
{
Ok(Transformed::yes(*alias.expr.clone()))
} else {
Ok(Transformed::no(expr))
}
})
})
.data()
}

#[test]
fn roundtrip_statement() -> Result<()> {
let tests: Vec<&str> = vec![
Expand Down Expand Up @@ -254,7 +274,11 @@ fn roundtrip_statement() -> Result<()> {
.sql_statement_to_plan(roundtrip_statement.clone())
.unwrap();

assert_eq!(plan, plan_roundtrip);
// Explicit output names can add unqualified self-aliases without changing the plan's meaning.
assert_eq!(
remove_column_self_aliases(plan)?,
remove_column_self_aliases(plan_roundtrip)?,
);
}

Ok(())
Expand Down Expand Up @@ -392,6 +416,37 @@ fn roundtrip_statement_with_dialect_4() -> Result<(), DataFusionError> {
Ok(())
}

#[test]
fn unparse_preserves_derived_aggregate_output_name() -> Result<()> {
let schema = Schema::new(vec![Field::new("j1_id", DataType::Int32, false)]);
let aggregate = sum(col("j1.j1_id"));
let output = Expr::Column(Column::from_name(aggregate.schema_name().to_string()));
let plan = table_scan(Some("j1"), &schema, None)?
.aggregate(Vec::<Expr>::new(), vec![aggregate])?
.project(vec![output.clone().alias("visible"), output.clone()])?
.project(vec![output])?
.build()?;

let sql = Unparser::new(&UnparserPostgreSqlDialect {})
.plan_to_sql(&plan)?
.to_string();
println!("UNPARSED_SQL={sql}");
assert_snapshot!(
sql,
@r#"SELECT "sum(j1.j1_id)" FROM (SELECT sum("j1"."j1_id") AS "visible", sum("j1"."j1_id") AS "sum(j1.j1_id)" FROM "j1") AS "derived_projection""#
);

let sql = Unparser::new(&BigQueryDialect {})
.plan_to_sql(&plan)?
.to_string();
println!("BIGQUERY_SQL={sql}");
assert_snapshot!(
sql,
@r#"SELECT `sum_40j1_46j1_id_41` FROM (SELECT sum(`j1`.`j1_id`) AS `visible`, sum(`j1`.`j1_id`) AS `sum_40j1_46j1_id_41` FROM `j1`)"#
);
Ok(())
}

#[test]
fn roundtrip_rebases_derived_projection_references() -> Result<(), DataFusionError> {
roundtrip_statement_with_dialect_helper!(
Expand Down
Loading