diff --git a/src/common/query/src/lib.rs b/src/common/query/src/lib.rs index ca81ad9e41d4..3686cff83699 100644 --- a/src/common/query/src/lib.rs +++ b/src/common/query/src/lib.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::fmt::{Debug, Formatter}; +use std::fmt::{Debug, Display, Formatter}; use std::sync::Arc; use api::greptime_proto::v1::add_column_location::LocationType; @@ -126,6 +126,17 @@ pub enum AddColumnLocation { After { column_name: String }, } +impl Display for AddColumnLocation { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + AddColumnLocation::First => write!(f, r#"FIRST"#), + AddColumnLocation::After { column_name } => { + write!(f, r#"AFTER {column_name}"#) + } + } + } +} + impl From<&AddColumnLocation> for Location { fn from(value: &AddColumnLocation) -> Self { match value { diff --git a/src/sql/src/statements.rs b/src/sql/src/statements.rs index de35b71a90a8..27a0c9327a24 100644 --- a/src/sql/src/statements.rs +++ b/src/sql/src/statements.rs @@ -43,6 +43,7 @@ use datatypes::schema::constraint::{CURRENT_TIMESTAMP, CURRENT_TIMESTAMP_FN}; use datatypes::schema::{ColumnDefaultConstraint, ColumnSchema, COMMENT_KEY}; use datatypes::types::{cast, TimestampType}; use datatypes::value::{OrderedF32, OrderedF64, Value}; +use itertools::Itertools; pub use option_map::OptionMap; use snafu::{ensure, OptionExt, ResultExt}; use sqlparser::ast::{ExactNumberInfo, UnaryOperator}; @@ -58,6 +59,29 @@ use crate::error::{ SerializeColumnDefaultConstraintSnafu, TimestampOverflowSnafu, UnsupportedDefaultValueSnafu, }; +const REDACTED_OPTIONS: [&str; 2] = ["access_key_id", "secret_access_key"]; + +/// Convert the options into redacted and sorted key-value string. Options with key in +/// [REDACTED_OPTIONS] will be converted into ` = '******'`. +fn redact_and_sort_options(options: &OptionMap) -> Vec { + let options = options.as_ref(); + let mut result = Vec::with_capacity(options.len()); + let keys = options.keys().sorted(); + for key in keys { + if let Some(val) = options.get(key) { + let redacted = REDACTED_OPTIONS + .iter() + .any(|opt| opt.eq_ignore_ascii_case(key)); + if redacted { + result.push(format!("{key} = '******'")); + } else { + result.push(format!("{key} = '{}'", val.escape_default())); + } + } + } + result +} + fn parse_string_to_value( column_name: &str, s: String, diff --git a/src/sql/src/statements/alter.rs b/src/sql/src/statements/alter.rs index cf3dc1bf9179..a54ba2d41b74 100644 --- a/src/sql/src/statements/alter.rs +++ b/src/sql/src/statements/alter.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::fmt::{Debug, Display}; + use common_query::AddColumnLocation; use sqlparser::ast::{ColumnDef, Ident, ObjectName, TableConstraint}; use sqlparser_derive::{Visit, VisitMut}; @@ -39,6 +41,14 @@ impl AlterTable { } } +impl Display for AlterTable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let table_name = self.table_name(); + let alter_operation = self.alter_operation(); + write!(f, r#"ALTER TABLE {table_name} {alter_operation}"#) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut)] pub enum AlterTableOperation { /// `ADD ` @@ -53,3 +63,100 @@ pub enum AlterTableOperation { /// `RENAME ` RenameTable { new_table_name: String }, } + +impl Display for AlterTableOperation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AlterTableOperation::AddConstraint(constraint) => write!(f, r#"ADD {constraint}"#), + AlterTableOperation::AddColumn { + column_def, + location, + } => { + if let Some(location) = location { + write!(f, r#"ADD COLUMN {column_def} {location}"#) + } else { + write!(f, r#"ADD COLUMN {column_def}"#) + } + } + AlterTableOperation::DropColumn { name } => write!(f, r#"DROP COLUMN {name}"#), + AlterTableOperation::RenameTable { new_table_name } => { + write!(f, r#"RENAME {new_table_name}"#) + } + } + } +} + +#[cfg(test)] +mod tests { + use std::assert_matches::assert_matches; + + use crate::dialect::GreptimeDbDialect; + use crate::parser::{ParseOptions, ParserContext}; + use crate::statements::statement::Statement; + + #[test] + fn test_display_alter() { + let sql = r"alter table monitor add column app string default 'shop' primary key;"; + let stmts = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::Alter { .. }); + + match &stmts[0] { + Statement::Alter(set) => { + let new_sql = format!("\n{}", set); + assert_eq!( + r#" +ALTER TABLE monitor ADD COLUMN app STRING DEFAULT 'shop' PRIMARY KEY"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + + let sql = r"alter table monitor drop column load_15;"; + let stmts = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::Alter { .. }); + + match &stmts[0] { + Statement::Alter(set) => { + let new_sql = format!("\n{}", set); + assert_eq!( + r#" +ALTER TABLE monitor DROP COLUMN load_15"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + + let sql = r"alter table monitor rename monitor_new;"; + let stmts = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::Alter { .. }); + + match &stmts[0] { + Statement::Alter(set) => { + let new_sql = format!("\n{}", set); + assert_eq!( + r#" +ALTER TABLE monitor RENAME monitor_new"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + } +} diff --git a/src/sql/src/statements/copy.rs b/src/sql/src/statements/copy.rs index 8d3104f29e69..c801c3bb62fc 100644 --- a/src/sql/src/statements/copy.rs +++ b/src/sql/src/statements/copy.rs @@ -12,10 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::fmt::Display; + use sqlparser::ast::ObjectName; use sqlparser_derive::{Visit, VisitMut}; -use crate::statements::OptionMap; +use crate::statements::{redact_and_sort_options, OptionMap}; #[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut)] pub enum Copy { @@ -23,18 +25,77 @@ pub enum Copy { CopyDatabase(CopyDatabase), } +impl Display for Copy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Copy::CopyTable(s) => s.fmt(f), + Copy::CopyDatabase(s) => s.fmt(f), + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut)] pub enum CopyTable { To(CopyTableArgument), From(CopyTableArgument), } +impl Display for CopyTable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "COPY ")?; + let (with, connection) = match self { + CopyTable::To(args) => { + write!(f, "{} TO {}", &args.table_name, &args.location)?; + (&args.with, &args.connection) + } + CopyTable::From(args) => { + write!(f, "{} FROM {}", &args.table_name, &args.location)?; + (&args.with, &args.connection) + } + }; + if !with.map.is_empty() { + let options = redact_and_sort_options(with); + write!(f, " WITH ({})", options.join(", "))?; + } + if !connection.map.is_empty() { + let options = redact_and_sort_options(connection); + write!(f, " CONNECTION ({})", options.join(", "))?; + } + Ok(()) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut)] pub enum CopyDatabase { To(CopyDatabaseArgument), From(CopyDatabaseArgument), } +impl Display for CopyDatabase { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "COPY DATABASE ")?; + let (with, connection) = match self { + CopyDatabase::To(args) => { + write!(f, "{} TO {}", &args.database_name, &args.location)?; + (&args.with, &args.connection) + } + CopyDatabase::From(args) => { + write!(f, "{} FROM {}", &args.database_name, &args.location)?; + (&args.with, &args.connection) + } + }; + if !with.map.is_empty() { + let options = redact_and_sort_options(with); + write!(f, " WITH ({})", options.join(", "))?; + } + if !connection.map.is_empty() { + let options = redact_and_sort_options(connection); + write!(f, " CONNECTION ({})", options.join(", "))?; + } + Ok(()) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut)] pub struct CopyDatabaseArgument { pub database_name: ObjectName, @@ -67,3 +128,112 @@ impl CopyTableArgument { .cloned() } } + +#[cfg(test)] +mod tests { + use std::assert_matches::assert_matches; + + use crate::dialect::GreptimeDbDialect; + use crate::parser::{ParseOptions, ParserContext}; + use crate::statements::statement::Statement; + + #[test] + fn test_display_copy_from_tb() { + let sql = r"copy tbl from 's3://my-bucket/data.parquet' + with (format = 'parquet', pattern = '.*parquet.*') + connection(region = 'us-west-2', secret_access_key = '12345678');"; + let stmts = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::Copy { .. }); + + match &stmts[0] { + Statement::Copy(copy) => { + let new_sql = format!("{}", copy); + assert_eq!( + r#"COPY tbl FROM s3://my-bucket/data.parquet WITH (format = 'parquet', pattern = '.*parquet.*') CONNECTION (region = 'us-west-2', secret_access_key = '******')"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + } + + #[test] + fn test_display_copy_to_tb() { + let sql = r"copy tbl to 's3://my-bucket/data.parquet' + with (format = 'parquet', pattern = '.*parquet.*') + connection(region = 'us-west-2', secret_access_key = '12345678');"; + let stmts = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::Copy { .. }); + + match &stmts[0] { + Statement::Copy(copy) => { + let new_sql = format!("{}", copy); + assert_eq!( + r#"COPY tbl TO s3://my-bucket/data.parquet WITH (format = 'parquet', pattern = '.*parquet.*') CONNECTION (region = 'us-west-2', secret_access_key = '******')"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + } + + #[test] + fn test_display_copy_from_db() { + let sql = r"copy database db1 from 's3://my-bucket/data.parquet' + with (format = 'parquet', pattern = '.*parquet.*') + connection(region = 'us-west-2', secret_access_key = '12345678');"; + let stmts = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::Copy { .. }); + + match &stmts[0] { + Statement::Copy(copy) => { + let new_sql = format!("{}", copy); + assert_eq!( + r#"COPY DATABASE db1 FROM s3://my-bucket/data.parquet WITH (format = 'parquet', pattern = '.*parquet.*') CONNECTION (region = 'us-west-2', secret_access_key = '******')"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + } + + #[test] + fn test_display_copy_to_db() { + let sql = r"copy database db1 to 's3://my-bucket/data.parquet' + with (format = 'parquet', pattern = '.*parquet.*') + connection(region = 'us-west-2', secret_access_key = '12345678');"; + let stmts = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::Copy { .. }); + + match &stmts[0] { + Statement::Copy(copy) => { + let new_sql = format!("{}", copy); + assert_eq!( + r#"COPY DATABASE db1 TO s3://my-bucket/data.parquet WITH (format = 'parquet', pattern = '.*parquet.*') CONNECTION (region = 'us-west-2', secret_access_key = '******')"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + } +} diff --git a/src/sql/src/statements/create.rs b/src/sql/src/statements/create.rs index cfcbd8d68242..eb992b48ef45 100644 --- a/src/sql/src/statements/create.rs +++ b/src/sql/src/statements/create.rs @@ -20,7 +20,7 @@ use sqlparser::ast::Expr; use sqlparser_derive::{Visit, VisitMut}; use crate::ast::{ColumnDef, Ident, ObjectName, SqlOption, TableConstraint, Value as SqlValue}; -use crate::statements::OptionMap; +use crate::statements::{redact_and_sort_options, OptionMap}; const LINE_SEP: &str = ",\n"; const COMMA_SEP: &str = ", "; @@ -47,6 +47,23 @@ macro_rules! format_list_comma { }; } +fn format_table_constraint(constraints: &[TableConstraint]) -> String { + constraints + .iter() + .map(|c| { + if is_time_index(c) { + let TableConstraint::Unique { columns, .. } = c else { + unreachable!() + }; + + format_indent!("{}TIME INDEX ({})", format_list_comma!(columns)) + } else { + format_indent!(c) + } + }) + .join(LINE_SEP) +} + /// Time index name, used in table constraints. pub const TIME_INDEX: &str = "__time_index"; @@ -74,58 +91,6 @@ pub struct CreateTable { pub partitions: Option, } -impl CreateTable { - fn format_constraints(&self) -> String { - self.constraints - .iter() - .map(|c| { - if is_time_index(c) { - let TableConstraint::Unique { columns, .. } = c else { - unreachable!() - }; - - format_indent!("{}TIME INDEX ({})", format_list_comma!(columns)) - } else { - format_indent!(c) - } - }) - .join(LINE_SEP) - } - - #[inline] - fn format_partitions(&self) -> String { - if let Some(partitions) = &self.partitions { - format!("{}\n", partitions) - } else { - String::default() - } - } - - #[inline] - fn format_if_not_exists(&self) -> &str { - if self.if_not_exists { - "IF NOT EXISTS" - } else { - "" - } - } - - #[inline] - fn format_options(&self) -> String { - if self.options.is_empty() { - String::default() - } else { - let options: Vec<&SqlOption> = self.options.iter().sorted().collect(); - let options = format_list_indent!(options); - format!( - r#"WITH( -{options} -)"# - ) - } - } -} - #[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut)] pub struct Partitions { pub column_list: Vec, @@ -166,36 +131,37 @@ impl Display for Partitions { "PARTITION ON COLUMNS ({}) (\n{}\n)", format_list_comma!(self.column_list), format_list_indent!(self.exprs), - ) - } else { - write!(f, "") + )?; } + Ok(()) } } impl Display for CreateTable { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let if_not_exists = self.format_if_not_exists(); - let name = &self.name; - let columns = format_list_indent!(self.columns); - let constraints = self.format_constraints(); - let partitions = self.format_partitions(); - let engine = &self.engine; - let options = self.format_options(); - let maybe_external = if self.engine == FILE_ENGINE { - "EXTERNAL " - } else { - "" - }; - write!( - f, - r#"CREATE {maybe_external}TABLE {if_not_exists} {name} ( -{columns}, -{constraints} -) -{partitions}ENGINE={engine} -{options}"# - ) + write!(f, "CREATE ")?; + if self.engine == FILE_ENGINE { + write!(f, "EXTERNAL ")?; + } + write!(f, "TABLE ")?; + if self.if_not_exists { + write!(f, "IF NOT EXISTS ")?; + } + writeln!(f, "{} (", &self.name)?; + writeln!(f, "{},", format_list_indent!(self.columns))?; + writeln!(f, "{}", format_table_constraint(&self.constraints))?; + writeln!(f, ")")?; + if let Some(partitions) = &self.partitions { + writeln!(f, "{partitions}")?; + } + writeln!(f, "ENGINE={}", &self.engine)?; + if !self.options.is_empty() { + writeln!(f, "WITH(")?; + let options: Vec<&SqlOption> = self.options.iter().sorted().collect(); + writeln!(f, "{}", format_list_indent!(options))?; + write!(f, ")")?; + } + Ok(()) } } @@ -216,6 +182,16 @@ impl CreateDatabase { } } +impl Display for CreateDatabase { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "CREATE DATABASE ")?; + if self.if_not_exists { + write!(f, "IF NOT EXISTS ")?; + } + write!(f, "{}", &self.name) + } +} + #[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut)] pub struct CreateExternalTable { /// Table name @@ -229,6 +205,27 @@ pub struct CreateExternalTable { pub engine: String, } +impl Display for CreateExternalTable { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "CREATE EXTERNAL TABLE ")?; + if self.if_not_exists { + write!(f, "IF NOT EXISTS ")?; + } + writeln!(f, "{} (", &self.name)?; + writeln!(f, "{},", format_list_indent!(self.columns))?; + writeln!(f, "{}", format_table_constraint(&self.constraints))?; + writeln!(f, ")")?; + writeln!(f, "ENGINE={}", &self.engine)?; + if !self.options.map.is_empty() { + let options = redact_and_sort_options(&self.options); + writeln!(f, "WITH(")?; + writeln!(f, "{}", format_list_indent!(options))?; + write!(f, ")")?; + } + Ok(()) + } +} + #[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut)] pub struct CreateTableLike { /// Table name @@ -237,6 +234,14 @@ pub struct CreateTableLike { pub source_name: ObjectName, } +impl Display for CreateTableLike { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let table_name = &self.table_name; + let source_name = &self.source_name; + write!(f, r#"CREATE TABLE {table_name} LIKE {source_name}"#) + } +} + #[cfg(test)] mod tests { use std::assert_matches::assert_matches; @@ -392,4 +397,116 @@ ENGINE=mito ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()); assert_matches!(result, Err(Error::InvalidTableOption { .. })) } + + #[test] + fn test_display_create_database() { + let sql = r"create database test;"; + let stmts = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::CreateDatabase { .. }); + + match &stmts[0] { + Statement::CreateDatabase(set) => { + let new_sql = format!("\n{}", set); + assert_eq!( + r#" +CREATE DATABASE test"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + + let sql = r"create database if not exists test;"; + let stmts = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::CreateDatabase { .. }); + + match &stmts[0] { + Statement::CreateDatabase(set) => { + let new_sql = format!("\n{}", set); + assert_eq!( + r#" +CREATE DATABASE IF NOT EXISTS test"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + } + + #[test] + fn test_display_create_table_like() { + let sql = r"create table t2 like t1;"; + let stmts = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::CreateTableLike { .. }); + + match &stmts[0] { + Statement::CreateTableLike(create) => { + let new_sql = format!("\n{}", create); + assert_eq!( + r#" +CREATE TABLE t2 LIKE t1"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + } + + #[test] + fn test_display_create_external_table() { + let sql = r#"CREATE EXTERNAL TABLE city ( + host string, + ts timestamp, + cpu float64 default 0, + memory float64, + TIME INDEX (ts), + PRIMARY KEY(host) +) WITH (location='/var/data/city.csv', format='csv');"#; + let stmts = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::CreateExternalTable { .. }); + + match &stmts[0] { + Statement::CreateExternalTable(create) => { + let new_sql = format!("\n{}", create); + assert_eq!( + r#" +CREATE EXTERNAL TABLE city ( + host STRING, + ts TIMESTAMP, + cpu FLOAT64 DEFAULT 0, + memory FLOAT64, + TIME INDEX (ts), + PRIMARY KEY (host) +) +ENGINE=file +WITH( + format = 'csv', + location = '/var/data/city.csv' +)"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + } } diff --git a/src/sql/src/statements/describe.rs b/src/sql/src/statements/describe.rs index 6fe190800900..743f2b0123c2 100644 --- a/src/sql/src/statements/describe.rs +++ b/src/sql/src/statements/describe.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::fmt::Display; + use sqlparser::ast::ObjectName; use sqlparser_derive::{Visit, VisitMut}; @@ -32,6 +34,13 @@ impl DescribeTable { } } +impl Display for DescribeTable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let name = self.name(); + write!(f, r#"DESCRIBE TABLE {name}"#) + } +} + #[cfg(test)] mod tests { use std::assert_matches::assert_matches; @@ -108,4 +117,28 @@ mod tests { ) .is_err()); } + + #[test] + fn test_display_describe_table() { + let sql = r"describe table monitor;"; + let stmts = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::DescribeTable { .. }); + + match &stmts[0] { + Statement::DescribeTable(set) => { + let new_sql = format!("\n{}", set); + assert_eq!( + r#" +DESCRIBE TABLE monitor"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + } } diff --git a/src/sql/src/statements/drop.rs b/src/sql/src/statements/drop.rs index 62da68a90c9c..4725f512816d 100644 --- a/src/sql/src/statements/drop.rs +++ b/src/sql/src/statements/drop.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::fmt::Display; + use sqlparser::ast::ObjectName; use sqlparser_derive::{Visit, VisitMut}; @@ -41,6 +43,17 @@ impl DropTable { } } +impl Display for DropTable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("DROP TABLE")?; + if self.drop_if_exists() { + f.write_str(" IF EXISTS")?; + } + let table_name = self.table_name(); + write!(f, r#" {table_name}"#) + } +} + /// DROP DATABASE statement. #[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut)] pub struct DropDatabase { @@ -66,3 +79,113 @@ impl DropDatabase { self.drop_if_exists } } + +impl Display for DropDatabase { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("DROP DATABASE")?; + if self.drop_if_exists() { + f.write_str(" IF EXISTS")?; + } + let name = self.name(); + write!(f, r#" {name}"#) + } +} + +#[cfg(test)] +mod tests { + use std::assert_matches::assert_matches; + + use crate::dialect::GreptimeDbDialect; + use crate::parser::{ParseOptions, ParserContext}; + use crate::statements::statement::Statement; + + #[test] + fn test_display_drop_database() { + let sql = r"drop database test;"; + let stmts = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::DropDatabase { .. }); + + match &stmts[0] { + Statement::DropDatabase(set) => { + let new_sql = format!("\n{}", set); + assert_eq!( + r#" +DROP DATABASE test"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + + let sql = r"drop database if exists test;"; + let stmts = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::DropDatabase { .. }); + + match &stmts[0] { + Statement::DropDatabase(set) => { + let new_sql = format!("\n{}", set); + assert_eq!( + r#" +DROP DATABASE IF EXISTS test"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + } + + #[test] + fn test_display_drop_table() { + let sql = r"drop table test;"; + let stmts = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::DropTable { .. }); + + match &stmts[0] { + Statement::DropTable(set) => { + let new_sql = format!("\n{}", set); + assert_eq!( + r#" +DROP TABLE test"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + + let sql = r"drop table if exists test;"; + let stmts = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::DropTable { .. }); + + match &stmts[0] { + Statement::DropTable(set) => { + let new_sql = format!("\n{}", set); + assert_eq!( + r#" +DROP TABLE IF EXISTS test"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + } +} diff --git a/src/sql/src/statements/set_variables.rs b/src/sql/src/statements/set_variables.rs index 71d6849833a8..7a2a94a531df 100644 --- a/src/sql/src/statements/set_variables.rs +++ b/src/sql/src/statements/set_variables.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::fmt::Display; + use sqlparser::ast::{Expr, ObjectName}; use sqlparser_derive::{Visit, VisitMut}; @@ -21,3 +23,50 @@ pub struct SetVariables { pub variable: ObjectName, pub value: Vec, } + +impl Display for SetVariables { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let variable = &self.variable; + let value = &self + .value + .iter() + .map(|expr| format!("{}", expr)) + .collect::>() + .join(", "); + + write!(f, r#"SET {variable} = {value}"#) + } +} + +#[cfg(test)] +mod tests { + use std::assert_matches::assert_matches; + + use crate::dialect::GreptimeDbDialect; + use crate::parser::{ParseOptions, ParserContext}; + use crate::statements::statement::Statement; + + #[test] + fn test_display_show_variables() { + let sql = r"set delayed_insert_timeout=300;"; + let stmts = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::SetVariables { .. }); + + match &stmts[0] { + Statement::SetVariables(set) => { + let new_sql = format!("\n{}", set); + assert_eq!( + r#" +SET delayed_insert_timeout = 300"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + } +} diff --git a/src/sql/src/statements/show.rs b/src/sql/src/statements/show.rs index 13cbb2f69ce0..f20a6a59191c 100644 --- a/src/sql/src/statements/show.rs +++ b/src/sql/src/statements/show.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::fmt; +use std::fmt::{self, Display}; use sqlparser_derive::{Visit, VisitMut}; @@ -26,7 +26,7 @@ pub enum ShowKind { Where(Expr), } -impl fmt::Display for ShowKind { +impl Display for ShowKind { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { ShowKind::All => write!(f, "ALL"), @@ -51,6 +51,20 @@ pub struct ShowColumns { pub full: bool, } +impl Display for ShowColumns { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SHOW ")?; + if self.full { + write!(f, "FULL ")?; + } + write!(f, "COLUMNS IN {} ", &self.table)?; + if let Some(database) = &self.database { + write!(f, "IN {database} ")?; + } + write!(f, "{}", &self.kind) + } +} + /// The SQL `SHOW INDEX` statement #[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut)] pub struct ShowIndex { @@ -59,6 +73,16 @@ pub struct ShowIndex { pub database: Option, } +impl Display for ShowIndex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SHOW INDEX IN {} ", &self.table)?; + if let Some(database) = &self.database { + write!(f, "IN {database} ")?; + } + write!(f, "{}", &self.kind) + } +} + impl ShowDatabases { /// Creates a statement for `SHOW DATABASES` pub fn new(kind: ShowKind) -> Self { @@ -66,6 +90,13 @@ impl ShowDatabases { } } +impl Display for ShowDatabases { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let kind = &self.kind; + write!(f, r#"SHOW DATABASES {kind}"#) + } +} + /// SQL structure for `SHOW TABLES`. #[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut)] pub struct ShowTables { @@ -74,18 +105,46 @@ pub struct ShowTables { pub full: bool, } +impl Display for ShowTables { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SHOW ")?; + if self.full { + write!(f, "FULL ")?; + } + write!(f, "TABLES ")?; + if let Some(database) = &self.database { + write!(f, "IN {database} ")?; + } + write!(f, "{}", &self.kind) + } +} + /// SQL structure for `SHOW CREATE TABLE`. #[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut)] pub struct ShowCreateTable { pub table_name: ObjectName, } +impl Display for ShowCreateTable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let table_name = &self.table_name; + write!(f, r#"SHOW CREATE TABLE {table_name}"#) + } +} + /// SQL structure for `SHOW VARIABLES xxx`. #[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut)] pub struct ShowVariables { pub variable: ObjectName, } +impl Display for ShowVariables { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let variable = &self.variable; + write!(f, r#"SHOW VARIABLES {variable}"#) + } +} + #[cfg(test)] mod tests { use std::assert_matches::assert_matches; @@ -171,4 +230,162 @@ mod tests { ) .is_err()); } + + #[test] + fn test_display_show_variables() { + let sql = r"show variables v1;"; + let stmts: Vec = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::ShowVariables { .. }); + match &stmts[0] { + Statement::ShowVariables(show) => { + let new_sql = format!("\n{}", show); + assert_eq!( + r#" +SHOW VARIABLES v1"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + } + + #[test] + fn test_display_show_create_table() { + let sql = r"show create table monitor;"; + let stmts: Vec = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::ShowCreateTable { .. }); + match &stmts[0] { + Statement::ShowCreateTable(show) => { + let new_sql = format!("\n{}", show); + assert_eq!( + r#" +SHOW CREATE TABLE monitor"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + } + + #[test] + fn test_display_show_index() { + let sql = r"show index from t1 from d1;"; + let stmts: Vec = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::ShowIndex { .. }); + match &stmts[0] { + Statement::ShowIndex(show) => { + let new_sql = format!("\n{}", show); + assert_eq!( + r#" +SHOW INDEX IN t1 IN d1 ALL"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + } + + #[test] + fn test_display_show_columns() { + let sql = r"show full columns in t1 in d1;"; + let stmts: Vec = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::ShowColumns { .. }); + match &stmts[0] { + Statement::ShowColumns(show) => { + let new_sql = format!("\n{}", show); + assert_eq!( + r#" +SHOW FULL COLUMNS IN t1 IN d1 ALL"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + } + + #[test] + fn test_display_show_tables() { + let sql = r"show full tables in d1;"; + let stmts: Vec = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::ShowTables { .. }); + match &stmts[0] { + Statement::ShowTables(show) => { + let new_sql = format!("\n{}", show); + assert_eq!( + r#" +SHOW FULL TABLES IN d1 ALL"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + + let sql = r"show full tables;"; + let stmts: Vec = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::ShowTables { .. }); + match &stmts[0] { + Statement::ShowTables(show) => { + let new_sql = format!("\n{}", show); + assert_eq!( + r#" +SHOW FULL TABLES ALL"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + } + + #[test] + fn test_display_show_databases() { + let sql = r"show databases;"; + let stmts: Vec = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::ShowDatabases { .. }); + match &stmts[0] { + Statement::ShowDatabases(show) => { + let new_sql = format!("\n{}", show); + assert_eq!( + r#" +SHOW DATABASES ALL"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + } } diff --git a/src/sql/src/statements/statement.rs b/src/sql/src/statements/statement.rs index 5d1b5f47f893..70566893d65a 100644 --- a/src/sql/src/statements/statement.rs +++ b/src/sql/src/statements/statement.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::fmt::Display; + use datafusion_sql::parser::Statement as DfStatement; use sqlparser::ast::Statement as SpStatement; use sqlparser_derive::{Visit, VisitMut}; @@ -89,6 +91,41 @@ pub enum Statement { ShowVariables(ShowVariables), } +impl Display for Statement { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Statement::Query(s) => s.inner.fmt(f), + Statement::Insert(s) => s.inner.fmt(f), + Statement::Delete(s) => s.inner.fmt(f), + Statement::CreateTable(s) => s.fmt(f), + Statement::CreateExternalTable(s) => s.fmt(f), + Statement::CreateTableLike(s) => s.fmt(f), + Statement::DropTable(s) => s.fmt(f), + Statement::DropDatabase(s) => s.fmt(f), + Statement::CreateDatabase(s) => s.fmt(f), + Statement::Alter(s) => s.fmt(f), + Statement::ShowDatabases(s) => s.fmt(f), + Statement::ShowTables(s) => s.fmt(f), + Statement::ShowColumns(s) => s.fmt(f), + Statement::ShowIndex(s) => s.fmt(f), + Statement::ShowCreateTable(s) => s.fmt(f), + Statement::DescribeTable(s) => s.fmt(f), + Statement::Explain(s) => s.fmt(f), + Statement::Copy(s) => s.fmt(f), + Statement::Tql(s) => s.fmt(f), + Statement::TruncateTable(s) => s.fmt(f), + Statement::SetVariables(s) => s.fmt(f), + Statement::ShowVariables(s) => s.fmt(f), + Statement::ShowCharset(kind) => { + write!(f, "SHOW CHARSET {kind}") + } + Statement::ShowCollation(kind) => { + write!(f, "SHOW COLLATION {kind}") + } + } + } +} + /// Comment hints from SQL. /// It'll be enabled when using `--comment` in mysql client. /// Eg: `SELECT * FROM system.number LIMIT 1; -- { ErrorCode 25 }` diff --git a/src/sql/src/statements/tql.rs b/src/sql/src/statements/tql.rs index 6bc4136068ea..07f8cb8876ec 100644 --- a/src/sql/src/statements/tql.rs +++ b/src/sql/src/statements/tql.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::fmt::Display; + use sqlparser_derive::{Visit, VisitMut}; #[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut)] @@ -21,6 +23,32 @@ pub enum Tql { Analyze(TqlAnalyze), } +impl Display for Tql { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Tql::Eval(t) => t.fmt(f), + Tql::Explain(t) => t.fmt(f), + Tql::Analyze(t) => t.fmt(f), + } + } +} + +// TODO: encapsulate shard TQL args into a struct and implement Display for it. +fn format_tql( + f: &mut std::fmt::Formatter<'_>, + start: &str, + end: &str, + step: &str, + lookback: Option<&str>, + query: &str, +) -> std::fmt::Result { + write!(f, "({start}, {end}, {step}")?; + if let Some(lookback) = lookback { + write!(f, ", {lookback}")?; + } + write!(f, ") {query}") +} + /// TQL EVAL (, , , [lookback]) #[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut)] pub struct TqlEval { @@ -31,6 +59,20 @@ pub struct TqlEval { pub query: String, } +impl Display for TqlEval { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "TQL EVAL")?; + format_tql( + f, + &self.start, + &self.end, + &self.step, + self.lookback.as_deref(), + &self.query, + ) + } +} + /// TQL EXPLAIN [VERBOSE] [, , , [lookback]] /// doesn't execute the query but tells how the query would be executed (similar to SQL EXPLAIN). #[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut)] @@ -43,6 +85,23 @@ pub struct TqlExplain { pub is_verbose: bool, } +impl Display for TqlExplain { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "TQL EXPLAIN ")?; + if self.is_verbose { + write!(f, "VERBOSE ")?; + } + format_tql( + f, + &self.start, + &self.end, + &self.step, + self.lookback.as_deref(), + &self.query, + ) + } +} + /// TQL ANALYZE [VERBOSE] (, , , [lookback]) /// executes the plan and tells the detailed per-step execution time (similar to SQL ANALYZE). #[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut)] @@ -55,6 +114,23 @@ pub struct TqlAnalyze { pub is_verbose: bool, } +impl Display for TqlAnalyze { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "TQL ANALYZE ")?; + if self.is_verbose { + write!(f, "VERBOSE ")?; + } + format_tql( + f, + &self.start, + &self.end, + &self.step, + self.lookback.as_deref(), + &self.query, + ) + } +} + /// Intermediate structure used to unify parameter mappings for various TQL operations. /// This struct serves as a common parameter container for parsing TQL queries /// and constructing corresponding TQL operations: `TqlEval`, `TqlAnalyze` or `TqlExplain`. diff --git a/src/sql/src/statements/transform/type_alias.rs b/src/sql/src/statements/transform/type_alias.rs index 353c19f68c67..464d0ca0c01a 100644 --- a/src/sql/src/statements/transform/type_alias.rs +++ b/src/sql/src/statements/transform/type_alias.rs @@ -365,7 +365,7 @@ CREATE TABLE data_types ( match &stmts[0] { Statement::CreateTable(c) => { - let expected = r#"CREATE TABLE data_types ( + let expected = r#"CREATE TABLE data_types ( s STRING, tt TEXT, mt TEXT, diff --git a/src/sql/src/statements/truncate.rs b/src/sql/src/statements/truncate.rs index aa08cde559b4..c1a063f959ce 100644 --- a/src/sql/src/statements/truncate.rs +++ b/src/sql/src/statements/truncate.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::fmt::Display; + use sqlparser::ast::ObjectName; use sqlparser_derive::{Visit, VisitMut}; @@ -31,3 +33,42 @@ impl TruncateTable { &self.table_name } } + +impl Display for TruncateTable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let table_name = self.table_name(); + write!(f, r#"TRUNCATE TABLE {table_name}"#) + } +} + +#[cfg(test)] +mod tests { + use std::assert_matches::assert_matches; + + use crate::dialect::GreptimeDbDialect; + use crate::parser::{ParseOptions, ParserContext}; + use crate::statements::statement::Statement; + + #[test] + fn test_display_for_tuncate_table() { + let sql = r"truncate table t1;"; + let stmts: Vec = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + assert_eq!(1, stmts.len()); + assert_matches!(&stmts[0], Statement::TruncateTable { .. }); + match &stmts[0] { + Statement::TruncateTable(trunc) => { + let new_sql = format!("\n{}", trunc); + assert_eq!( + r#" +TRUNCATE TABLE t1"#, + &new_sql + ); + } + _ => { + unreachable!(); + } + } + } +}