From 85a2bdefc3813e2a7255bb580ca335a7fe62e085 Mon Sep 17 00:00:00 2001 From: baishen Date: Mon, 15 Aug 2022 16:17:30 +0800 Subject: [PATCH] feat(planner): support explain syntax --- Cargo.lock | 25 ++ src/query/ast/Cargo.toml | 1 + src/query/ast/src/ast/expr.rs | 5 +- src/query/ast/src/ast/format/mod.rs | 45 +++ src/query/ast/src/ast/format/syntax/ddl.rs | 228 +++++++++++ src/query/ast/src/ast/format/syntax/dml.rs | 198 ++++++++++ src/query/ast/src/ast/format/syntax/expr.rs | 370 ++++++++++++++++++ src/query/ast/src/ast/format/syntax/mod.rs | 49 +++ src/query/ast/src/ast/format/syntax/query.rs | 368 +++++++++++++++++ src/query/ast/src/ast/mod.rs | 2 + src/query/ast/src/ast/statements/copy.rs | 20 +- src/query/ast/src/ast/statements/explain.rs | 3 +- src/query/ast/src/ast/statements/statement.rs | 3 +- src/query/ast/src/ast/statements/table.rs | 5 +- src/query/ast/src/parser/statement.rs | 35 +- src/query/ast/src/parser/token.rs | 2 + src/query/ast/tests/it/testdata/statement.txt | 2 +- .../interpreters/interpreter_explain_v2.rs | 13 +- src/query/service/src/sql/optimizer/mod.rs | 12 +- .../suites/mode/standalone/04_0002_explain_v2 | 118 ++++++ 20 files changed, 1465 insertions(+), 39 deletions(-) create mode 100644 src/query/ast/src/ast/format/mod.rs create mode 100644 src/query/ast/src/ast/format/syntax/ddl.rs create mode 100644 src/query/ast/src/ast/format/syntax/dml.rs create mode 100644 src/query/ast/src/ast/format/syntax/expr.rs create mode 100644 src/query/ast/src/ast/format/syntax/mod.rs create mode 100644 src/query/ast/src/ast/format/syntax/query.rs diff --git a/Cargo.lock b/Cargo.lock index 3c2017042d34a..51548563e2ca8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,6 +122,12 @@ dependencies = [ "nodrop", ] +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "arrayvec" version = "0.7.2" @@ -990,6 +996,7 @@ dependencies = [ "nom", "nom-rule", "pratt", + "pretty", "pretty_assertions", "regex", "thiserror", @@ -5711,6 +5718,18 @@ dependencies = [ "termtree", ] +[[package]] +name = "pretty" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83f3aa1e3ca87d3b124db7461265ac176b40c277f37e503eaa29c9c75c037846" +dependencies = [ + "arrayvec 0.5.2", + "log", + "typed-arena", + "unicode-segmentation", +] + [[package]] name = "pretty_assertions" version = "1.2.1" @@ -7902,6 +7921,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "typed-arena" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0685c84d5d54d1c26f7d3eb96cd41550adb97baed141a761cf335d3d33bcd0ae" + [[package]] name = "typed-headers" version = "0.2.0" diff --git a/src/query/ast/Cargo.toml b/src/query/ast/Cargo.toml index 19c56f9bd9299..f74c6fe28fc8c 100644 --- a/src/query/ast/Cargo.toml +++ b/src/query/ast/Cargo.toml @@ -30,6 +30,7 @@ logos = "0.12.1" nom = "7.1.1" nom-rule = "0.3.0" pratt = "0.3.0" +pretty = "0.11.3" thiserror = "1.0.31" url = "2.2.2" diff --git a/src/query/ast/src/ast/expr.rs b/src/query/ast/src/ast/expr.rs index 7b87a0799e8f8..7f2b815479c0c 100644 --- a/src/query/ast/src/ast/expr.rs +++ b/src/query/ast/src/ast/expr.rs @@ -819,7 +819,10 @@ impl<'a> Display for Expr<'a> { } write!(f, " END")?; } - Expr::Exists { subquery, .. } => { + Expr::Exists { not, subquery, .. } => { + if *not { + write!(f, "NOT ")?; + } write!(f, "EXISTS ({subquery})")?; } Expr::Subquery { diff --git a/src/query/ast/src/ast/format/mod.rs b/src/query/ast/src/ast/format/mod.rs new file mode 100644 index 0000000000000..04f2cb9dbc590 --- /dev/null +++ b/src/query/ast/src/ast/format/mod.rs @@ -0,0 +1,45 @@ +// Copyright 2022 Datafuse Labs. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod syntax; + +use pretty::RcDoc; +pub use syntax::pretty_statement; + +pub(crate) const NEST_FACTOR: isize = 4; + +pub(crate) fn interweave_comma<'a, D>(docs: D) -> RcDoc<'a> +where D: Iterator> { + RcDoc::intersperse(docs, RcDoc::text(",").append(RcDoc::line())) +} + +pub(crate) fn inline_comma<'a, D>(docs: D) -> RcDoc<'a> +where D: Iterator> { + RcDoc::intersperse(docs, RcDoc::text(",").append(RcDoc::space())) +} + +pub(crate) fn inline_dot<'a, D>(docs: D) -> RcDoc<'a> +where D: Iterator> { + RcDoc::intersperse(docs, RcDoc::text(".")) +} + +pub(crate) fn parenthenized(doc: RcDoc<'_>) -> RcDoc<'_> { + RcDoc::text("(") + .append(RcDoc::line_()) + .append(doc) + .nest(NEST_FACTOR) + .append(RcDoc::line_()) + .append(RcDoc::text(")")) + .group() +} diff --git a/src/query/ast/src/ast/format/syntax/ddl.rs b/src/query/ast/src/ast/format/syntax/ddl.rs new file mode 100644 index 0000000000000..c210497956961 --- /dev/null +++ b/src/query/ast/src/ast/format/syntax/ddl.rs @@ -0,0 +1,228 @@ +// Copyright 2022 Datafuse Labs. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use pretty::RcDoc; + +use super::expr::pretty_expr; +use super::query::pretty_query; +use crate::ast::interweave_comma; +use crate::ast::parenthenized; +use crate::ast::AlterTableAction; +use crate::ast::AlterTableStmt; +use crate::ast::AlterViewStmt; +use crate::ast::CreateTableSource; +use crate::ast::CreateTableStmt; +use crate::ast::CreateViewStmt; +use crate::ast::NEST_FACTOR; + +pub(crate) fn pretty_create_table(stmt: CreateTableStmt) -> RcDoc { + RcDoc::text("CREATE") + .append(if stmt.transient { + RcDoc::space().append(RcDoc::text("TRANSIENT")) + } else { + RcDoc::nil() + }) + .append(RcDoc::space().append(RcDoc::text("TABLE"))) + .append(if stmt.if_not_exists { + RcDoc::space().append(RcDoc::text("IF NOT EXISTS")) + } else { + RcDoc::nil() + }) + .append( + RcDoc::space() + .append(if let Some(catalog) = stmt.catalog { + RcDoc::text(catalog.to_string()).append(RcDoc::text(".")) + } else { + RcDoc::nil() + }) + .append(if let Some(database) = stmt.database { + RcDoc::text(database.to_string()).append(RcDoc::text(".")) + } else { + RcDoc::nil() + }) + .append(RcDoc::text(stmt.table.to_string())), + ) + .append(if let Some(source) = stmt.source { + pretty_table_source(source) + } else { + RcDoc::nil() + }) + .append(if let Some(engine) = stmt.engine { + RcDoc::space() + .append(RcDoc::text("ENGINE =")) + .append(RcDoc::space()) + .append(engine.to_string()) + } else { + RcDoc::nil() + }) + .append(if !stmt.cluster_by.is_empty() { + RcDoc::line() + .append(RcDoc::text("CLUSTER BY ")) + .append(parenthenized( + interweave_comma(stmt.cluster_by.into_iter().map(pretty_expr)).group(), + )) + } else { + RcDoc::nil() + }) + .append(if !stmt.table_options.is_empty() { + RcDoc::line() + .append(interweave_comma(stmt.table_options.iter().map(|(k, v)| { + RcDoc::text(k.clone()) + .append(RcDoc::space()) + .append(RcDoc::text("=")) + .append(RcDoc::space()) + .append(RcDoc::text("'")) + .append(RcDoc::text(v.clone())) + .append(RcDoc::text("'")) + }))) + .group() + } else { + RcDoc::nil() + }) + .append(if let Some(as_query) = stmt.as_query { + RcDoc::line().append(RcDoc::text("AS")).append( + RcDoc::line() + .nest(NEST_FACTOR) + .append(pretty_query(*as_query).nest(NEST_FACTOR).group()), + ) + } else { + RcDoc::nil() + }) +} + +fn pretty_table_source(source: CreateTableSource) -> RcDoc { + match source { + CreateTableSource::Columns(columns) => RcDoc::space().append(parenthenized( + interweave_comma( + columns + .into_iter() + .map(|column| RcDoc::text(column.to_string())), + ) + .group(), + )), + CreateTableSource::Like { + catalog, + database, + table, + } => RcDoc::space() + .append(RcDoc::text("LIKE")) + .append(RcDoc::space()) + .append(if let Some(catalog) = catalog { + RcDoc::text(catalog.to_string()).append(RcDoc::text(".")) + } else { + RcDoc::nil() + }) + .append(if let Some(database) = database { + RcDoc::text(database.to_string()).append(RcDoc::text(".")) + } else { + RcDoc::nil() + }) + .append(RcDoc::text(table.to_string())), + } +} + +pub(crate) fn pretty_alter_table(stmt: AlterTableStmt) -> RcDoc { + RcDoc::text("ALTER TABLE") + .append(if stmt.if_exists { + RcDoc::space().append(RcDoc::text("IF EXISTS")) + } else { + RcDoc::nil() + }) + .append( + RcDoc::space() + .append(if let Some(catalog) = stmt.catalog { + RcDoc::text(catalog.to_string()).append(RcDoc::text(".")) + } else { + RcDoc::nil() + }) + .append(if let Some(database) = stmt.database { + RcDoc::text(database.to_string()).append(RcDoc::text(".")) + } else { + RcDoc::nil() + }) + .append(RcDoc::text(stmt.table.to_string())), + ) + .append(pretty_alter_table_action(stmt.action)) +} + +pub(crate) fn pretty_alter_table_action(action: AlterTableAction) -> RcDoc { + match action { + AlterTableAction::RenameTable { new_table } => RcDoc::line() + .append(RcDoc::text("RENAME TO ")) + .append(RcDoc::text(new_table.to_string())), + AlterTableAction::AlterTableClusterKey { cluster_by } => RcDoc::line() + .append(RcDoc::text("CLUSTER BY ")) + .append(parenthenized( + interweave_comma(cluster_by.into_iter().map(pretty_expr)).group(), + )), + AlterTableAction::DropTableClusterKey => { + RcDoc::line().append(RcDoc::text("DROP CLUSTER KEY")) + } + } +} + +pub(crate) fn pretty_create_view(stmt: CreateViewStmt) -> RcDoc { + RcDoc::text("CREATE VIEW") + .append(if stmt.if_not_exists { + RcDoc::space().append(RcDoc::text("IF NOT EXISTS")) + } else { + RcDoc::nil() + }) + .append( + RcDoc::space() + .append(if let Some(catalog) = stmt.catalog { + RcDoc::text(catalog.to_string()).append(RcDoc::text(".")) + } else { + RcDoc::nil() + }) + .append(if let Some(database) = stmt.database { + RcDoc::text(database.to_string()).append(RcDoc::text(".")) + } else { + RcDoc::nil() + }) + .append(RcDoc::text(stmt.view.to_string())), + ) + .append( + RcDoc::line().append(RcDoc::text("AS")).append( + RcDoc::line() + .nest(NEST_FACTOR) + .append(pretty_query(*stmt.query).nest(NEST_FACTOR).group()), + ), + ) +} + +pub(crate) fn pretty_alter_view(stmt: AlterViewStmt) -> RcDoc { + RcDoc::text("ALTER VIEW") + .append( + RcDoc::space() + .append(if let Some(catalog) = stmt.catalog { + RcDoc::text(catalog.to_string()).append(RcDoc::text(".")) + } else { + RcDoc::nil() + }) + .append(if let Some(database) = stmt.database { + RcDoc::text(database.to_string()).append(RcDoc::text(".")) + } else { + RcDoc::nil() + }) + .append(RcDoc::text(stmt.view.to_string())), + ) + .append( + RcDoc::line().append(RcDoc::text("AS")).append( + RcDoc::line() + .nest(NEST_FACTOR) + .append(pretty_query(*stmt.query).nest(NEST_FACTOR).group()), + ), + ) +} diff --git a/src/query/ast/src/ast/format/syntax/dml.rs b/src/query/ast/src/ast/format/syntax/dml.rs new file mode 100644 index 0000000000000..5cd112a7c6f64 --- /dev/null +++ b/src/query/ast/src/ast/format/syntax/dml.rs @@ -0,0 +1,198 @@ +// Copyright 2022 Datafuse Labs. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use pretty::RcDoc; + +use super::expr::pretty_expr; +use super::query::pretty_query; +use super::query::pretty_table; +use crate::ast::inline_comma; +use crate::ast::interweave_comma; +use crate::ast::parenthenized; +use crate::ast::CopyStmt; +use crate::ast::CopyUnit; +use crate::ast::Expr; +use crate::ast::InsertSource; +use crate::ast::InsertStmt; +use crate::ast::TableReference; +use crate::ast::NEST_FACTOR; + +pub(crate) fn pretty_insert(insert_stmt: InsertStmt) -> RcDoc { + RcDoc::text("INSERT") + .append(RcDoc::space()) + .append(if insert_stmt.overwrite { + RcDoc::text("OVERWRITE") + } else { + RcDoc::text("INTO") + }) + .append( + RcDoc::line() + .nest(NEST_FACTOR) + .append(if let Some(catalog) = insert_stmt.catalog { + RcDoc::text(catalog.to_string()).append(RcDoc::text(".")) + } else { + RcDoc::nil() + }) + .append(if let Some(database) = insert_stmt.database { + RcDoc::text(database.to_string()).append(RcDoc::text(".")) + } else { + RcDoc::nil() + }) + .append(RcDoc::text(insert_stmt.table.to_string())) + .append(if !insert_stmt.columns.is_empty() { + RcDoc::space() + .append(RcDoc::text("(")) + .append(inline_comma( + insert_stmt + .columns + .into_iter() + .map(|ident| RcDoc::text(ident.to_string())), + )) + .append(RcDoc::text(")")) + } else { + RcDoc::nil() + }), + ) + .append(pretty_source(insert_stmt.source)) +} + +fn pretty_source(source: InsertSource) -> RcDoc { + RcDoc::line().append(match source { + InsertSource::Streaming { + format, + rest_tokens, + } => RcDoc::text("FORMAT") + .append(RcDoc::space()) + .append(RcDoc::text(format)) + .append( + RcDoc::line().nest(NEST_FACTOR).append(RcDoc::text( + (&rest_tokens[0].source[rest_tokens.first().unwrap().span.start + ..rest_tokens.last().unwrap().span.end]) + .to_string(), + )), + ), + InsertSource::Values { rest_tokens } => RcDoc::text("VALUES").append( + RcDoc::line().nest(NEST_FACTOR).append(RcDoc::text( + (&rest_tokens[0].source[rest_tokens.first().unwrap().span.start + ..rest_tokens.last().unwrap().span.end]) + .to_string(), + )), + ), + InsertSource::Select { query } => pretty_query(*query), + }) +} + +pub(crate) fn pretty_delete<'a>( + table: TableReference<'a>, + selection: Option>, +) -> RcDoc<'a> { + RcDoc::text("DELETE FROM") + .append(RcDoc::line().nest(NEST_FACTOR).append(pretty_table(table))) + .append(if let Some(selection) = selection { + RcDoc::line().append(RcDoc::text("WHERE")).append( + RcDoc::line() + .nest(NEST_FACTOR) + .append(pretty_expr(selection).nest(NEST_FACTOR).group()), + ) + } else { + RcDoc::nil() + }) +} + +pub(crate) fn pretty_copy(copy_stmt: CopyStmt) -> RcDoc { + RcDoc::text("COPY") + .append(RcDoc::line().append(RcDoc::text("INTO "))) + .append(pretty_copy_unit(copy_stmt.dst)) + .append(RcDoc::line().append(RcDoc::text("FROM "))) + .append(pretty_copy_unit(copy_stmt.src)) + .append(if !copy_stmt.files.is_empty() { + RcDoc::line() + .append(RcDoc::text("FILES = ")) + .append(parenthenized( + interweave_comma( + copy_stmt + .files + .into_iter() + .map(|file| RcDoc::text(format!("{:?}", file))), + ) + .group(), + )) + } else { + RcDoc::nil() + }) + .append(if !copy_stmt.pattern.is_empty() { + RcDoc::line() + .append(RcDoc::text("PATTERN = ")) + .append(RcDoc::text(format!("{:?}", copy_stmt.pattern))) + } else { + RcDoc::nil() + }) + .append(if !copy_stmt.file_format.is_empty() { + RcDoc::line() + .append(RcDoc::text("FILE_FORMAT = ")) + .append(parenthenized( + interweave_comma(copy_stmt.file_format.iter().map(|(k, v)| { + RcDoc::text(k.to_string()) + .append(RcDoc::space()) + .append(RcDoc::text("=")) + .append(RcDoc::space()) + .append(RcDoc::text(format!("{:?}", v))) + })) + .group(), + )) + } else { + RcDoc::nil() + }) + .append(if !copy_stmt.validation_mode.is_empty() { + RcDoc::line() + .append(RcDoc::text("VALIDATION_MODE = ")) + .append(RcDoc::text(copy_stmt.validation_mode)) + } else { + RcDoc::nil() + }) + .append(if copy_stmt.size_limit != 0 { + RcDoc::line() + .append(RcDoc::text("SIZE_LIMIT = ")) + .append(RcDoc::text(format!("{}", copy_stmt.size_limit))) + } else { + RcDoc::nil() + }) +} + +fn pretty_copy_unit(copy_unit: CopyUnit) -> RcDoc { + match copy_unit { + CopyUnit::Table { + catalog, + database, + table, + } => if let Some(catalog) = catalog { + RcDoc::text(catalog.to_string()).append(RcDoc::text(".")) + } else { + RcDoc::nil() + } + .append(if let Some(database) = database { + RcDoc::text(database.to_string()).append(RcDoc::text(".")) + } else { + RcDoc::nil() + }) + .append(RcDoc::text(table.to_string())), + CopyUnit::StageLocation { name, path } => RcDoc::text("@") + .append(RcDoc::text(name)) + .append(RcDoc::text(path)), + CopyUnit::UriLocation(v) => RcDoc::text(v.to_string()), + CopyUnit::Query(query) => RcDoc::text("(") + .append(pretty_query(*query)) + .append(RcDoc::text(")")), + } +} diff --git a/src/query/ast/src/ast/format/syntax/expr.rs b/src/query/ast/src/ast/format/syntax/expr.rs new file mode 100644 index 0000000000000..db961b1f5d7ca --- /dev/null +++ b/src/query/ast/src/ast/format/syntax/expr.rs @@ -0,0 +1,370 @@ +// Copyright 2022 Datafuse Labs. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use pretty::RcDoc; + +use super::query::pretty_query; +use crate::ast::inline_comma; +use crate::ast::interweave_comma; +use crate::ast::parenthenized; +use crate::ast::BinaryOperator; +use crate::ast::Expr; +use crate::ast::MapAccessor; +use crate::ast::NEST_FACTOR; + +pub(crate) fn pretty_expr(expr: Expr) -> RcDoc { + match expr { + Expr::ColumnRef { + database, + table, + column, + .. + } => if let Some(database) = database { + RcDoc::text(database.to_string()).append(RcDoc::text(".")) + } else { + RcDoc::nil() + } + .append(if let Some(table) = table { + RcDoc::text(table.to_string()).append(RcDoc::text(".")) + } else { + RcDoc::nil() + }) + .append(RcDoc::text(column.to_string())), + Expr::IsNull { expr, not, .. } => pretty_expr(*expr) + .append(RcDoc::space()) + .append(RcDoc::text("IS")) + .append(if not { + RcDoc::space().append(RcDoc::text("NOT")) + } else { + RcDoc::nil() + }) + .append(RcDoc::space()) + .append(RcDoc::text("NULL")), + Expr::IsDistinctFrom { + left, right, not, .. + } => pretty_expr(*left) + .append(RcDoc::space()) + .append(RcDoc::text("IS")) + .append(if not { + RcDoc::space().append(RcDoc::text("NOT")) + } else { + RcDoc::nil() + }) + .append(RcDoc::space()) + .append(RcDoc::text("DISTINCT FROM")) + .append(RcDoc::space()) + .append(pretty_expr(*right)), + Expr::InList { + expr, list, not, .. + } => pretty_expr(*expr) + .append(if not { + RcDoc::space().append(RcDoc::text("NOT")) + } else { + RcDoc::nil() + }) + .append(RcDoc::space()) + .append(RcDoc::text("IN (")) + .append(inline_comma(list.into_iter().map(pretty_expr))) + .append(RcDoc::text(")")), + Expr::InSubquery { + expr, + subquery, + not, + .. + } => pretty_expr(*expr) + .append(if not { + RcDoc::space().append(RcDoc::text("NOT")) + } else { + RcDoc::nil() + }) + .append(RcDoc::space()) + .append(RcDoc::text("IN (")) + .append(pretty_query(*subquery)) + .append(RcDoc::text(")")), + Expr::Between { + expr, + low, + high, + not, + .. + } => pretty_expr(*expr) + .append(if not { + RcDoc::space().append(RcDoc::text("NOT")) + } else { + RcDoc::nil() + }) + .append(RcDoc::space()) + .append(RcDoc::text("BETWEEN")) + .append(RcDoc::space()) + .append(pretty_expr(*low)) + .append(RcDoc::space()) + .append(RcDoc::text("AND")) + .append(RcDoc::space()) + .append(pretty_expr(*high)), + Expr::UnaryOp { op, expr, .. } => RcDoc::text(op.to_string()) + .append(RcDoc::space()) + .append(pretty_expr(*expr)), + Expr::BinaryOp { + op, left, right, .. + } => pretty_expr(*left) + .append( + match op { + BinaryOperator::And | BinaryOperator::Or => RcDoc::line(), + _ => RcDoc::space(), + } + .append(RcDoc::text(op.to_string())), + ) + .append(RcDoc::space()) + .append(pretty_expr(*right)), + Expr::Cast { + expr, + target_type, + pg_style, + .. + } => { + if pg_style { + pretty_expr(*expr) + .append(RcDoc::text("::")) + .append(RcDoc::text(target_type.to_string())) + } else { + RcDoc::text("CAST(") + .append(pretty_expr(*expr)) + .append(RcDoc::space()) + .append(RcDoc::text("AS")) + .append(RcDoc::space()) + .append(RcDoc::text(target_type.to_string())) + .append(RcDoc::text(")")) + } + } + Expr::TryCast { + expr, target_type, .. + } => RcDoc::text("TRY_CAST(") + .append(pretty_expr(*expr)) + .append(RcDoc::space()) + .append(RcDoc::text("AS")) + .append(RcDoc::space()) + .append(RcDoc::text(target_type.to_string())) + .append(RcDoc::text(")")), + Expr::Extract { + kind: field, expr, .. + } => RcDoc::text("EXTRACT(") + .append(RcDoc::text(field.to_string())) + .append(RcDoc::space()) + .append(RcDoc::text("FROM")) + .append(RcDoc::space()) + .append(pretty_expr(*expr)) + .append(RcDoc::text(")")), + Expr::Position { + substr_expr, + str_expr, + .. + } => RcDoc::text("POSITION(") + .append(pretty_expr(*substr_expr)) + .append(RcDoc::space()) + .append(RcDoc::text("IN")) + .append(RcDoc::space()) + .append(pretty_expr(*str_expr)) + .append(RcDoc::text(")")), + Expr::Substring { + expr, + substring_from, + substring_for, + .. + } => RcDoc::text("SUBSTRING(") + .append(pretty_expr(*expr)) + .append(if let Some(substring_from) = substring_from { + RcDoc::space() + .append(RcDoc::text("FROM")) + .append(RcDoc::space()) + .append(pretty_expr(*substring_from)) + } else { + RcDoc::nil() + }) + .append(if let Some(substring_for) = substring_for { + RcDoc::space() + .append(RcDoc::text("FOR")) + .append(RcDoc::space()) + .append(pretty_expr(*substring_for)) + } else { + RcDoc::nil() + }) + .append(RcDoc::text(")")), + Expr::Trim { + expr, trim_where, .. + } => RcDoc::text("TRIM(") + .append(if let Some((trim_where, trim_expr)) = trim_where { + RcDoc::text(trim_where.to_string()) + .append(RcDoc::space()) + .append(pretty_expr(*trim_expr)) + .append(RcDoc::space()) + .append(RcDoc::text("FROM")) + } else { + RcDoc::nil() + }) + .append(RcDoc::space()) + .append(pretty_expr(*expr)), + Expr::Literal { lit, .. } => RcDoc::text(lit.to_string()), + Expr::CountAll { .. } => RcDoc::text("COUNT(*)"), + Expr::Tuple { exprs, .. } => RcDoc::text("(") + .append(inline_comma(exprs.into_iter().map(pretty_expr))) + .append(RcDoc::text(")")), + Expr::FunctionCall { + distinct, + name, + args, + params, + .. + } => RcDoc::text(name.to_string()) + .append(if !params.is_empty() { + RcDoc::text("(") + .append(inline_comma( + params + .into_iter() + .map(|literal| RcDoc::text(literal.to_string())), + )) + .append(")") + } else { + RcDoc::nil() + }) + .append(RcDoc::text("(")) + .append(if distinct { + RcDoc::text("DISTINCT").append(RcDoc::space()) + } else { + RcDoc::nil() + }) + .append(inline_comma(args.into_iter().map(pretty_expr))) + .append(RcDoc::text(")")), + Expr::Case { + operand, + conditions, + results, + else_result, + .. + } => RcDoc::text("CASE") + .append(if let Some(op) = operand { + RcDoc::space().append(RcDoc::text(op.to_string())) + } else { + RcDoc::nil() + }) + .append( + RcDoc::line() + .append(interweave_comma(conditions.iter().zip(results).map( + |(cond, res)| { + RcDoc::text("WHEN") + .append(RcDoc::space()) + .append(pretty_expr(cond.clone())) + .append(RcDoc::space()) + .append(RcDoc::text("THEN")) + .append(RcDoc::space()) + .append(pretty_expr(res.clone())) + }, + ))) + .nest(NEST_FACTOR) + .group(), + ) + .append(if let Some(el) = else_result { + RcDoc::line() + .nest(NEST_FACTOR) + .append(RcDoc::text("ELSE")) + .append(RcDoc::space()) + .append(pretty_expr(*el)) + } else { + RcDoc::nil() + }) + .append(RcDoc::line()) + .append(RcDoc::text("END")), + Expr::Exists { not, subquery, .. } => if not { + RcDoc::text("NOT").append(RcDoc::space()) + } else { + RcDoc::nil() + } + .append(RcDoc::text("EXISTS")) + .append(RcDoc::space()) + .append(parenthenized(pretty_query(*subquery))), + Expr::Subquery { + subquery, modifier, .. + } => if let Some(m) = modifier { + RcDoc::text(m.to_string()).append(RcDoc::space()) + } else { + RcDoc::nil() + } + .append(parenthenized(pretty_query(*subquery))), + Expr::MapAccess { expr, accessor, .. } => pretty_expr(*expr).append(match accessor { + MapAccessor::Bracket { key } => RcDoc::text("[") + .append(RcDoc::text(key.to_string())) + .append(RcDoc::text("]")), + MapAccessor::Period { key } => RcDoc::text(".").append(RcDoc::text(key.to_string())), + MapAccessor::Colon { key } => RcDoc::text(":").append(RcDoc::text(key.to_string())), + }), + Expr::Array { exprs, .. } => RcDoc::text("[") + .append(inline_comma(exprs.into_iter().map(pretty_expr))) + .append(RcDoc::text("]")), + Expr::Interval { expr, unit, .. } => RcDoc::text("INTERVAL") + .append(RcDoc::space()) + .append(pretty_expr(*expr)) + .append(RcDoc::space()) + .append(RcDoc::text(unit.to_string())), + Expr::DateAdd { + date, + interval, + unit, + .. + } => RcDoc::text("DATE_ADD(") + .append(pretty_expr(*date)) + .append(RcDoc::text(",")) + .append(RcDoc::space()) + .append(RcDoc::text("INTERVAL")) + .append(RcDoc::space()) + .append(pretty_expr(*interval)) + .append(RcDoc::space()) + .append(RcDoc::text(unit.to_string())) + .append(RcDoc::text(")")), + Expr::DateSub { + date, + interval, + unit, + .. + } => RcDoc::text("DATE_SUB(") + .append(pretty_expr(*date)) + .append(RcDoc::text(",")) + .append(RcDoc::space()) + .append(RcDoc::text("INTERVAL")) + .append(RcDoc::space()) + .append(pretty_expr(*interval)) + .append(RcDoc::space()) + .append(RcDoc::text(unit.to_string())) + .append(RcDoc::text(")")), + Expr::DateTrunc { unit, date, .. } => RcDoc::text("DATE_TRUNC(") + .append(RcDoc::text(unit.to_string())) + .append(RcDoc::text(",")) + .append(RcDoc::space()) + .append(pretty_expr(*date)) + .append(RcDoc::text(")")), + Expr::NullIf { expr1, expr2, .. } => RcDoc::text("NULLIF(") + .append(pretty_expr(*expr1)) + .append(RcDoc::text(",")) + .append(RcDoc::space()) + .append(pretty_expr(*expr2)) + .append(RcDoc::text(")")), + Expr::Coalesce { exprs, .. } => RcDoc::text("COALESCE(") + .append(inline_comma(exprs.into_iter().map(pretty_expr))) + .append(RcDoc::text(")")), + Expr::IfNull { expr1, expr2, .. } => RcDoc::text("IFNULL(") + .append(pretty_expr(*expr1)) + .append(RcDoc::text(",")) + .append(RcDoc::space()) + .append(pretty_expr(*expr2)) + .append(RcDoc::text(")")), + } +} diff --git a/src/query/ast/src/ast/format/syntax/mod.rs b/src/query/ast/src/ast/format/syntax/mod.rs new file mode 100644 index 0000000000000..1df0fa3d79c4c --- /dev/null +++ b/src/query/ast/src/ast/format/syntax/mod.rs @@ -0,0 +1,49 @@ +// Copyright 2022 Datafuse Labs. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod ddl; +mod dml; +mod expr; +mod query; + +use common_exception::Result; +use ddl::*; +use dml::*; +use pretty::RcDoc; +use query::*; + +use crate::ast::Statement; + +pub fn pretty_statement(stmt: Statement, max_width: usize) -> Result { + let pretty_stmt = match stmt { + // Format and beautify large SQL statements to make them easy to read. + Statement::Query(query) => pretty_query(*query), + Statement::Insert(insert_stmt) => pretty_insert(insert_stmt), + Statement::Delete { + table_reference, + selection, + } => pretty_delete(table_reference, selection), + Statement::Copy(copy_stmt) => pretty_copy(copy_stmt), + Statement::CreateTable(create_table_stmt) => pretty_create_table(create_table_stmt), + Statement::AlterTable(alter_table_stmt) => pretty_alter_table(alter_table_stmt), + Statement::CreateView(create_view_stmt) => pretty_create_view(create_view_stmt), + Statement::AlterView(alter_view_stmt) => pretty_alter_view(alter_view_stmt), + // Other SQL statements are relatively short and don't need extra format. + _ => RcDoc::text(stmt.to_string()), + }; + + let mut bs = Vec::new(); + pretty_stmt.render(max_width, &mut bs)?; + Ok(String::from_utf8(bs)?) +} diff --git a/src/query/ast/src/ast/format/syntax/query.rs b/src/query/ast/src/ast/format/syntax/query.rs new file mode 100644 index 0000000000000..93151484bb76e --- /dev/null +++ b/src/query/ast/src/ast/format/syntax/query.rs @@ -0,0 +1,368 @@ +// Copyright 2022 Datafuse Labs. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use pretty::RcDoc; + +use super::expr::pretty_expr; +use crate::ast::inline_comma; +use crate::ast::inline_dot; +use crate::ast::interweave_comma; +use crate::ast::parenthenized; +use crate::ast::Expr; +use crate::ast::JoinCondition; +use crate::ast::JoinOperator; +use crate::ast::OrderByExpr; +use crate::ast::Query; +use crate::ast::SelectTarget; +use crate::ast::SetExpr; +use crate::ast::SetOperator; +use crate::ast::TableReference; +use crate::ast::TimeTravelPoint; +use crate::ast::With; +use crate::ast::CTE; +use crate::ast::NEST_FACTOR; + +pub(crate) fn pretty_query(query: Query) -> RcDoc { + pretty_with(query.with) + .append(pretty_body(query.body)) + .append(pretty_order_by(query.order_by)) + .append(pretty_limit(query.limit)) + .append(pretty_offset(query.offset)) + .append(pretty_format(query.format)) + .group() +} + +fn pretty_with(with: Option) -> RcDoc { + if let Some(with) = with { + RcDoc::text("WITH") + .append(if with.recursive { + RcDoc::space().append(RcDoc::text("RECURSIVE")) + } else { + RcDoc::nil() + }) + .append(RcDoc::line().nest(NEST_FACTOR)) + .append( + interweave_comma(with.ctes.into_iter().map(pretty_cte)) + .nest(NEST_FACTOR) + .group(), + ) + .append(RcDoc::line()) + } else { + RcDoc::nil() + } +} + +fn pretty_cte(cte: CTE) -> RcDoc { + RcDoc::text(format!("{} AS", cte.alias)) + .append(RcDoc::softline()) + .append(parenthenized(pretty_query(cte.query))) +} + +fn pretty_body(body: SetExpr) -> RcDoc { + match body { + SetExpr::Select(select_stmt) => if select_stmt.distinct { + RcDoc::text("SELECT DISTINCT") + } else { + RcDoc::text("SELECT") + } + .append(pretty_select_list(select_stmt.select_list)) + .append(pretty_from(select_stmt.from)) + .append(pretty_selection(select_stmt.selection)) + .append(pretty_group_by(select_stmt.group_by)) + .append(pretty_having(select_stmt.having)), + SetExpr::Query(query) => parenthenized(pretty_query(*query)), + SetExpr::SetOperation(set_operation) => pretty_body(*set_operation.left) + .append( + RcDoc::line() + .append(match set_operation.op { + SetOperator::Union => RcDoc::text("UNION"), + SetOperator::Except => RcDoc::text("EXCEPT"), + SetOperator::Intersect => RcDoc::text("INTERSECT"), + }) + .append(if set_operation.all { + RcDoc::space().append(RcDoc::text("ALL")) + } else { + RcDoc::nil() + }), + ) + .append(RcDoc::line()) + .append(pretty_body(*set_operation.right)), + } +} + +fn pretty_select_list(select_list: Vec) -> RcDoc { + if select_list.len() > 1 { + RcDoc::line() + } else { + RcDoc::space() + } + .nest(NEST_FACTOR) + .append( + interweave_comma(select_list.into_iter().map(|select_target| { + match select_target { + SelectTarget::AliasedExpr { expr, alias } => { + pretty_expr(*expr).append(if let Some(alias) = alias { + RcDoc::space() + .append(RcDoc::text("AS")) + .append(RcDoc::space()) + .append(RcDoc::text(alias.to_string())) + } else { + RcDoc::nil() + }) + } + SelectTarget::QualifiedName(object_name) => inline_dot( + object_name + .into_iter() + .map(|indirection| RcDoc::text(indirection.to_string())), + ) + .group(), + } + })) + .nest(NEST_FACTOR) + .group(), + ) +} + +fn pretty_from(from: Vec) -> RcDoc { + if !from.is_empty() { + RcDoc::line() + .append(RcDoc::text("FROM").append(RcDoc::line().nest(NEST_FACTOR))) + .append( + interweave_comma(from.into_iter().map(pretty_table)) + .nest(NEST_FACTOR) + .group(), + ) + } else { + RcDoc::nil() + } +} + +fn pretty_selection(selection: Option) -> RcDoc { + if let Some(selection) = selection { + RcDoc::line().append(RcDoc::text("WHERE")).append( + RcDoc::line() + .nest(NEST_FACTOR) + .append(pretty_expr(selection).nest(NEST_FACTOR).group()), + ) + } else { + RcDoc::nil() + } +} + +fn pretty_group_by(group_by: Vec) -> RcDoc { + if !group_by.is_empty() { + RcDoc::line() + .append( + RcDoc::text("GROUP BY").append( + if group_by.len() > 1 { + RcDoc::line() + } else { + RcDoc::space() + } + .nest(NEST_FACTOR), + ), + ) + .append( + interweave_comma(group_by.into_iter().map(pretty_expr)) + .nest(NEST_FACTOR) + .group(), + ) + } else { + RcDoc::nil() + } +} + +fn pretty_having(having: Option) -> RcDoc { + if let Some(having) = having { + RcDoc::line() + .append(RcDoc::text("HAVING").append(RcDoc::line().nest(NEST_FACTOR))) + .append(pretty_expr(having)) + } else { + RcDoc::nil() + } +} + +pub(crate) fn pretty_table(table: TableReference) -> RcDoc { + match table { + TableReference::Table { + span: _, + catalog, + database, + table, + alias, + travel_point, + } => if let Some(catalog) = catalog { + RcDoc::text(catalog.to_string()).append(RcDoc::text(".")) + } else { + RcDoc::nil() + } + .append(if let Some(database) = database { + RcDoc::text(database.to_string()).append(RcDoc::text(".")) + } else { + RcDoc::nil() + }) + .append(RcDoc::text(table.to_string())) + .append(if let Some(TimeTravelPoint::Snapshot(sid)) = travel_point { + RcDoc::text(format!(" AT (SNAPSHOT => {sid})")) + } else if let Some(TimeTravelPoint::Timestamp(ts)) = travel_point { + RcDoc::text(format!(" AT (TIMESTAMP => {ts})")) + } else { + RcDoc::nil() + }) + .append(if let Some(alias) = alias { + RcDoc::text(format!(" AS {alias}")) + } else { + RcDoc::nil() + }), + TableReference::Subquery { + span: _, + subquery, + alias, + } => parenthenized(pretty_query(*subquery)).append(if let Some(alias) = alias { + RcDoc::text(format!(" AS {alias}")) + } else { + RcDoc::nil() + }), + TableReference::TableFunction { + span: _, + name, + params, + alias, + } => RcDoc::text(name.to_string()) + .append(RcDoc::text("(")) + .append(inline_comma(params.into_iter().map(pretty_expr))) + .append(RcDoc::text(")")) + .append(if let Some(alias) = alias { + RcDoc::text(format!(" AS {alias}")) + } else { + RcDoc::nil() + }), + TableReference::Join { span: _, join } => pretty_table(*join.left) + .append(RcDoc::line()) + .append(if join.condition == JoinCondition::Natural { + RcDoc::text("NATURAL").append(RcDoc::space()) + } else { + RcDoc::nil() + }) + .append(match join.op { + JoinOperator::Inner => RcDoc::text("INNER JOIN"), + JoinOperator::LeftOuter => RcDoc::text("LEFT OUTER JOIN"), + JoinOperator::RightOuter => RcDoc::text("RIGHT OUTER JOIN"), + JoinOperator::FullOuter => RcDoc::text("FULL OUTER JOIN"), + JoinOperator::CrossJoin => RcDoc::text("CROSS JOIN"), + }) + .append(RcDoc::space().append(pretty_table(*join.right))) + .append(match &join.condition { + JoinCondition::On(expr) => RcDoc::space() + .append(RcDoc::text("ON")) + .append(RcDoc::space()) + .append(pretty_expr(*expr.clone())), + JoinCondition::Using(idents) => RcDoc::space() + .append(RcDoc::text("USING(")) + .append(inline_comma( + idents.iter().map(|ident| RcDoc::text(ident.to_string())), + )) + .append(RcDoc::text(")")), + _ => RcDoc::nil(), + }), + } +} + +fn pretty_order_by(order_by: Vec) -> RcDoc { + if !order_by.is_empty() { + RcDoc::line() + .append( + RcDoc::text("ORDER BY").append( + if order_by.len() > 1 { + RcDoc::line() + } else { + RcDoc::space() + } + .nest(NEST_FACTOR), + ), + ) + .append( + interweave_comma(order_by.into_iter().map(pretty_order_by_expr)) + .nest(NEST_FACTOR) + .group(), + ) + } else { + RcDoc::nil() + } +} + +fn pretty_limit(limit: Vec) -> RcDoc { + if !limit.is_empty() { + RcDoc::line() + .append( + RcDoc::text("LIMIT").append( + if limit.len() > 1 { + RcDoc::line() + } else { + RcDoc::space() + } + .nest(NEST_FACTOR), + ), + ) + .append( + interweave_comma(limit.into_iter().map(pretty_expr)) + .nest(NEST_FACTOR) + .group(), + ) + } else { + RcDoc::nil() + } +} + +fn pretty_offset(offset: Option) -> RcDoc { + if let Some(offset) = offset { + RcDoc::line() + .append(RcDoc::text("OFFSET").append(RcDoc::space().nest(NEST_FACTOR))) + .append(pretty_expr(offset)) + } else { + RcDoc::nil() + } +} + +fn pretty_format<'a>(format: Option) -> RcDoc<'a> { + if let Some(format) = format { + RcDoc::line() + .append(RcDoc::text("FORMAT").append(RcDoc::space().nest(NEST_FACTOR))) + .append(RcDoc::text(format)) + } else { + RcDoc::nil() + } +} + +fn pretty_order_by_expr(order_by_expr: OrderByExpr) -> RcDoc { + RcDoc::text(order_by_expr.expr.to_string()) + .append(if let Some(asc) = order_by_expr.asc { + if asc { + RcDoc::space().append(RcDoc::text("ASC")) + } else { + RcDoc::space().append(RcDoc::text("DESC")) + } + } else { + RcDoc::nil() + }) + .append(if let Some(nulls_first) = order_by_expr.nulls_first { + if nulls_first { + RcDoc::space().append(RcDoc::text("NULLS FIRST")) + } else { + RcDoc::space().append(RcDoc::text("NULLS LAST")) + } + } else { + RcDoc::nil() + }) +} diff --git a/src/query/ast/src/ast/mod.rs b/src/query/ast/src/ast/mod.rs index 5f4accf056646..ed1b2921a63b8 100644 --- a/src/query/ast/src/ast/mod.rs +++ b/src/query/ast/src/ast/mod.rs @@ -13,6 +13,7 @@ // limitations under the License. mod expr; +mod format; mod query; mod statements; @@ -20,6 +21,7 @@ use std::fmt::Display; use std::fmt::Formatter; pub use expr::*; +pub use format::*; pub use query::*; pub use statements::*; diff --git a/src/query/ast/src/ast/statements/copy.rs b/src/query/ast/src/ast/statements/copy.rs index 6b7dc6f2ff66d..2d5e73ea219dc 100644 --- a/src/query/ast/src/ast/statements/copy.rs +++ b/src/query/ast/src/ast/statements/copy.rs @@ -46,14 +46,6 @@ impl Display for CopyStmt<'_> { write!(f, " INTO {}", self.dst)?; write!(f, " FROM {}", self.src)?; - if !self.file_format.is_empty() { - write!(f, " FILE_FORMAT = (")?; - for (k, v) in self.file_format.iter() { - write!(f, " {} = '{}'", k, v)?; - } - write!(f, " )")?; - } - if !self.files.is_empty() { write!(f, " FILES = (")?; write_quoted_comma_separated_list(f, &self.files)?; @@ -64,14 +56,22 @@ impl Display for CopyStmt<'_> { write!(f, " PATTERN = '{}'", self.pattern)?; } - if self.size_limit != 0 { - write!(f, " SIZE_LIMIT = {}", self.size_limit)?; + if !self.file_format.is_empty() { + write!(f, " FILE_FORMAT = (")?; + for (k, v) in self.file_format.iter() { + write!(f, " {} = '{}'", k, v)?; + } + write!(f, " )")?; } if !self.validation_mode.is_empty() { write!(f, "VALIDATION_MODE = {}", self.validation_mode)?; } + if self.size_limit != 0 { + write!(f, " SIZE_LIMIT = {}", self.size_limit)?; + } + Ok(()) } } diff --git a/src/query/ast/src/ast/statements/explain.rs b/src/query/ast/src/ast/statements/explain.rs index 156f90e4c7263..aa73ccfc8caeb 100644 --- a/src/query/ast/src/ast/statements/explain.rs +++ b/src/query/ast/src/ast/statements/explain.rs @@ -14,9 +14,10 @@ #[derive(Debug, Clone, PartialEq, Eq)] pub enum ExplainKind { - Syntax, + Syntax(String), Graph, Pipeline, Fragments, Raw, + Plan, } diff --git a/src/query/ast/src/ast/statements/statement.rs b/src/query/ast/src/ast/statements/statement.rs index f6bfe27b617b2..a09263f19a0d3 100644 --- a/src/query/ast/src/ast/statements/statement.rs +++ b/src/query/ast/src/ast/statements/statement.rs @@ -179,11 +179,12 @@ impl<'a> Display for Statement<'a> { Statement::Explain { kind, query } => { write!(f, "EXPLAIN")?; match *kind { - ExplainKind::Syntax => (), + ExplainKind::Syntax(_) => write!(f, " SYNTAX")?, ExplainKind::Graph => write!(f, " GRAPH")?, ExplainKind::Pipeline => write!(f, " PIPELINE")?, ExplainKind::Fragments => write!(f, " FRAGMENTS")?, ExplainKind::Raw => write!(f, " RAW")?, + ExplainKind::Plan => (), } write!(f, " {query}")?; } diff --git a/src/query/ast/src/ast/statements/table.rs b/src/query/ast/src/ast/statements/table.rs index 448a66d03a520..1a0345c13403d 100644 --- a/src/query/ast/src/ast/statements/table.rs +++ b/src/query/ast/src/ast/statements/table.rs @@ -131,12 +131,13 @@ impl Display for CreateTableStmt<'_> { } if let Some(engine) = &self.engine { - write!(f, " ENGINE={engine}")?; + write!(f, " ENGINE = {engine}")?; } if !self.cluster_by.is_empty() { - write!(f, " CLUSTER BY ")?; + write!(f, " CLUSTER BY (")?; write_comma_separated_list(f, &self.cluster_by)?; + write!(f, ")")? } // Format table options diff --git a/src/query/ast/src/parser/statement.rs b/src/query/ast/src/parser/statement.rs index 70231d08177aa..d49d6acc683e2 100644 --- a/src/query/ast/src/parser/statement.rs +++ b/src/query/ast/src/parser/statement.rs @@ -38,20 +38,27 @@ use crate::util::*; use crate::ErrorKind; pub fn statement(i: Input) -> IResult { - let explain = map( - rule! { - EXPLAIN ~ ( PIPELINE | GRAPH | FRAGMENTS | RAW )? ~ #statement - }, - |(_, opt_kind, statement)| Statement::Explain { - kind: match opt_kind.map(|token| token.kind) { - Some(TokenKind::PIPELINE) => ExplainKind::Pipeline, - Some(TokenKind::GRAPH) => ExplainKind::Graph, - Some(TokenKind::FRAGMENTS) => ExplainKind::Fragments, - Some(TokenKind::RAW) => ExplainKind::Raw, - None => ExplainKind::Syntax, - _ => unreachable!(), - }, - query: Box::new(statement.stmt), + let explain = map_res( + rule! { + EXPLAIN ~ ( SYNTAX | PIPELINE | GRAPH | FRAGMENTS | RAW )? ~ #statement + }, + |(_, opt_kind, statement)| { + Ok(Statement::Explain { + kind: match opt_kind.map(|token| token.kind) { + Some(TokenKind::SYNTAX) => { + let pretty_stmt = pretty_statement(statement.stmt.clone(), 10) + .map_err(|_| ErrorKind::Other("invalid statement"))?; + ExplainKind::Syntax(pretty_stmt) + } + Some(TokenKind::PIPELINE) => ExplainKind::Pipeline, + Some(TokenKind::GRAPH) => ExplainKind::Graph, + Some(TokenKind::FRAGMENTS) => ExplainKind::Fragments, + Some(TokenKind::RAW) => ExplainKind::Raw, + None => ExplainKind::Plan, + _ => unreachable!(), + }, + query: Box::new(statement.stmt), + }) }, ); let insert = map( diff --git a/src/query/ast/src/parser/token.rs b/src/query/ast/src/parser/token.rs index ae4df2c207f7e..7904a3a3e7cbd 100644 --- a/src/query/ast/src/parser/token.rs +++ b/src/query/ast/src/parser/token.rs @@ -605,6 +605,8 @@ pub enum TokenKind { SNAPSHOT, #[token("STAGE", ignore(ascii_case))] STAGE, + #[token("SYNTAX", ignore(ascii_case))] + SYNTAX, #[token("USAGE", ignore(ascii_case))] USAGE, #[token("UPDATE", ignore(ascii_case))] diff --git a/src/query/ast/tests/it/testdata/statement.txt b/src/query/ast/tests/it/testdata/statement.txt index b7b44377bdc94..2652ffd4e44e4 100644 --- a/src/query/ast/tests/it/testdata/statement.txt +++ b/src/query/ast/tests/it/testdata/statement.txt @@ -536,7 +536,7 @@ CreateTable( ---------- Input ---------- create table t like t2 engine = memory; ---------- Output --------- -CREATE TABLE t LIKE t2 ENGINE=MEMORY +CREATE TABLE t LIKE t2 ENGINE = MEMORY ---------- AST ------------ CreateTable( CreateTableStmt { diff --git a/src/query/service/src/interpreters/interpreter_explain_v2.rs b/src/query/service/src/interpreters/interpreter_explain_v2.rs index 2e1e8544161ba..a2cfd2ac2dce5 100644 --- a/src/query/service/src/interpreters/interpreter_explain_v2.rs +++ b/src/query/service/src/interpreters/interpreter_explain_v2.rs @@ -50,7 +50,8 @@ impl Interpreter for ExplainInterpreterV2 { async fn execute(&self) -> Result { let blocks = match &self.kind { - ExplainKind::Syntax | ExplainKind::Raw => self.explain_syntax_or_raw(&self.plan)?, + ExplainKind::Syntax(pretty_stmt) => self.explain_syntax(pretty_stmt.clone())?, + ExplainKind::Raw | ExplainKind::Plan => self.explain_raw_or_plan(&self.plan)?, ExplainKind::Pipeline => match &self.plan { Plan::Query { s_expr, metadata, .. @@ -105,7 +106,15 @@ impl ExplainInterpreterV2 { }) } - pub fn explain_syntax_or_raw(&self, plan: &Plan) -> Result> { + pub fn explain_syntax(&self, pretty_stmt: String) -> Result> { + let line_splitted_result: Vec<&str> = pretty_stmt.lines().collect(); + let formatted_sql = Series::from_data(line_splitted_result); + Ok(vec![DataBlock::create(self.schema.clone(), vec![ + formatted_sql, + ])]) + } + + pub fn explain_raw_or_plan(&self, plan: &Plan) -> Result> { let result = plan.format_indent()?; let line_splitted_result: Vec<&str> = result.lines().collect(); let formatted_plan = Series::from_data(line_splitted_result); diff --git a/src/query/service/src/sql/optimizer/mod.rs b/src/query/service/src/sql/optimizer/mod.rs index 6896edf0f8077..400bd98d5582a 100644 --- a/src/query/service/src/sql/optimizer/mod.rs +++ b/src/query/service/src/sql/optimizer/mod.rs @@ -84,15 +84,13 @@ pub fn optimize( metadata, rewrite_kind, }), - Plan::Explain { kind, plan } => { - if kind == ExplainKind::Raw { - return Ok(Plan::Explain { kind, plan }); - } - Ok(Plan::Explain { + Plan::Explain { kind, plan } => match kind { + ExplainKind::Raw | ExplainKind::Syntax(_) => Ok(Plan::Explain { kind, plan }), + _ => Ok(Plan::Explain { kind, plan: Box::new(optimize(ctx, opt_ctx, *plan)?), - }) - } + }), + }, Plan::Copy(v) => { Ok(Plan::Copy(Box::new(match *v { CopyPlanV2::IntoStage { diff --git a/tests/logictest/suites/mode/standalone/04_0002_explain_v2 b/tests/logictest/suites/mode/standalone/04_0002_explain_v2 index 8e2a6be72b2ec..5dc0545ea4d84 100644 --- a/tests/logictest/suites/mode/standalone/04_0002_explain_v2 +++ b/tests/logictest/suites/mode/standalone/04_0002_explain_v2 @@ -67,6 +67,124 @@ Project: [a (#0),b (#1),a (#2),b (#3)] ├── LogicalGet: default.default.t1 └── LogicalGet: default.default.t2 +onlyif mysql +statement query T +explain syntax select 1, 'ab', [1,2,3], (1, 'a'); + +---- +SELECT + 1, + 'ab', + [1, 2, 3], + (1, 'a') + +onlyif mysql +statement query T +explain syntax select a, sum(b) as sum from t1 where a in (1, 2) and b > 0 and b < 100 group by a order by a; + +---- +SELECT + a, + sum(b) AS sum +FROM + t1 +WHERE + a IN (1, 2) + AND b > 0 + AND b < 100 +GROUP BY a +ORDER BY a + +onlyif mysql +statement query T +explain syntax select * from t1 inner join t2 on t1.a = t2.a and t1.b = t2.b and t1.a > 2; + +---- +SELECT * +FROM + t1 + INNER JOIN t2 ON t1.a = t2.a + AND t1.b = t2.b + AND t1.a > 2 + +onlyif mysql +statement query T +explain syntax delete from t1 where a > 100 and b > 1 and b < 10; + +---- +DELETE FROM + t1 +WHERE + a > 100 + AND b > 1 + AND b < 10 + +onlyif mysql +statement query T +explain syntax copy into t1 from 's3://mybucket/data.csv' file_format = ( type = 'CSV' field_delimiter = ',' record_delimiter = '\n' skip_header = 1) size_limit=10; + +---- +COPY +INTO t1 +FROM 's3://mybucket/data.csv' +FILE_FORMAT = ( + field_delimiter = ",", + record_delimiter = "\n", + skip_header = "1", + type = "CSV" +) +SIZE_LIMIT = 10 + +onlyif mysql +statement query T +explain syntax copy into 's3://mybucket/data.csv' from t1 file_format = ( type = 'CSV' field_delimiter = ',' record_delimiter = '\n' skip_header = 1) size_limit=10; + +---- +COPY +INTO 's3://mybucket/data.csv' +FROM t1 +FILE_FORMAT = ( + field_delimiter = ",", + record_delimiter = "\n", + skip_header = "1", + type = "CSV" +) +SIZE_LIMIT = 10 + +onlyif mysql +statement query T +explain syntax create table t3(a int64, b uint64, c float64, d string, e array(int32), f tuple(f1 bool, f2 string)) engine=fuse cluster by (a, b, c) comment='test' compression='LZ4'; + +---- +CREATE TABLE t3 ( + a Int64 NOT NULL, + b UInt64 NOT NULL, + c Float64 NOT NULL, + d STRING NOT NULL, + e ARRAY(Int32) NOT NULL, + f TUPLE(f1 BOOLEAN, f2 STRING) NOT NULL +) ENGINE = FUSE +CLUSTER BY ( + a, + b, + c +) +comment = 'test', +compression = 'LZ4' + +onlyif mysql +statement query T +explain syntax create view v as select number % 3 as a from numbers(100) where number > 10; + +---- +CREATE VIEW v +AS + SELECT number % 3 AS a + FROM + numbers(100) + WHERE + number > 10 + onlyif mysql statement ok drop table t1;