diff --git a/datafusion/sql/src/unparser/plan.rs b/datafusion/sql/src/unparser/plan.rs index 2c9300f48344..5b608ad6f260 100644 --- a/datafusion/sql/src/unparser/plan.rs +++ b/datafusion/sql/src/unparser/plan.rs @@ -19,11 +19,13 @@ use crate::unparser::utils::{ find_unnest_node_within_select, unproject_agg_exprs, unproject_unnest_expr, }; use datafusion_common::{ - internal_err, not_impl_err, Column, DataFusionError, Result, TableReference, + internal_err, not_impl_err, + tree_node::{TransformedResult, TreeNode}, + Column, DataFusionError, Result, TableReference, }; use datafusion_expr::{ expr::Alias, Distinct, Expr, JoinConstraint, JoinType, LogicalPlan, - LogicalPlanBuilder, Projection, SortExpr, + LogicalPlanBuilder, Projection, SortExpr, TableScan, }; use sqlparser::ast::{self, Ident, SetExpr}; use std::sync::Arc; @@ -36,7 +38,7 @@ use super::{ rewrite::{ inject_column_aliases_into_subquery, normalize_union_schema, rewrite_plan_for_sort_on_non_projected_fields, - subquery_alias_inner_query_and_columns, + subquery_alias_inner_query_and_columns, TableAliasRewriter, }, utils::{ find_agg_node_within_select, find_window_nodes_within_select, @@ -277,12 +279,9 @@ impl Unparser<'_> { ) -> Result<()> { match plan { LogicalPlan::TableScan(scan) => { - if scan.projection.is_some() - || !scan.filters.is_empty() - || scan.fetch.is_some() + if let Some(unparsed_table_scan) = + Self::unparse_table_scan_pushdown(plan, None)? { - let unparsed_table_scan = - Self::unparse_table_scan_pushdown(plan, None)?; return self.select_to_sql_recursively( &unparsed_table_scan, query, @@ -366,6 +365,21 @@ impl Unparser<'_> { )))); } + if limit.skip > 0 { + let Some(query) = query.as_mut() else { + return internal_err!( + "Offset operator only valid in a statement context." + ); + }; + query.offset(Some(ast::Offset { + rows: ast::OffsetRows::None, + value: ast::Expr::Value(ast::Value::Number( + limit.skip.to_string(), + false, + )), + })); + } + self.select_to_sql_recursively( limit.input.as_ref(), query, @@ -388,6 +402,13 @@ impl Unparser<'_> { ); }; + if let Some(fetch) = sort.fetch { + query_ref.limit(Some(ast::Expr::Value(ast::Value::Number( + fetch.to_string(), + false, + )))); + } + let agg = find_agg_node_within_select(plan, select.already_projected()); // unproject sort expressions let sort_exprs: Vec = sort @@ -532,10 +553,18 @@ 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( + let unparsed_table_scan = Self::unparse_table_scan_pushdown( plan, Some(plan_alias.alias.clone()), )?; + // if the child plan is a TableScan with pushdown operations, we don't need to + // create an additional subquery for it + if !select.already_projected() && unparsed_table_scan.is_none() { + select.projection(vec![ast::SelectItem::Wildcard( + ast::WildcardAdditionalOptions::default(), + )]); + } + let plan = unparsed_table_scan.unwrap_or_else(|| plan.clone()); if !columns.is_empty() && !self.dialect.supports_column_alias_in_table_alias() { @@ -644,51 +673,83 @@ impl Unparser<'_> { } } + fn is_scan_with_pushdown(scan: &TableScan) -> bool { + scan.projection.is_some() || !scan.filters.is_empty() || scan.fetch.is_some() + } + + /// Try to unparse a table scan with pushdown operations into a new subquery plan. + /// If the table scan is without any pushdown operations, return None. fn unparse_table_scan_pushdown( plan: &LogicalPlan, alias: Option, - ) -> Result { + ) -> Result> { 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" - ); + if !Self::is_scan_with_pushdown(table_scan) { + return Ok(None); } + let table_schema = table_scan.source.schema(); + let mut filter_alias_rewriter = + alias.as_ref().map(|alias_name| TableAliasRewriter { + table_schema: &table_schema, + alias_name: alias_name.clone(), + }); let mut builder = LogicalPlanBuilder::scan( table_scan.table_name.clone(), Arc::clone(&table_scan.source), None, )?; + // We will rebase the column references to the new alias if it exists. + // If the projection or filters are empty, we will append alias to the table scan. + // + // Example: + // select t1.c1 from t1 where t1.c1 > 1 -> select a.c1 from t1 as a where a.c1 > 1 + if alias.is_some() + && (table_scan.projection.is_some() || !table_scan.filters.is_empty()) + { + builder = builder.alias(alias.clone().unwrap())?; + } + 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); + let schema = table_scan.source.schema(); + let field = schema.field(i); if alias.is_some() { Column::new(alias.clone(), field.name().clone()) } else { - Column::new(qualifier.cloned(), field.name().clone()) + Column::new( + Some(table_scan.table_name.clone()), + field.name().clone(), + ) } }) .collect::>(); - if let Some(alias) = alias { - builder = builder.alias(alias)?; - } builder = builder.project(project_columns)?; } - let filter_expr = table_scan + let filter_expr: Result> = table_scan .filters .iter() .cloned() - .reduce(|acc, expr| acc.and(expr)); - if let Some(filter) = filter_expr { + .map(|expr| { + if let Some(ref mut rewriter) = filter_alias_rewriter { + expr.rewrite(rewriter).data() + } else { + Ok(expr) + } + }) + .reduce(|acc, expr_result| { + acc.and_then(|acc_expr| { + expr_result.map(|expr| acc_expr.and(expr)) + }) + }) + .transpose(); + + if let Some(filter) = filter_expr? { builder = builder.filter(filter)?; } @@ -696,18 +757,59 @@ impl Unparser<'_> { builder = builder.limit(0, Some(fetch))?; } - builder.build() + // If the table scan has an alias but no projection or filters, it means no column references are rebased. + // So we will append the alias to this subquery. + // Example: + // select * from t1 limit 10 -> (select * from t1 limit 10) as a + if alias.is_some() + && (table_scan.projection.is_none() && table_scan.filters.is_empty()) + { + builder = builder.alias(alias.clone().unwrap())?; + } + + Ok(Some(builder.build()?)) } LogicalPlan::SubqueryAlias(subquery_alias) => { - let new_plan = Self::unparse_table_scan_pushdown( + Self::unparse_table_scan_pushdown( &subquery_alias.input, Some(subquery_alias.alias.clone()), - )?; - LogicalPlanBuilder::from(new_plan) - .alias(subquery_alias.alias.clone())? - .build() + ) + } + // SubqueryAlias could be rewritten to a plan with a projection as the top node by [rewrite::subquery_alias_inner_query_and_columns]. + // The inner table scan could be a scan with pushdown operations. + LogicalPlan::Projection(projection) => { + if let Some(plan) = + Self::unparse_table_scan_pushdown(&projection.input, alias.clone())? + { + let exprs = if alias.is_some() { + let mut alias_rewriter = + alias.as_ref().map(|alias_name| TableAliasRewriter { + table_schema: plan.schema().as_arrow(), + alias_name: alias_name.clone(), + }); + projection + .expr + .iter() + .cloned() + .map(|expr| { + if let Some(ref mut rewriter) = alias_rewriter { + expr.rewrite(rewriter).data() + } else { + Ok(expr) + } + }) + .collect::>>()? + } else { + projection.expr.clone() + }; + Ok(Some( + LogicalPlanBuilder::from(plan).project(exprs)?.build()?, + )) + } else { + Ok(None) + } } - _ => Ok(plan.clone()), + _ => Ok(None), } } diff --git a/datafusion/sql/src/unparser/rewrite.rs b/datafusion/sql/src/unparser/rewrite.rs index 70339a061b34..84a35b347e52 100644 --- a/datafusion/sql/src/unparser/rewrite.rs +++ b/datafusion/sql/src/unparser/rewrite.rs @@ -20,9 +20,10 @@ use std::{ sync::Arc, }; +use arrow_schema::Schema; use datafusion_common::{ - tree_node::{Transformed, TransformedResult, TreeNode}, - Result, + tree_node::{Transformed, TransformedResult, TreeNode, TreeNodeRewriter}, + Column, Result, TableReference, }; use datafusion_expr::{expr::Alias, tree_node::transform_sort_vec}; use datafusion_expr::{Expr, LogicalPlan, Projection, Sort, SortExpr}; @@ -100,25 +101,25 @@ fn rewrite_sort_expr_for_union(exprs: Vec) -> Result> { Ok(sort_exprs) } -// Rewrite logic plan for query that order by columns are not in projections -// Plan before rewrite: -// -// Projection: j1.j1_string, j2.j2_string -// Sort: j1.j1_id DESC NULLS FIRST, j2.j2_id DESC NULLS FIRST -// Projection: j1.j1_string, j2.j2_string, j1.j1_id, j2.j2_id -// Inner Join: Filter: j1.j1_id = j2.j2_id -// TableScan: j1 -// TableScan: j2 -// -// Plan after rewrite -// -// Sort: j1.j1_id DESC NULLS FIRST, j2.j2_id DESC NULLS FIRST -// Projection: j1.j1_string, j2.j2_string -// Inner Join: Filter: j1.j1_id = j2.j2_id -// TableScan: j1 -// TableScan: j2 -// -// This prevents the original plan generate query with derived table but missing alias. +/// Rewrite logic plan for query that order by columns are not in projections +/// Plan before rewrite: +/// +/// Projection: j1.j1_string, j2.j2_string +/// Sort: j1.j1_id DESC NULLS FIRST, j2.j2_id DESC NULLS FIRST +/// Projection: j1.j1_string, j2.j2_string, j1.j1_id, j2.j2_id +/// Inner Join: Filter: j1.j1_id = j2.j2_id +/// TableScan: j1 +/// TableScan: j2 +/// +/// Plan after rewrite +/// +/// Sort: j1.j1_id DESC NULLS FIRST, j2.j2_id DESC NULLS FIRST +/// Projection: j1.j1_string, j2.j2_string +/// Inner Join: Filter: j1.j1_id = j2.j2_id +/// TableScan: j1 +/// TableScan: j2 +/// +/// This prevents the original plan generate query with derived table but missing alias. pub(super) fn rewrite_plan_for_sort_on_non_projected_fields( p: &Projection, ) -> Option { @@ -190,33 +191,33 @@ pub(super) fn rewrite_plan_for_sort_on_non_projected_fields( } } -// This logic is to work out the columns and inner query for SubqueryAlias plan for both types of -// subquery -// - `(SELECT column_a as a from table) AS A` -// - `(SELECT column_a from table) AS A (a)` -// -// A roundtrip example for table alias with columns -// -// query: SELECT id FROM (SELECT j1_id from j1) AS c (id) -// -// LogicPlan: -// Projection: c.id -// SubqueryAlias: c -// Projection: j1.j1_id AS id -// Projection: j1.j1_id -// TableScan: j1 -// -// Before introducing this logic, the unparsed query would be `SELECT c.id FROM (SELECT j1.j1_id AS -// id FROM (SELECT j1.j1_id FROM j1)) AS c`. -// The query is invalid as `j1.j1_id` is not a valid identifier in the derived table -// `(SELECT j1.j1_id FROM j1)` -// -// With this logic, the unparsed query will be: -// `SELECT c.id FROM (SELECT j1.j1_id FROM j1) AS c (id)` -// -// Caveat: this won't handle the case like `select * from (select 1, 2) AS a (b, c)` -// as the parser gives a wrong plan which has mismatch `Int(1)` types: Literal and -// Column in the Projections. Once the parser side is fixed, this logic should work +/// This logic is to work out the columns and inner query for SubqueryAlias plan for both types of +/// subquery +/// - `(SELECT column_a as a from table) AS A` +/// - `(SELECT column_a from table) AS A (a)` +/// +/// A roundtrip example for table alias with columns +/// +/// query: SELECT id FROM (SELECT j1_id from j1) AS c (id) +/// +/// LogicPlan: +/// Projection: c.id +/// SubqueryAlias: c +/// Projection: j1.j1_id AS id +/// Projection: j1.j1_id +/// TableScan: j1 +/// +/// Before introducing this logic, the unparsed query would be `SELECT c.id FROM (SELECT j1.j1_id AS +/// id FROM (SELECT j1.j1_id FROM j1)) AS c`. +/// The query is invalid as `j1.j1_id` is not a valid identifier in the derived table +/// `(SELECT j1.j1_id FROM j1)` +/// +/// With this logic, the unparsed query will be: +/// `SELECT c.id FROM (SELECT j1.j1_id FROM j1) AS c (id)` +/// +/// Caveat: this won't handle the case like `select * from (select 1, 2) AS a (b, c)` +/// as the parser gives a wrong plan which has mismatch `Int(1)` types: Literal and +/// Column in the Projections. Once the parser side is fixed, this logic should work pub(super) fn subquery_alias_inner_query_and_columns( subquery_alias: &datafusion_expr::SubqueryAlias, ) -> (&LogicalPlan, Vec) { @@ -292,7 +293,7 @@ pub(super) fn inject_column_aliases_into_subquery( /// - `SELECT col1, col2 FROM table` with aliases `["alias_1", "some_alias_2"]` will be transformed to /// - `SELECT col1 AS alias_1, col2 AS some_alias_2 FROM table` pub(super) fn inject_column_aliases( - projection: &datafusion_expr::Projection, + projection: &Projection, aliases: impl IntoIterator, ) -> LogicalPlan { let mut updated_projection = projection.clone(); @@ -329,3 +330,39 @@ fn find_projection(logical_plan: &LogicalPlan) -> Option<&Projection> { _ => None, } } + +/// A `TreeNodeRewriter` implementation that rewrites `Expr::Column` expressions by +/// replacing the column's name with an alias if the column exists in the provided schema. +/// +/// This is typically used to apply table aliases in query plans, ensuring that +/// the column references in the expressions use the correct table alias. +/// +/// # Fields +/// +/// * `table_schema`: The schema (`SchemaRef`) representing the table structure +/// from which the columns are referenced. This is used to look up columns by their names. +/// * `alias_name`: The alias (`TableReference`) that will replace the table name +/// in the column references when applicable. +pub struct TableAliasRewriter<'a> { + pub table_schema: &'a Schema, + pub alias_name: TableReference, +} + +impl TreeNodeRewriter for TableAliasRewriter<'_> { + type Node = Expr; + + fn f_down(&mut self, expr: Expr) -> Result> { + match expr { + Expr::Column(column) => { + if let Ok(field) = self.table_schema.field_with_name(&column.name) { + let new_column = + Column::new(Some(self.alias_name.clone()), field.name().clone()); + Ok(Transformed::yes(Expr::Column(new_column))) + } else { + Ok(Transformed::no(Expr::Column(column))) + } + } + _ => Ok(Transformed::no(expr)), + } + } +} diff --git a/datafusion/sql/tests/cases/plan_to_sql.rs b/datafusion/sql/tests/cases/plan_to_sql.rs index 99b28934f668..ebab62928093 100644 --- a/datafusion/sql/tests/cases/plan_to_sql.rs +++ b/datafusion/sql/tests/cases/plan_to_sql.rs @@ -73,7 +73,7 @@ fn roundtrip_expr() { let ast = expr_to_sql(&expr)?; - Ok(format!("{}", ast)) + Ok(ast.to_string()) }; for (table, query, expected) in tests { @@ -187,7 +187,7 @@ fn roundtrip_statement() -> Result<()> { let roundtrip_statement = plan_to_sql(&plan)?; - let actual = format!("{}", &roundtrip_statement); + let actual = &roundtrip_statement.to_string(); println!("roundtrip sql: {actual}"); println!("plan {}", plan.display_indent()); @@ -219,7 +219,7 @@ fn roundtrip_crossjoin() -> Result<()> { let roundtrip_statement = plan_to_sql(&plan)?; - let actual = format!("{}", &roundtrip_statement); + let actual = &roundtrip_statement.to_string(); println!("roundtrip sql: {actual}"); println!("plan {}", plan.display_indent()); @@ -232,7 +232,7 @@ fn roundtrip_crossjoin() -> Result<()> { \n TableScan: j1\ \n TableScan: j2"; - assert_eq!(format!("{plan_roundtrip}"), expected); + assert_eq!(plan_roundtrip.to_string(), expected); Ok(()) } @@ -491,7 +491,7 @@ fn roundtrip_statement_with_dialect() -> Result<()> { let unparser = Unparser::new(&*query.unparser_dialect); let roundtrip_statement = unparser.plan_to_sql(&plan)?; - let actual = format!("{}", &roundtrip_statement); + let actual = &roundtrip_statement.to_string(); println!("roundtrip sql: {actual}"); println!("plan {}", plan.display_indent()); @@ -521,7 +521,7 @@ fn test_unnest_logical_plan() -> Result<()> { \n Projection: unnest_table.struct_col AS UNNEST(unnest_table.struct_col), unnest_table.array_col AS UNNEST(unnest_table.array_col), unnest_table.struct_col, unnest_table.array_col\ \n TableScan: unnest_table"; - assert_eq!(format!("{plan}"), expected); + assert_eq!(plan.to_string(), expected); Ok(()) } @@ -541,7 +541,7 @@ fn test_table_references_in_plan_to_sql() { .unwrap(); let sql = plan_to_sql(&plan).unwrap(); - assert_eq!(format!("{}", sql), expected_sql) + assert_eq!(sql.to_string(), expected_sql) } test("catalog.schema.table", "SELECT catalog.\"schema\".\"table\".id, catalog.\"schema\".\"table\".\"value\" FROM catalog.\"schema\".\"table\""); @@ -565,7 +565,7 @@ fn test_table_scan_with_no_projection_in_plan_to_sql() { .build() .unwrap(); let sql = plan_to_sql(&plan).unwrap(); - assert_eq!(format!("{}", sql), expected_sql) + assert_eq!(sql.to_string(), expected_sql) } test( @@ -653,8 +653,11 @@ fn test_pretty_roundtrip() -> Result<()> { Ok(()) } -fn sql_round_trip(query: &str, expect: &str) { - let statement = Parser::new(&GenericDialect {}) +fn sql_round_trip(dialect: D, query: &str, expect: &str) +where + D: Dialect, +{ + let statement = Parser::new(&dialect) .try_with_sql(query) .unwrap() .parse_statement() @@ -675,23 +678,103 @@ fn sql_round_trip(query: &str, expect: &str) { } #[test] -fn test_table_scan_pushdown() -> Result<()> { +fn test_table_scan_alias() -> Result<()> { let schema = Schema::new(vec![ Field::new("id", DataType::Utf8, false), Field::new("age", DataType::Utf8, false), ]); + let plan = table_scan(Some("t1"), &schema, None)? + .project(vec![col("id")])? + .alias("a")? + .build()?; + let sql = plan_to_sql(&plan)?; + assert_eq!(sql.to_string(), "SELECT * FROM (SELECT t1.id FROM t1) AS a"); + + let plan = table_scan(Some("t1"), &schema, None)? + .project(vec![col("id")])? + .alias("a")? + .build()?; + + let sql = plan_to_sql(&plan)?; + assert_eq!(sql.to_string(), "SELECT * FROM (SELECT t1.id FROM t1) AS a"); + + let plan = table_scan(Some("t1"), &schema, None)? + .filter(col("id").gt(lit(5)))? + .project(vec![col("id")])? + .alias("a")? + .build()?; + let sql = plan_to_sql(&plan)?; + assert_eq!( + sql.to_string(), + "SELECT * FROM (SELECT t1.id FROM t1 WHERE (t1.id > 5)) AS a" + ); + + 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))], + )? + .project(vec![col("id")])? + .alias("a")? + .build()?; + let table_scan_with_two_filter = plan_to_sql(&table_scan_with_two_filter)?; + assert_eq!( + table_scan_with_two_filter.to_string(), + "SELECT a.id FROM t1 AS a WHERE ((a.id > 1) AND (a.age < 2))" + ); + + let table_scan_with_fetch = + table_scan_with_filter_and_fetch(Some("t1"), &schema, None, vec![], Some(10))? + .project(vec![col("id")])? + .alias("a")? + .build()?; + let table_scan_with_fetch = plan_to_sql(&table_scan_with_fetch)?; + assert_eq!( + table_scan_with_fetch.to_string(), + "SELECT a.id FROM (SELECT * FROM t1 LIMIT 10) AS a" + ); + + let table_scan_with_pushdown_all = table_scan_with_filter_and_fetch( + Some("t1"), + &schema, + Some(vec![0, 1]), + vec![col("id").gt(lit(1))], + Some(10), + )? + .project(vec![col("id")])? + .alias("a")? + .build()?; + let table_scan_with_pushdown_all = plan_to_sql(&table_scan_with_pushdown_all)?; + assert_eq!( + table_scan_with_pushdown_all.to_string(), + "SELECT a.id FROM (SELECT a.id, a.age FROM t1 AS a WHERE (a.id > 1) LIMIT 10) AS a" + ); + Ok(()) +} + +#[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), + scan_with_projection.to_string(), "SELECT t1.id, t1.age FROM t1" ); + let scan_with_projection = table_scan(Some("t1"), &schema, Some(vec![1]))?.build()?; + let scan_with_projection = plan_to_sql(&scan_with_projection)?; + assert_eq!(scan_with_projection.to_string(), "SELECT 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"); + assert_eq!(scan_with_no_projection.to_string(), "SELECT * FROM t1"); let table_scan_with_projection_alias = table_scan(Some("t1"), &schema, Some(vec![0, 1]))? @@ -700,17 +783,28 @@ fn test_table_scan_pushdown() -> Result<()> { let table_scan_with_projection_alias = plan_to_sql(&table_scan_with_projection_alias)?; assert_eq!( - format!("{}", table_scan_with_projection_alias), + table_scan_with_projection_alias.to_string(), "SELECT ta.id, ta.age FROM t1 AS ta" ); + let table_scan_with_projection_alias = + table_scan(Some("t1"), &schema, Some(vec![1]))? + .alias("ta")? + .build()?; + let table_scan_with_projection_alias = + plan_to_sql(&table_scan_with_projection_alias)?; + assert_eq!( + table_scan_with_projection_alias.to_string(), + "SELECT 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), + table_scan_with_no_projection_alias.to_string(), "SELECT * FROM t1 AS ta" ); @@ -722,7 +816,7 @@ fn test_table_scan_pushdown() -> Result<()> { 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), + query_from_table_scan_with_projection.to_string(), "SELECT * FROM (SELECT t1.id, t1.age FROM t1)" ); @@ -735,7 +829,7 @@ fn test_table_scan_pushdown() -> Result<()> { .build()?; let table_scan_with_filter = plan_to_sql(&table_scan_with_filter)?; assert_eq!( - format!("{}", table_scan_with_filter), + table_scan_with_filter.to_string(), "SELECT * FROM t1 WHERE (t1.id > t1.age)" ); @@ -748,23 +842,23 @@ fn test_table_scan_pushdown() -> Result<()> { .build()?; let table_scan_with_two_filter = plan_to_sql(&table_scan_with_two_filter)?; assert_eq!( - format!("{}", table_scan_with_two_filter), + table_scan_with_two_filter.to_string(), "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_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!( + table_scan_with_filter_alias.to_string(), + "SELECT * FROM t1 AS ta WHERE (ta.id > ta.age)" + ); let table_scan_with_projection_and_filter = table_scan_with_filters( Some("t1"), @@ -776,16 +870,30 @@ fn test_table_scan_pushdown() -> Result<()> { 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), + table_scan_with_projection_and_filter.to_string(), "SELECT t1.id, t1.age FROM t1 WHERE (t1.id > t1.age)" ); + let table_scan_with_projection_and_filter = table_scan_with_filters( + Some("t1"), + &schema, + Some(vec![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!( + table_scan_with_projection_and_filter.to_string(), + "SELECT 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), + table_scan_with_inline_fetch.to_string(), "SELECT * FROM t1 LIMIT 10" ); @@ -800,7 +908,7 @@ fn test_table_scan_pushdown() -> Result<()> { 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), + table_scan_with_projection_and_inline_fetch.to_string(), "SELECT t1.id, t1.age FROM t1 LIMIT 10" ); @@ -814,15 +922,37 @@ fn test_table_scan_pushdown() -> Result<()> { .build()?; let table_scan_with_all = plan_to_sql(&table_scan_with_all)?; assert_eq!( - format!("{}", table_scan_with_all), + table_scan_with_all.to_string(), "SELECT t1.id, t1.age FROM t1 WHERE (t1.id > t1.age) LIMIT 10" ); Ok(()) } +// temporary disabled as need to include PR that adds `sort_with_limit` method +// #[test] +// fn test_sort_with_push_down_fetch() -> Result<()> { +// let schema = Schema::new(vec![ +// Field::new("id", DataType::Utf8, false), +// Field::new("age", DataType::Utf8, false), +// ]); + +// let plan = table_scan(Some("t1"), &schema, None)? +// .project(vec![col("id"), col("age")])? +// .sort_with_limit(vec![col("age").sort(true, true)], Some(10))? +// .build()?; + +// let sql = plan_to_sql(&plan)?; +// assert_eq!( +// format!("{}", sql), +// "SELECT t1.id, t1.age FROM t1 ORDER BY t1.age ASC NULLS FIRST LIMIT 10" +// ); +// Ok(()) +// } + #[test] fn test_interval_lhs_eq() { sql_round_trip( + GenericDialect {}, "select interval '2 seconds' = interval '2 seconds'", "SELECT (INTERVAL '2.000000000 SECS' = INTERVAL '2.000000000 SECS')", ); @@ -831,6 +961,7 @@ fn test_interval_lhs_eq() { #[test] fn test_interval_lhs_lt() { sql_round_trip( + GenericDialect {}, "select interval '2 seconds' < interval '2 seconds'", "SELECT (INTERVAL '2.000000000 SECS' < INTERVAL '2.000000000 SECS')", ); @@ -839,19 +970,19 @@ fn test_interval_lhs_lt() { #[test] fn test_order_by_to_sql() { // order by aggregation function - sql_round_trip( + sql_round_trip(MySqlDialect {}, r#"SELECT id, first_name, SUM(id) FROM person GROUP BY id, first_name ORDER BY SUM(id) ASC, first_name DESC, id, first_name LIMIT 10"#, r#"SELECT person.id, person.first_name, sum(person.id) FROM person GROUP BY person.id, person.first_name ORDER BY sum(person.id) ASC NULLS LAST, person.first_name DESC NULLS FIRST, person.id ASC NULLS LAST, person.first_name ASC NULLS LAST LIMIT 10"#, ); // order by aggregation function alias - sql_round_trip( + sql_round_trip(MySqlDialect {}, r#"SELECT id, first_name, SUM(id) as total_sum FROM person GROUP BY id, first_name ORDER BY total_sum ASC, first_name DESC, id, first_name LIMIT 10"#, r#"SELECT person.id, person.first_name, sum(person.id) AS total_sum FROM person GROUP BY person.id, person.first_name ORDER BY total_sum ASC NULLS LAST, person.first_name DESC NULLS FIRST, person.id ASC NULLS LAST, person.first_name ASC NULLS LAST LIMIT 10"#, ); // order by scalar function from projection - sql_round_trip( + sql_round_trip(MySqlDialect {}, r#"SELECT id, first_name, substr(first_name,0,5) FROM person ORDER BY id, substr(first_name,0,5)"#, r#"SELECT person.id, person.first_name, substr(person.first_name, 0, 5) FROM person ORDER BY person.id ASC NULLS LAST, substr(person.first_name, 0, 5) ASC NULLS LAST"#, ); @@ -859,7 +990,7 @@ fn test_order_by_to_sql() { #[test] fn test_aggregation_to_sql() { - sql_round_trip( + sql_round_trip(MySqlDialect {}, r#"SELECT id, first_name, SUM(id) AS total_sum, SUM(id) OVER (PARTITION BY first_name ROWS BETWEEN 5 PRECEDING AND 2 FOLLOWING) AS moving_sum, @@ -880,8 +1011,23 @@ GROUP BY person.id, person.first_name"#.replace("\n", " ").as_str(), #[test] fn test_unnest_to_sql() { - sql_round_trip( + sql_round_trip(MySqlDialect {}, r#"SELECT unnest(array_col) as u1, struct_col, array_col FROM unnest_table WHERE array_col != NULL ORDER BY struct_col, array_col"#, r#"SELECT UNNEST(unnest_table.array_col) AS u1, unnest_table.struct_col, unnest_table.array_col FROM unnest_table WHERE (unnest_table.array_col <> NULL) ORDER BY unnest_table.struct_col ASC NULLS LAST, unnest_table.array_col ASC NULLS LAST"#, ); +} + +#[test] +fn test_without_offset() { + sql_round_trip(MySqlDialect {}, "select 1", "SELECT 1"); +} + +#[test] +fn test_with_offset0() { + sql_round_trip(MySqlDialect {}, "select 1 offset 0", "SELECT 1"); +} + +#[test] +fn test_with_offset95() { + sql_round_trip(MySqlDialect {}, "select 1 offset 95", "SELECT 1 OFFSET 95"); } \ No newline at end of file