This is an automated email from the ASF dual-hosted git repository.

alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion.git


The following commit(s) were added to refs/heads/main by this push:
     new ddbdf4bf7f Unparse TableScan with projections, filters or fetch to SQL 
string (#12158)
ddbdf4bf7f is described below

commit ddbdf4bf7f215c01aca435f8edd00412e84aab60
Author: Jax Liu <[email protected]>
AuthorDate: Sat Sep 7 18:18:22 2024 +0800

    Unparse TableScan with projections, filters or fetch to SQL string (#12158)
    
    * support to unparse table scan with projection
    
    * support to unparse table_scan with filter and fetch
    
    * add the doc for public function
    
    * clippy
    
    * remove pub get method for table scan
    
    * fix merge conflict
    
    * add issue reference
    
    * fix typo
---
 datafusion/expr/src/logical_plan/builder.rs |  36 +++++++
 datafusion/sql/src/unparser/plan.rs         |  99 ++++++++++++++++--
 datafusion/sql/tests/cases/plan_to_sql.rs   | 154 +++++++++++++++++++++++++++-
 3 files changed, 278 insertions(+), 11 deletions(-)

diff --git a/datafusion/expr/src/logical_plan/builder.rs 
b/datafusion/expr/src/logical_plan/builder.rs
index fc961b83f7..f4dad08542 100644
--- a/datafusion/expr/src/logical_plan/builder.rs
+++ b/datafusion/expr/src/logical_plan/builder.rs
@@ -333,6 +333,19 @@ impl LogicalPlanBuilder {
             .map(Self::new)
     }
 
+    /// Convert a table provider into a builder with a TableScan with filter 
and fetch
+    pub fn scan_with_filters_fetch(
+        table_name: impl Into<TableReference>,
+        table_source: Arc<dyn TableSource>,
+        projection: Option<Vec<usize>>,
+        filters: Vec<Expr>,
+        fetch: Option<usize>,
+    ) -> Result<Self> {
+        TableScan::try_new(table_name, table_source, projection, filters, 
fetch)
+            .map(LogicalPlan::TableScan)
+            .map(Self::new)
+    }
+
     /// Wrap a plan in a window
     pub fn window_plan(
         input: LogicalPlan,
@@ -1424,6 +1437,29 @@ pub fn table_scan_with_filters(
     LogicalPlanBuilder::scan_with_filters(name, table_source, projection, 
filters)
 }
 
+/// Create a LogicalPlanBuilder representing a scan of a table with the 
provided name and schema,
+/// filters, and inlined fetch.
+/// This is mostly used for testing and documentation.
+pub fn table_scan_with_filter_and_fetch(
+    name: Option<impl Into<TableReference>>,
+    table_schema: &Schema,
+    projection: Option<Vec<usize>>,
+    filters: Vec<Expr>,
+    fetch: Option<usize>,
+) -> Result<LogicalPlanBuilder> {
+    let table_source = table_source(table_schema);
+    let name = name
+        .map(|n| n.into())
+        .unwrap_or_else(|| TableReference::bare(UNNAMED_TABLE));
+    LogicalPlanBuilder::scan_with_filters_fetch(
+        name,
+        table_source,
+        projection,
+        filters,
+        fetch,
+    )
+}
+
 fn table_source(table_schema: &Schema) -> Arc<dyn TableSource> {
     let table_schema = Arc::new(table_schema.clone());
     Arc::new(LogicalTableSource { table_schema })
diff --git a/datafusion/sql/src/unparser/plan.rs 
b/datafusion/sql/src/unparser/plan.rs
index 478cfd0ecd..802d476257 100644
--- a/datafusion/sql/src/unparser/plan.rs
+++ b/datafusion/sql/src/unparser/plan.rs
@@ -15,16 +15,16 @@
 // specific language governing permissions and limitations
 // under the License.
 
+use crate::unparser::utils::unproject_agg_exprs;
 use datafusion_common::{
-    internal_err, not_impl_err, plan_err, Column, DataFusionError, Result,
+    internal_err, not_impl_err, plan_err, Column, DataFusionError, Result, 
TableReference,
 };
 use datafusion_expr::{
-    expr::Alias, Distinct, Expr, JoinConstraint, JoinType, LogicalPlan, 
Projection,
-    SortExpr,
+    expr::Alias, Distinct, Expr, JoinConstraint, JoinType, LogicalPlan,
+    LogicalPlanBuilder, Projection, SortExpr,
 };
 use sqlparser::ast::{self, Ident, SetExpr};
-
-use crate::unparser::utils::unproject_agg_exprs;
+use std::sync::Arc;
 
 use super::{
     ast::{
@@ -240,6 +240,19 @@ impl Unparser<'_> {
     ) -> Result<()> {
         match plan {
             LogicalPlan::TableScan(scan) => {
+                if scan.projection.is_some()
+                    || !scan.filters.is_empty()
+                    || scan.fetch.is_some()
+                {
+                    let unparsed_table_scan =
+                        Self::unparse_table_scan_pushdown(plan, None)?;
+                    return self.select_to_sql_recursively(
+                        &unparsed_table_scan,
+                        query,
+                        select,
+                        relation,
+                    );
+                }
                 let mut builder = TableRelationBuilder::default();
                 let mut table_parts = vec![];
                 if let Some(catalog_name) = scan.table_name.catalog() {
@@ -455,7 +468,10 @@ impl Unparser<'_> {
             LogicalPlan::SubqueryAlias(plan_alias) => {
                 let (plan, mut columns) =
                     subquery_alias_inner_query_and_columns(plan_alias);
-
+                let plan = Self::unparse_table_scan_pushdown(
+                    plan,
+                    Some(plan_alias.alias.clone()),
+                )?;
                 if !columns.is_empty()
                     && !self.dialect.supports_column_alias_in_table_alias()
                 {
@@ -467,7 +483,7 @@ impl Unparser<'_> {
                     };
 
                     // Instead of specifying column aliases as part of the 
outer table, inject them directly into the inner projection
-                    let rewritten_plan = inject_column_aliases(inner_p, 
columns);
+                    let rewritten_plan = inject_column_aliases(&inner_p, 
columns);
                     columns = vec![];
 
                     self.select_to_sql_recursively(
@@ -477,7 +493,7 @@ impl Unparser<'_> {
                         relation,
                     )?;
                 } else {
-                    self.select_to_sql_recursively(plan, query, select, 
relation)?;
+                    self.select_to_sql_recursively(&plan, query, select, 
relation)?;
                 }
 
                 relation.alias(Some(
@@ -532,6 +548,73 @@ impl Unparser<'_> {
         }
     }
 
+    fn unparse_table_scan_pushdown(
+        plan: &LogicalPlan,
+        alias: Option<TableReference>,
+    ) -> Result<LogicalPlan> {
+        match plan {
+            LogicalPlan::TableScan(table_scan) => {
+                // TODO: support filters for table scan with alias. Remove 
this check after #12368 issue.
+                // see the issue: 
https://github.com/apache/datafusion/issues/12368
+                if alias.is_some() && !table_scan.filters.is_empty() {
+                    return not_impl_err!(
+                        "Subquery alias is not supported for table scan with 
pushdown filters"
+                    );
+                }
+
+                let mut builder = LogicalPlanBuilder::scan(
+                    table_scan.table_name.clone(),
+                    Arc::clone(&table_scan.source),
+                    None,
+                )?;
+                if let Some(project_vec) = &table_scan.projection {
+                    let project_columns = project_vec
+                        .iter()
+                        .cloned()
+                        .map(|i| {
+                            let (qualifier, field) =
+                                table_scan.projected_schema.qualified_field(i);
+                            if alias.is_some() {
+                                Column::new(alias.clone(), 
field.name().clone())
+                            } else {
+                                Column::new(qualifier.cloned(), 
field.name().clone())
+                            }
+                        })
+                        .collect::<Vec<_>>();
+                    if let Some(alias) = alias {
+                        builder = builder.alias(alias)?;
+                    }
+                    builder = builder.project(project_columns)?;
+                }
+
+                let filter_expr = table_scan
+                    .filters
+                    .iter()
+                    .cloned()
+                    .reduce(|acc, expr| acc.and(expr));
+                if let Some(filter) = filter_expr {
+                    builder = builder.filter(filter)?;
+                }
+
+                if let Some(fetch) = table_scan.fetch {
+                    builder = builder.limit(0, Some(fetch))?;
+                }
+
+                builder.build()
+            }
+            LogicalPlan::SubqueryAlias(subquery_alias) => {
+                let new_plan = Self::unparse_table_scan_pushdown(
+                    &subquery_alias.input,
+                    Some(subquery_alias.alias.clone()),
+                )?;
+                LogicalPlanBuilder::from(new_plan)
+                    .alias(subquery_alias.alias.clone())?
+                    .build()
+            }
+            _ => Ok(plan.clone()),
+        }
+    }
+
     fn select_item_to_sql(&self, expr: &Expr) -> Result<ast::SelectItem> {
         match expr {
             Expr::Alias(Alias { expr, name, .. }) => {
diff --git a/datafusion/sql/tests/cases/plan_to_sql.rs 
b/datafusion/sql/tests/cases/plan_to_sql.rs
index 4a6923593f..fa95d05c32 100644
--- a/datafusion/sql/tests/cases/plan_to_sql.rs
+++ b/datafusion/sql/tests/cases/plan_to_sql.rs
@@ -21,7 +21,7 @@ use std::vec;
 use arrow_schema::*;
 use datafusion_common::{DFSchema, Result, TableReference};
 use datafusion_expr::test::function_stub::{count_udaf, max_udaf, min_udaf, 
sum_udaf};
-use datafusion_expr::{col, table_scan};
+use datafusion_expr::{col, lit, table_scan, wildcard, LogicalPlanBuilder};
 use datafusion_sql::planner::{ContextProvider, PlannerContext, SqlToRel};
 use datafusion_sql::unparser::dialect::{
     DefaultDialect as UnparserDefaultDialect, Dialect as UnparserDialect,
@@ -29,12 +29,14 @@ use datafusion_sql::unparser::dialect::{
 };
 use datafusion_sql::unparser::{expr_to_sql, plan_to_sql, Unparser};
 
+use crate::common::{MockContextProvider, MockSessionState};
+use datafusion_expr::builder::{
+    table_scan_with_filter_and_fetch, table_scan_with_filters,
+};
 use datafusion_functions::core::planner::CoreFunctionPlanner;
 use sqlparser::dialect::{Dialect, GenericDialect, MySqlDialect};
 use sqlparser::parser::Parser;
 
-use crate::common::{MockContextProvider, MockSessionState};
-
 #[test]
 fn roundtrip_expr() {
     let tests: Vec<(TableReference, &str, &str)> = vec![
@@ -619,6 +621,152 @@ fn sql_round_trip(query: &str, expect: &str) {
     assert_eq!(roundtrip_statement.to_string(), expect);
 }
 
+#[test]
+fn test_table_scan_pushdown() -> Result<()> {
+    let schema = Schema::new(vec![
+        Field::new("id", DataType::Utf8, false),
+        Field::new("age", DataType::Utf8, false),
+    ]);
+
+    let scan_with_projection =
+        table_scan(Some("t1"), &schema, Some(vec![0, 1]))?.build()?;
+    let scan_with_projection = plan_to_sql(&scan_with_projection)?;
+    assert_eq!(
+        format!("{}", scan_with_projection),
+        "SELECT t1.id, t1.age FROM t1"
+    );
+
+    let scan_with_no_projection = table_scan(Some("t1"), &schema, 
None)?.build()?;
+    let scan_with_no_projection = plan_to_sql(&scan_with_no_projection)?;
+    assert_eq!(format!("{}", scan_with_no_projection), "SELECT * FROM t1");
+
+    let table_scan_with_projection_alias =
+        table_scan(Some("t1"), &schema, Some(vec![0, 1]))?
+            .alias("ta")?
+            .build()?;
+    let table_scan_with_projection_alias =
+        plan_to_sql(&table_scan_with_projection_alias)?;
+    assert_eq!(
+        format!("{}", table_scan_with_projection_alias),
+        "SELECT ta.id, ta.age FROM t1 AS ta"
+    );
+
+    let table_scan_with_no_projection_alias = table_scan(Some("t1"), &schema, 
None)?
+        .alias("ta")?
+        .build()?;
+    let table_scan_with_no_projection_alias =
+        plan_to_sql(&table_scan_with_no_projection_alias)?;
+    assert_eq!(
+        format!("{}", table_scan_with_no_projection_alias),
+        "SELECT * FROM t1 AS ta"
+    );
+
+    let query_from_table_scan_with_projection = LogicalPlanBuilder::from(
+        table_scan(Some("t1"), &schema, Some(vec![0, 1]))?.build()?,
+    )
+    .project(vec![wildcard()])?
+    .build()?;
+    let query_from_table_scan_with_projection =
+        plan_to_sql(&query_from_table_scan_with_projection)?;
+    assert_eq!(
+        format!("{}", query_from_table_scan_with_projection),
+        "SELECT * FROM (SELECT t1.id, t1.age FROM t1)"
+    );
+
+    let table_scan_with_filter = table_scan_with_filters(
+        Some("t1"),
+        &schema,
+        None,
+        vec![col("id").gt(col("age"))],
+    )?
+    .build()?;
+    let table_scan_with_filter = plan_to_sql(&table_scan_with_filter)?;
+    assert_eq!(
+        format!("{}", table_scan_with_filter),
+        "SELECT * FROM t1 WHERE (t1.id > t1.age)"
+    );
+
+    let table_scan_with_two_filter = table_scan_with_filters(
+        Some("t1"),
+        &schema,
+        None,
+        vec![col("id").gt(lit(1)), col("age").lt(lit(2))],
+    )?
+    .build()?;
+    let table_scan_with_two_filter = plan_to_sql(&table_scan_with_two_filter)?;
+    assert_eq!(
+        format!("{}", table_scan_with_two_filter),
+        "SELECT * FROM t1 WHERE ((t1.id > 1) AND (t1.age < 2))"
+    );
+
+    // TODO: support filters for table scan with alias. Enable this test after 
#12368 issue is fixed
+    // see the issue: https://github.com/apache/datafusion/issues/12368
+    // let table_scan_with_filter_alias = table_scan_with_filters(
+    //     Some("t1"),
+    //     &schema,
+    //     None,
+    //     vec![col("id").gt(col("age"))],
+    // )?.alias("ta")?.build()?;
+    // let table_scan_with_filter_alias = 
plan_to_sql(&table_scan_with_filter_alias)?;
+    // assert_eq!(
+    //     format!("{}", table_scan_with_filter_alias),
+    //     "SELECT * FROM t1 AS ta WHERE (ta.id > ta.age)"
+    // );
+
+    let table_scan_with_projection_and_filter = table_scan_with_filters(
+        Some("t1"),
+        &schema,
+        Some(vec![0, 1]),
+        vec![col("id").gt(col("age"))],
+    )?
+    .build()?;
+    let table_scan_with_projection_and_filter =
+        plan_to_sql(&table_scan_with_projection_and_filter)?;
+    assert_eq!(
+        format!("{}", table_scan_with_projection_and_filter),
+        "SELECT t1.id, t1.age FROM t1 WHERE (t1.id > t1.age)"
+    );
+
+    let table_scan_with_inline_fetch =
+        table_scan_with_filter_and_fetch(Some("t1"), &schema, None, vec![], 
Some(10))?
+            .build()?;
+    let table_scan_with_inline_fetch = 
plan_to_sql(&table_scan_with_inline_fetch)?;
+    assert_eq!(
+        format!("{}", table_scan_with_inline_fetch),
+        "SELECT * FROM t1 LIMIT 10"
+    );
+
+    let table_scan_with_projection_and_inline_fetch = 
table_scan_with_filter_and_fetch(
+        Some("t1"),
+        &schema,
+        Some(vec![0, 1]),
+        vec![],
+        Some(10),
+    )?
+    .build()?;
+    let table_scan_with_projection_and_inline_fetch =
+        plan_to_sql(&table_scan_with_projection_and_inline_fetch)?;
+    assert_eq!(
+        format!("{}", table_scan_with_projection_and_inline_fetch),
+        "SELECT t1.id, t1.age FROM t1 LIMIT 10"
+    );
+
+    let table_scan_with_all = table_scan_with_filter_and_fetch(
+        Some("t1"),
+        &schema,
+        Some(vec![0, 1]),
+        vec![col("id").gt(col("age"))],
+        Some(10),
+    )?
+    .build()?;
+    let table_scan_with_all = plan_to_sql(&table_scan_with_all)?;
+    assert_eq!(
+        format!("{}", table_scan_with_all),
+        "SELECT t1.id, t1.age FROM t1 WHERE (t1.id > t1.age) LIMIT 10"
+    );
+    Ok(())
+}
+
 #[test]
 fn test_interval_lhs_eq() {
     sql_round_trip(


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to