Skip to content

Commit 28a7f91

Browse files
goldmedalsgrebnov
authored andcommitted
Unparse SubqueryAlias without projections to SQL (apache#12896)
* change pub function comment to doc * unparse subquery alias without projections * fix tests * rollback the empty line * rollback the empty line * exclude the table_scan with pushdown case * fmt and clippy * simplify the ast to string and remove unused debug code
1 parent 11dde09 commit 28a7f91

3 files changed

Lines changed: 184 additions & 84 deletions

File tree

datafusion/sql/src/unparser/plan.rs

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ use super::{
4646
},
4747
Unparser,
4848
};
49+
use crate::unparser::utils::unproject_agg_exprs;
50+
use datafusion_common::{
51+
internal_err, not_impl_err,
52+
tree_node::{TransformedResult, TreeNode},
53+
Column, DataFusionError, Result, TableReference,
54+
};
55+
use datafusion_expr::{
56+
expr::Alias, Distinct, Expr, JoinConstraint, JoinType, LogicalPlan,
57+
LogicalPlanBuilder, Projection, SortExpr, TableScan,
58+
};
59+
use sqlparser::ast::{self, Ident, SetExpr};
60+
use std::sync::Arc;
4961

5062
/// Convert a DataFusion [`LogicalPlan`] to [`ast::Statement`]
5163
///
@@ -279,12 +291,9 @@ impl Unparser<'_> {
279291
) -> Result<()> {
280292
match plan {
281293
LogicalPlan::TableScan(scan) => {
282-
if scan.projection.is_some()
283-
|| !scan.filters.is_empty()
284-
|| scan.fetch.is_some()
294+
if let Some(unparsed_table_scan) =
295+
Self::unparse_table_scan_pushdown(plan, None)?
285296
{
286-
let unparsed_table_scan =
287-
Self::unparse_table_scan_pushdown(plan, None)?;
288297
return self.select_to_sql_recursively(
289298
&unparsed_table_scan,
290299
query,
@@ -556,10 +565,18 @@ impl Unparser<'_> {
556565
LogicalPlan::SubqueryAlias(plan_alias) => {
557566
let (plan, mut columns) =
558567
subquery_alias_inner_query_and_columns(plan_alias);
559-
let plan = Self::unparse_table_scan_pushdown(
568+
let unparsed_table_scan = Self::unparse_table_scan_pushdown(
560569
plan,
561570
Some(plan_alias.alias.clone()),
562571
)?;
572+
// if the child plan is a TableScan with pushdown operations, we don't need to
573+
// create an additional subquery for it
574+
if !select.already_projected() && unparsed_table_scan.is_none() {
575+
select.projection(vec![ast::SelectItem::Wildcard(
576+
ast::WildcardAdditionalOptions::default(),
577+
)]);
578+
}
579+
let plan = unparsed_table_scan.unwrap_or_else(|| plan.clone());
563580
if !columns.is_empty()
564581
&& !self.dialect.supports_column_alias_in_table_alias()
565582
{
@@ -668,12 +685,21 @@ impl Unparser<'_> {
668685
}
669686
}
670687

688+
fn is_scan_with_pushdown(scan: &TableScan) -> bool {
689+
scan.projection.is_some() || !scan.filters.is_empty() || scan.fetch.is_some()
690+
}
691+
692+
/// Try to unparse a table scan with pushdown operations into a new subquery plan.
693+
/// If the table scan is without any pushdown operations, return None.
671694
fn unparse_table_scan_pushdown(
672695
plan: &LogicalPlan,
673696
alias: Option<TableReference>,
674-
) -> Result<LogicalPlan> {
697+
) -> Result<Option<LogicalPlan>> {
675698
match plan {
676699
LogicalPlan::TableScan(table_scan) => {
700+
if !Self::is_scan_with_pushdown(table_scan) {
701+
return Ok(None);
702+
}
677703
let mut filter_alias_rewriter =
678704
alias.as_ref().map(|alias_name| TableAliasRewriter {
679705
table_schema: table_scan.source.schema(),
@@ -734,18 +760,15 @@ impl Unparser<'_> {
734760
builder = builder.limit(0, Some(fetch))?;
735761
}
736762

737-
builder.build()
763+
Ok(Some(builder.build()?))
738764
}
739765
LogicalPlan::SubqueryAlias(subquery_alias) => {
740-
let new_plan = Self::unparse_table_scan_pushdown(
766+
Self::unparse_table_scan_pushdown(
741767
&subquery_alias.input,
742768
Some(subquery_alias.alias.clone()),
743-
)?;
744-
LogicalPlanBuilder::from(new_plan)
745-
.alias(subquery_alias.alias.clone())?
746-
.build()
769+
)
747770
}
748-
_ => Ok(plan.clone()),
771+
_ => Ok(None),
749772
}
750773
}
751774

datafusion/sql/src/unparser/rewrite.rs

Lines changed: 47 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -101,25 +101,25 @@ fn rewrite_sort_expr_for_union(exprs: Vec<SortExpr>) -> Result<Vec<SortExpr>> {
101101
Ok(sort_exprs)
102102
}
103103

104-
// Rewrite logic plan for query that order by columns are not in projections
105-
// Plan before rewrite:
106-
//
107-
// Projection: j1.j1_string, j2.j2_string
108-
// Sort: j1.j1_id DESC NULLS FIRST, j2.j2_id DESC NULLS FIRST
109-
// Projection: j1.j1_string, j2.j2_string, j1.j1_id, j2.j2_id
110-
// Inner Join: Filter: j1.j1_id = j2.j2_id
111-
// TableScan: j1
112-
// TableScan: j2
113-
//
114-
// Plan after rewrite
115-
//
116-
// Sort: j1.j1_id DESC NULLS FIRST, j2.j2_id DESC NULLS FIRST
117-
// Projection: j1.j1_string, j2.j2_string
118-
// Inner Join: Filter: j1.j1_id = j2.j2_id
119-
// TableScan: j1
120-
// TableScan: j2
121-
//
122-
// This prevents the original plan generate query with derived table but missing alias.
104+
/// Rewrite logic plan for query that order by columns are not in projections
105+
/// Plan before rewrite:
106+
///
107+
/// Projection: j1.j1_string, j2.j2_string
108+
/// Sort: j1.j1_id DESC NULLS FIRST, j2.j2_id DESC NULLS FIRST
109+
/// Projection: j1.j1_string, j2.j2_string, j1.j1_id, j2.j2_id
110+
/// Inner Join: Filter: j1.j1_id = j2.j2_id
111+
/// TableScan: j1
112+
/// TableScan: j2
113+
///
114+
/// Plan after rewrite
115+
///
116+
/// Sort: j1.j1_id DESC NULLS FIRST, j2.j2_id DESC NULLS FIRST
117+
/// Projection: j1.j1_string, j2.j2_string
118+
/// Inner Join: Filter: j1.j1_id = j2.j2_id
119+
/// TableScan: j1
120+
/// TableScan: j2
121+
///
122+
/// This prevents the original plan generate query with derived table but missing alias.
123123
pub(super) fn rewrite_plan_for_sort_on_non_projected_fields(
124124
p: &Projection,
125125
) -> Option<LogicalPlan> {
@@ -191,33 +191,33 @@ pub(super) fn rewrite_plan_for_sort_on_non_projected_fields(
191191
}
192192
}
193193

194-
// This logic is to work out the columns and inner query for SubqueryAlias plan for both types of
195-
// subquery
196-
// - `(SELECT column_a as a from table) AS A`
197-
// - `(SELECT column_a from table) AS A (a)`
198-
//
199-
// A roundtrip example for table alias with columns
200-
//
201-
// query: SELECT id FROM (SELECT j1_id from j1) AS c (id)
202-
//
203-
// LogicPlan:
204-
// Projection: c.id
205-
// SubqueryAlias: c
206-
// Projection: j1.j1_id AS id
207-
// Projection: j1.j1_id
208-
// TableScan: j1
209-
//
210-
// Before introducing this logic, the unparsed query would be `SELECT c.id FROM (SELECT j1.j1_id AS
211-
// id FROM (SELECT j1.j1_id FROM j1)) AS c`.
212-
// The query is invalid as `j1.j1_id` is not a valid identifier in the derived table
213-
// `(SELECT j1.j1_id FROM j1)`
214-
//
215-
// With this logic, the unparsed query will be:
216-
// `SELECT c.id FROM (SELECT j1.j1_id FROM j1) AS c (id)`
217-
//
218-
// Caveat: this won't handle the case like `select * from (select 1, 2) AS a (b, c)`
219-
// as the parser gives a wrong plan which has mismatch `Int(1)` types: Literal and
220-
// Column in the Projections. Once the parser side is fixed, this logic should work
194+
/// This logic is to work out the columns and inner query for SubqueryAlias plan for both types of
195+
/// subquery
196+
/// - `(SELECT column_a as a from table) AS A`
197+
/// - `(SELECT column_a from table) AS A (a)`
198+
///
199+
/// A roundtrip example for table alias with columns
200+
///
201+
/// query: SELECT id FROM (SELECT j1_id from j1) AS c (id)
202+
///
203+
/// LogicPlan:
204+
/// Projection: c.id
205+
/// SubqueryAlias: c
206+
/// Projection: j1.j1_id AS id
207+
/// Projection: j1.j1_id
208+
/// TableScan: j1
209+
///
210+
/// Before introducing this logic, the unparsed query would be `SELECT c.id FROM (SELECT j1.j1_id AS
211+
/// id FROM (SELECT j1.j1_id FROM j1)) AS c`.
212+
/// The query is invalid as `j1.j1_id` is not a valid identifier in the derived table
213+
/// `(SELECT j1.j1_id FROM j1)`
214+
///
215+
/// With this logic, the unparsed query will be:
216+
/// `SELECT c.id FROM (SELECT j1.j1_id FROM j1) AS c (id)`
217+
///
218+
/// Caveat: this won't handle the case like `select * from (select 1, 2) AS a (b, c)`
219+
/// as the parser gives a wrong plan which has mismatch `Int(1)` types: Literal and
220+
/// Column in the Projections. Once the parser side is fixed, this logic should work
221221
pub(super) fn subquery_alias_inner_query_and_columns(
222222
subquery_alias: &datafusion_expr::SubqueryAlias,
223223
) -> (&LogicalPlan, Vec<Ident>) {
@@ -330,6 +330,7 @@ fn find_projection(logical_plan: &LogicalPlan) -> Option<&Projection> {
330330
_ => None,
331331
}
332332
}
333+
333334
/// A `TreeNodeRewriter` implementation that rewrites `Expr::Column` expressions by
334335
/// replacing the column's name with an alias if the column exists in the provided schema.
335336
///

0 commit comments

Comments
 (0)