diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 6da6a90d0..5427238ec 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -26,7 +26,9 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; use crate::ast::{display_comma_separated, Expr, ObjectName, StructField, UnionField}; +use crate::dialect::Dialect; +use super::to_sql::ToSql; use super::{value::escape_single_quote_string, ColumnDef}; #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -496,6 +498,105 @@ pub enum DataType { TsQuery, } +impl ToSql for DataType { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + if dialect.requires_pascalcase_types() { + self.write_sql_pascalcase(f, dialect) + } else { + write!(f, "{}", self) + } + } +} + +impl DataType { + /// Formats the data type with PascalCase type names (for ClickHouse compatibility). + fn write_sql_pascalcase(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + match self { + // Types that need PascalCase conversion (currently uppercase in Display) + DataType::Int8(zerofill) => write_type_pascalcase(f, "Int8", zerofill), + DataType::Int64 => write!(f, "Int64"), + DataType::Float64 => write!(f, "Float64"), + DataType::String(size) => match size { + Some(s) => write!(f, "String({s})"), + None => write!(f, "String"), + }, + DataType::Bool => write!(f, "Bool"), + DataType::Boolean => write!(f, "Boolean"), + DataType::Date => write!(f, "Date"), + DataType::Datetime(precision) => match precision { + Some(p) => write!(f, "DateTime({p})"), + None => write!(f, "DateTime"), + }, + + // Container types that need recursive PascalCase formatting + DataType::Nullable(inner) => { + write!(f, "Nullable(")?; + inner.write_sql(f, dialect)?; + write!(f, ")") + } + DataType::LowCardinality(inner) => { + write!(f, "LowCardinality(")?; + inner.write_sql(f, dialect)?; + write!(f, ")") + } + DataType::Array(elem) => match elem { + ArrayElemTypeDef::None => write!(f, "Array"), + ArrayElemTypeDef::SquareBracket(t, None) => { + t.write_sql(f, dialect)?; + write!(f, "[]") + } + ArrayElemTypeDef::SquareBracket(t, Some(size)) => { + t.write_sql(f, dialect)?; + write!(f, "[{size}]") + } + ArrayElemTypeDef::AngleBracket(t) => { + write!(f, "Array<")?; + t.write_sql(f, dialect)?; + write!(f, ">") + } + ArrayElemTypeDef::Parenthesis(t) => { + write!(f, "Array(")?; + t.write_sql(f, dialect)?; + write!(f, ")") + } + }, + DataType::Map(key, value) => { + write!(f, "Map(")?; + key.write_sql(f, dialect)?; + write!(f, ", ")?; + value.write_sql(f, dialect)?; + write!(f, ")") + } + DataType::Tuple(fields) => { + write!(f, "Tuple(")?; + let mut first = true; + for field in fields { + if !first { + write!(f, ", ")?; + } + first = false; + if let Some(name) = &field.field_name { + write!(f, "{} ", name)?; + } + field.field_type.write_sql(f, dialect)?; + } + write!(f, ")") + } + + // All other types use the default Display implementation + _ => write!(f, "{}", self), + } + } +} + +/// Helper function to write a type name with optional length in PascalCase +fn write_type_pascalcase(f: &mut dyn fmt::Write, type_name: &str, len: &Option) -> fmt::Result { + match len { + Some(l) => write!(f, "{type_name}({l})"), + None => write!(f, "{type_name}"), + } +} + impl fmt::Display for DataType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 2a24741f3..54fb00004 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -48,10 +48,11 @@ use crate::ast::{ HiveFormat, HiveIOFormat, HiveRowFormat, HiveSetLocation, Ident, InitializeKind, MySQLColumnPosition, ObjectName, OnCommit, OneOrManyWithParens, OperateFunctionArg, OrderByExpr, ProjectionSelect, Query, RefreshModeKind, RowAccessPolicy, SequenceOptions, - Spanned, SqlOption, StorageSerializationPolicy, TableVersion, Tag, TriggerEvent, + Spanned, SqlOption, StorageSerializationPolicy, TableVersion, Tag, ToSql, TriggerEvent, TriggerExecBody, TriggerObject, TriggerPeriod, TriggerReferencing, Value, ValueWithSpan, WrappedCollection, }; +use crate::dialect::Dialect; use crate::display_utils::{DisplayCommaSeparated, Indent, NewLine, SpaceOrNewline}; use crate::keywords::Keyword; use crate::tokenizer::{Span, Token}; @@ -916,6 +917,99 @@ impl fmt::Display for AlterIndexOperation { } } +impl ToSql for AlterTableOperation { + fn write_sql(&self, f: &mut dyn Write, dialect: &dyn Dialect) -> fmt::Result { + match self { + AlterTableOperation::AddColumn { + column_keyword, + if_not_exists, + column_def, + column_position, + } => { + write!(f, "ADD")?; + if *column_keyword { + write!(f, " COLUMN")?; + } + if *if_not_exists { + write!(f, " IF NOT EXISTS")?; + } + write!(f, " ")?; + column_def.write_sql(f, dialect)?; + + if let Some(position) = column_position { + write!(f, " {position}")?; + } + + Ok(()) + } + AlterTableOperation::ChangeColumn { + old_name, + new_name, + data_type, + options, + column_position, + } => { + write!(f, "CHANGE COLUMN {old_name} {new_name} ")?; + data_type.write_sql(f, dialect)?; + if !options.is_empty() { + write!(f, " {}", display_separated(options, " "))?; + } + if let Some(position) = column_position { + write!(f, " {position}")?; + } + Ok(()) + } + AlterTableOperation::ModifyColumn { + col_name, + data_type, + options, + column_position, + } => { + write!(f, "MODIFY COLUMN {col_name} ")?; + data_type.write_sql(f, dialect)?; + if !options.is_empty() { + write!(f, " {}", display_separated(options, " "))?; + } + if let Some(position) = column_position { + write!(f, " {position}")?; + } + Ok(()) + } + AlterTableOperation::AlterColumn { column_name, op } => { + write!(f, "ALTER COLUMN {column_name} ")?; + op.write_sql(f, dialect) + } + // All other operations delegate to Display + _ => write!(f, "{}", self), + } + } +} + +impl ToSql for AlterColumnOperation { + fn write_sql(&self, f: &mut dyn Write, dialect: &dyn Dialect) -> fmt::Result { + match self { + AlterColumnOperation::SetDataType { + data_type, + using, + had_set, + } => { + if *had_set { + write!(f, "SET DATA TYPE ")?; + } else { + write!(f, "TYPE ")?; + } + data_type.write_sql(f, dialect)?; + if let Some(using) = using { + write!(f, " USING {using}")?; + } + Ok(()) + } + // All other operations delegate to Display + _ => write!(f, "{}", self), + } + } +} + /// An `ALTER TYPE` statement (`Statement::AlterType`) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -1347,6 +1441,25 @@ impl fmt::Display for ProcedureParam { } } +impl ToSql for ProcedureParam { + fn write_sql(&self, f: &mut dyn Write, dialect: &dyn Dialect) -> fmt::Result { + if let Some(mode) = &self.mode { + write!(f, "{mode} {} ", self.name)?; + self.data_type.write_sql(f, dialect)?; + if let Some(default) = &self.default { + write!(f, " = {}", default)?; + } + } else { + write!(f, "{} ", self.name)?; + self.data_type.write_sql(f, dialect)?; + if let Some(default) = &self.default { + write!(f, " = {}", default)?; + } + } + Ok(()) + } +} + /// SQL column definition #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -1371,6 +1484,21 @@ impl fmt::Display for ColumnDef { } } +impl ToSql for ColumnDef { + fn write_sql(&self, f: &mut dyn Write, dialect: &dyn Dialect) -> fmt::Result { + if self.data_type == DataType::Unspecified { + write!(f, "{}", self.name)?; + } else { + write!(f, "{} ", self.name)?; + self.data_type.write_sql(f, dialect)?; + } + for option in &self.options { + write!(f, " {option}")?; + } + Ok(()) + } +} + /// Column definition specified in a `CREATE VIEW` statement. /// /// Syntax @@ -1433,6 +1561,27 @@ impl fmt::Display for ViewColumnDef { } } +impl ToSql for ViewColumnDef { + fn write_sql(&self, f: &mut dyn Write, dialect: &dyn Dialect) -> fmt::Result { + write!(f, "{}", self.name)?; + if let Some(data_type) = self.data_type.as_ref() { + write!(f, " ")?; + data_type.write_sql(f, dialect)?; + } + if let Some(options) = self.options.as_ref() { + match options { + ColumnOptions::CommaSeparated(column_options) => { + write!(f, " {}", display_comma_separated(column_options.as_slice()))?; + } + ColumnOptions::SpaceSeparated(column_options) => { + write!(f, " {}", display_separated(column_options.as_slice(), " "))? + } + } + } + Ok(()) + } +} + /// An optionally-named `ColumnOption`: `[ CONSTRAINT ] `. /// /// Note that implementations are substantially more permissive than the ANSI @@ -3072,120 +3221,408 @@ impl fmt::Display for CreateTable { } } -/// PostgreSQL partition bound specification for `PARTITION OF`. -/// -/// Specifies partition bounds for a child partition table. -/// -/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtable.html) -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum ForValues { - /// `FOR VALUES IN (expr, ...)` - In(Vec), - /// `FOR VALUES FROM (expr|MINVALUE|MAXVALUE, ...) TO (expr|MINVALUE|MAXVALUE, ...)` - From { - from: Vec, - to: Vec, - }, - /// `FOR VALUES WITH (MODULUS n, REMAINDER r)` - With { modulus: u64, remainder: u64 }, - /// `DEFAULT` - Default, -} - -impl fmt::Display for ForValues { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ForValues::In(values) => { - write!(f, "FOR VALUES IN ({})", display_comma_separated(values)) - } - ForValues::From { from, to } => { - write!( - f, - "FOR VALUES FROM ({}) TO ({})", - display_comma_separated(from), - display_comma_separated(to) - ) - } - ForValues::With { modulus, remainder } => { - write!( - f, - "FOR VALUES WITH (MODULUS {modulus}, REMAINDER {remainder})" - ) - } - ForValues::Default => write!(f, "DEFAULT"), - } - } -} - -/// A value in a partition bound specification. -/// -/// Used in RANGE partition bounds where values can be expressions, -/// MINVALUE (negative infinity), or MAXVALUE (positive infinity). -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum PartitionBoundValue { - Expr(Expr), - MinValue, - MaxValue, -} - -impl fmt::Display for PartitionBoundValue { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - PartitionBoundValue::Expr(expr) => write!(f, "{expr}"), - PartitionBoundValue::MinValue => write!(f, "MINVALUE"), - PartitionBoundValue::MaxValue => write!(f, "MAXVALUE"), - } - } -} - -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -/// ```sql -/// CREATE DOMAIN name [ AS ] data_type -/// [ COLLATE collation ] -/// [ DEFAULT expression ] -/// [ domain_constraint [ ... ] ] -/// -/// where domain_constraint is: -/// -/// [ CONSTRAINT constraint_name ] -/// { NOT NULL | NULL | CHECK (expression) } -/// ``` -/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createdomain.html) -pub struct CreateDomain { - /// The name of the domain to be created. - pub name: ObjectName, - /// The data type of the domain. - pub data_type: DataType, - /// The collation of the domain. - pub collation: Option, - /// The default value of the domain. - pub default: Option, - /// The constraints of the domain. - pub constraints: Vec, -} - -impl fmt::Display for CreateDomain { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +impl ToSql for CreateTable { + fn write_sql(&self, f: &mut dyn Write, dialect: &dyn Dialect) -> fmt::Result { write!( f, - "CREATE DOMAIN {name} AS {data_type}", + "CREATE {or_replace}{external}{global}{temporary}{transient}{volatile}{dynamic}{iceberg}TABLE {if_not_exists}{name}", + or_replace = if self.or_replace { "OR REPLACE " } else { "" }, + external = if self.external { "EXTERNAL " } else { "" }, + global = self.global + .map(|global| { + if global { + "GLOBAL " + } else { + "LOCAL " + } + }) + .unwrap_or(""), + if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" }, + temporary = if self.temporary { "TEMPORARY " } else { "" }, + transient = if self.transient { "TRANSIENT " } else { "" }, + volatile = if self.volatile { "VOLATILE " } else { "" }, + iceberg = if self.iceberg { "ICEBERG " } else { "" }, + dynamic = if self.dynamic { "DYNAMIC " } else { "" }, name = self.name, - data_type = self.data_type )?; - if let Some(collation) = &self.collation { - write!(f, " COLLATE {collation}")?; + if let Some(partition_of) = &self.partition_of { + write!(f, " PARTITION OF {partition_of}")?; } - if let Some(default) = &self.default { - write!(f, " DEFAULT {default}")?; + if let Some(on_cluster) = &self.on_cluster { + write!(f, " ON CLUSTER {on_cluster}")?; } - if !self.constraints.is_empty() { - write!(f, " {}", display_separated(&self.constraints, " "))?; + if !self.columns.is_empty() || !self.constraints.is_empty() { + write!(f, " (")?; + let mut first = true; + for col in &self.columns { + if !first { + write!(f, ", ")?; + } + first = false; + col.write_sql(f, dialect)?; + } + if !self.columns.is_empty() && !self.constraints.is_empty() { + write!(f, ", ")?; + } + for (i, constraint) in self.constraints.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{constraint}")?; + } + write!(f, ")")?; + } else if self.query.is_none() + && self.like.is_none() + && self.clone.is_none() + && self.partition_of.is_none() + { + write!(f, " ()")?; + } else if let Some(CreateTableLikeKind::Parenthesized(like_in_columns_list)) = &self.like { + write!(f, " ({like_in_columns_list})")?; + } + if let Some(for_values) = &self.for_values { + write!(f, " {for_values}")?; + } + if let Some(comment) = &self.comment { + write!(f, " COMMENT '{comment}'")?; + } + if self.without_rowid { + write!(f, " WITHOUT ROWID")?; + } + if let Some(CreateTableLikeKind::Plain(like)) = &self.like { + write!(f, " {like}")?; + } + if let Some(c) = &self.clone { + write!(f, " CLONE {c}")?; + } + if let Some(version) = &self.version { + write!(f, " {version}")?; + } + match &self.hive_distribution { + HiveDistributionStyle::PARTITIONED { columns } => { + write!(f, " PARTITIONED BY ({})", display_comma_separated(columns))?; + } + HiveDistributionStyle::SKEWED { + columns, + on, + stored_as_directories, + } => { + write!( + f, + " SKEWED BY ({})) ON ({})", + display_comma_separated(columns), + display_comma_separated(on) + )?; + if *stored_as_directories { + write!(f, " STORED AS DIRECTORIES")?; + } + } + _ => (), + } + if let Some(clustered_by) = &self.clustered_by { + write!(f, " {clustered_by}")?; + } + if let Some(HiveFormat { + row_format, + serde_properties, + storage, + location, + }) = &self.hive_formats + { + match row_format { + Some(HiveRowFormat::SERDE { class }) => write!(f, " ROW FORMAT SERDE '{class}'")?, + Some(HiveRowFormat::DELIMITED { delimiters }) => { + write!(f, " ROW FORMAT DELIMITED")?; + if !delimiters.is_empty() { + write!(f, " {}", display_separated(delimiters, " "))?; + } + } + None => (), + } + match storage { + Some(HiveIOFormat::IOF { + input_format, + output_format, + }) => write!( + f, + " STORED AS INPUTFORMAT {input_format} OUTPUTFORMAT {output_format}" + )?, + Some(HiveIOFormat::FileFormat { format }) if !self.external => { + write!(f, " STORED AS {format}")? + } + _ => (), + } + if let Some(serde_properties) = serde_properties.as_ref() { + write!( + f, + " WITH SERDEPROPERTIES ({})", + display_comma_separated(serde_properties) + )?; + } + if !self.external { + if let Some(loc) = location { + write!(f, " LOCATION '{loc}'")?; + } + } + } + if self.external { + if let Some(file_format) = self.file_format { + write!(f, " STORED AS {file_format}")?; + } + if let Some(location) = &self.location { + write!(f, " LOCATION '{location}'")?; + } + } + match &self.table_options { + options @ CreateTableOptions::With(_) + | options @ CreateTableOptions::Plain(_) + | options @ CreateTableOptions::TableProperties(_) => write!(f, " {options}")?, + _ => (), + } + if let Some(primary_key) = &self.primary_key { + write!(f, " PRIMARY KEY {primary_key}")?; + } + if let Some(order_by) = &self.order_by { + write!(f, " ORDER BY {order_by}")?; + } + if let Some(inherits) = &self.inherits { + write!(f, " INHERITS ({})", display_comma_separated(inherits))?; + } + if let Some(partition_by) = self.partition_by.as_ref() { + write!(f, " PARTITION BY {partition_by}")?; + } + if let Some(cluster_by) = self.cluster_by.as_ref() { + write!(f, " CLUSTER BY {cluster_by}")?; + } + if let options @ CreateTableOptions::Options(_) = &self.table_options { + write!(f, " {options}")?; + } + if let Some(external_volume) = self.external_volume.as_ref() { + write!(f, " EXTERNAL_VOLUME='{external_volume}'")?; + } + if let Some(catalog) = self.catalog.as_ref() { + write!(f, " CATALOG='{catalog}'")?; + } + if self.iceberg { + if let Some(base_location) = self.base_location.as_ref() { + write!(f, " BASE_LOCATION='{base_location}'")?; + } + } + if let Some(catalog_sync) = self.catalog_sync.as_ref() { + write!(f, " CATALOG_SYNC='{catalog_sync}'")?; + } + if let Some(storage_serialization_policy) = self.storage_serialization_policy.as_ref() { + write!( + f, + " STORAGE_SERIALIZATION_POLICY={storage_serialization_policy}" + )?; + } + if self.copy_grants { + write!(f, " COPY GRANTS")?; + } + if let Some(is_enabled) = self.enable_schema_evolution { + write!( + f, + " ENABLE_SCHEMA_EVOLUTION={}", + if is_enabled { "TRUE" } else { "FALSE" } + )?; + } + if let Some(is_enabled) = self.change_tracking { + write!( + f, + " CHANGE_TRACKING={}", + if is_enabled { "TRUE" } else { "FALSE" } + )?; + } + if let Some(data_retention_time_in_days) = self.data_retention_time_in_days { + write!( + f, + " DATA_RETENTION_TIME_IN_DAYS={data_retention_time_in_days}", + )?; + } + if let Some(max_data_extension_time_in_days) = self.max_data_extension_time_in_days { + write!( + f, + " MAX_DATA_EXTENSION_TIME_IN_DAYS={max_data_extension_time_in_days}", + )?; + } + if let Some(default_ddl_collation) = &self.default_ddl_collation { + write!(f, " DEFAULT_DDL_COLLATION='{default_ddl_collation}'",)?; + } + if let Some(with_aggregation_policy) = &self.with_aggregation_policy { + write!(f, " WITH AGGREGATION POLICY {with_aggregation_policy}",)?; + } + if let Some(row_access_policy) = &self.with_row_access_policy { + write!(f, " {row_access_policy}",)?; + } + if let Some(tag) = &self.with_tags { + write!(f, " WITH TAG ({})", display_comma_separated(tag.as_slice()))?; + } + if let Some(target_lag) = &self.target_lag { + write!(f, " TARGET_LAG='{target_lag}'")?; + } + if let Some(warehouse) = &self.warehouse { + write!(f, " WAREHOUSE={warehouse}")?; + } + if let Some(refresh_mode) = &self.refresh_mode { + write!(f, " REFRESH_MODE={refresh_mode}")?; + } + if let Some(initialize) = &self.initialize { + write!(f, " INITIALIZE={initialize}")?; + } + if self.require_user { + write!(f, " REQUIRE USER")?; + } + if self.on_commit.is_some() { + let on_commit = match self.on_commit { + Some(OnCommit::DeleteRows) => "ON COMMIT DELETE ROWS", + Some(OnCommit::PreserveRows) => "ON COMMIT PRESERVE ROWS", + Some(OnCommit::Drop) => "ON COMMIT DROP", + None => "", + }; + write!(f, " {on_commit}")?; + } + if self.strict { + write!(f, " STRICT")?; + } + if let Some(query) = &self.query { + write!(f, " AS {query}")?; + } + Ok(()) + } +} + +/// PostgreSQL partition bound specification for `PARTITION OF`. +/// +/// Specifies partition bounds for a child partition table. +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtable.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ForValues { + /// `FOR VALUES IN (expr, ...)` + In(Vec), + /// `FOR VALUES FROM (expr|MINVALUE|MAXVALUE, ...) TO (expr|MINVALUE|MAXVALUE, ...)` + From { + from: Vec, + to: Vec, + }, + /// `FOR VALUES WITH (MODULUS n, REMAINDER r)` + With { modulus: u64, remainder: u64 }, + /// `DEFAULT` + Default, +} + +impl fmt::Display for ForValues { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ForValues::In(values) => { + write!(f, "FOR VALUES IN ({})", display_comma_separated(values)) + } + ForValues::From { from, to } => { + write!( + f, + "FOR VALUES FROM ({}) TO ({})", + display_comma_separated(from), + display_comma_separated(to) + ) + } + ForValues::With { modulus, remainder } => { + write!( + f, + "FOR VALUES WITH (MODULUS {modulus}, REMAINDER {remainder})" + ) + } + ForValues::Default => write!(f, "DEFAULT"), + } + } +} + +/// A value in a partition bound specification. +/// +/// Used in RANGE partition bounds where values can be expressions, +/// MINVALUE (negative infinity), or MAXVALUE (positive infinity). +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum PartitionBoundValue { + Expr(Expr), + MinValue, + MaxValue, +} + +impl fmt::Display for PartitionBoundValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + PartitionBoundValue::Expr(expr) => write!(f, "{expr}"), + PartitionBoundValue::MinValue => write!(f, "MINVALUE"), + PartitionBoundValue::MaxValue => write!(f, "MAXVALUE"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// ```sql +/// CREATE DOMAIN name [ AS ] data_type +/// [ COLLATE collation ] +/// [ DEFAULT expression ] +/// [ domain_constraint [ ... ] ] +/// +/// where domain_constraint is: +/// +/// [ CONSTRAINT constraint_name ] +/// { NOT NULL | NULL | CHECK (expression) } +/// ``` +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createdomain.html) +pub struct CreateDomain { + /// The name of the domain to be created. + pub name: ObjectName, + /// The data type of the domain. + pub data_type: DataType, + /// The collation of the domain. + pub collation: Option, + /// The default value of the domain. + pub default: Option, + /// The constraints of the domain. + pub constraints: Vec, +} + +impl fmt::Display for CreateDomain { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE DOMAIN {name} AS {data_type}", + name = self.name, + data_type = self.data_type + )?; + if let Some(collation) = &self.collation { + write!(f, " COLLATE {collation}")?; + } + if let Some(default) = &self.default { + write!(f, " DEFAULT {default}")?; + } + if !self.constraints.is_empty() { + write!(f, " {}", display_separated(&self.constraints, " "))?; + } + Ok(()) + } +} + +impl ToSql for CreateDomain { + fn write_sql(&self, f: &mut dyn Write, dialect: &dyn Dialect) -> fmt::Result { + write!(f, "CREATE DOMAIN {} AS ", self.name)?; + self.data_type.write_sql(f, dialect)?; + if let Some(collation) = &self.collation { + write!(f, " COLLATE {collation}")?; + } + if let Some(default) = &self.default { + write!(f, " DEFAULT {default}")?; + } + if !self.constraints.is_empty() { + write!(f, " {}", display_separated(&self.constraints, " "))?; } Ok(()) } @@ -3346,6 +3783,97 @@ impl fmt::Display for CreateFunction { } } +impl ToSql for CreateFunction { + fn write_sql(&self, f: &mut dyn Write, dialect: &dyn Dialect) -> fmt::Result { + write!( + f, + "CREATE {or_alter}{or_replace}{temp}FUNCTION {if_not_exists}{name}", + name = self.name, + temp = if self.temporary { "TEMPORARY " } else { "" }, + or_alter = if self.or_alter { "OR ALTER " } else { "" }, + or_replace = if self.or_replace { "OR REPLACE " } else { "" }, + if_not_exists = if self.if_not_exists { + "IF NOT EXISTS " + } else { + "" + }, + )?; + if let Some(args) = &self.args { + write!(f, "(")?; + let mut first = true; + for arg in args { + if !first { + write!(f, ", ")?; + } + first = false; + arg.write_sql(f, dialect)?; + } + write!(f, ")")?; + } + if let Some(return_type) = &self.return_type { + write!(f, " RETURNS ")?; + return_type.write_sql(f, dialect)?; + } + if let Some(determinism_specifier) = &self.determinism_specifier { + write!(f, " {determinism_specifier}")?; + } + if let Some(language) = &self.language { + write!(f, " LANGUAGE {language}")?; + } + if let Some(behavior) = &self.behavior { + write!(f, " {behavior}")?; + } + if let Some(called_on_null) = &self.called_on_null { + write!(f, " {called_on_null}")?; + } + if let Some(parallel) = &self.parallel { + write!(f, " {parallel}")?; + } + if let Some(security) = &self.security { + write!(f, " {security}")?; + } + for set_param in &self.set_params { + write!(f, " {set_param}")?; + } + if let Some(remote_connection) = &self.remote_connection { + write!(f, " REMOTE WITH CONNECTION {remote_connection}")?; + } + if let Some(CreateFunctionBody::AsBeforeOptions { body, link_symbol }) = &self.function_body + { + write!(f, " AS {body}")?; + if let Some(link_symbol) = link_symbol { + write!(f, ", {link_symbol}")?; + } + } + if let Some(CreateFunctionBody::Return(function_body)) = &self.function_body { + write!(f, " RETURN {function_body}")?; + } + if let Some(CreateFunctionBody::AsReturnExpr(function_body)) = &self.function_body { + write!(f, " AS RETURN {function_body}")?; + } + if let Some(CreateFunctionBody::AsReturnSelect(function_body)) = &self.function_body { + write!(f, " AS RETURN {function_body}")?; + } + if let Some(using) = &self.using { + write!(f, " {using}")?; + } + if let Some(options) = &self.options { + write!( + f, + " OPTIONS({})", + display_comma_separated(options.as_slice()) + )?; + } + if let Some(CreateFunctionBody::AsAfterOptions(function_body)) = &self.function_body { + write!(f, " AS {function_body}")?; + } + if let Some(CreateFunctionBody::AsBeginEnd(bes)) = &self.function_body { + write!(f, " AS {bes}")?; + } + Ok(()) + } +} + /// ```sql /// CREATE CONNECTOR [IF NOT EXISTS] connector_name /// [TYPE datasource_type] @@ -3993,6 +4521,72 @@ impl fmt::Display for CreateView { } } +impl ToSql for CreateView { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + write!( + f, + "CREATE {or_alter}{or_replace}", + or_alter = if self.or_alter { "OR ALTER " } else { "" }, + or_replace = if self.or_replace { "OR REPLACE " } else { "" }, + )?; + if let Some(ref params) = self.params { + write!(f, "{}", params)?; + } + write!( + f, + "{secure}{materialized}{temporary}VIEW {if_not_and_name}{to}", + if_not_and_name = if self.if_not_exists { + if self.name_before_not_exists { + format!("{} IF NOT EXISTS", self.name) + } else { + format!("IF NOT EXISTS {}", self.name) + } + } else { + format!("{}", self.name) + }, + secure = if self.secure { "SECURE " } else { "" }, + materialized = if self.materialized { + "MATERIALIZED " + } else { + "" + }, + temporary = if self.temporary { "TEMPORARY " } else { "" }, + to = self + .to + .as_ref() + .map(|to| format!(" TO {to}")) + .unwrap_or_default() + )?; + if !self.columns.is_empty() { + write!(f, " (")?; + crate::ast::write_comma_separated_tosql(f, &self.columns, dialect)?; + write!(f, ")")?; + } + if matches!(self.options, CreateTableOptions::With(_)) { + write!(f, " {}", self.options)?; + } + if let Some(ref comment) = self.comment { + write!(f, " COMMENT = '{}'", escape_single_quote_string(comment))?; + } + if !self.cluster_by.is_empty() { + write!( + f, + " CLUSTER BY ({})", + display_comma_separated(&self.cluster_by) + )?; + } + if matches!(self.options, CreateTableOptions::Options(_)) { + write!(f, " {}", self.options)?; + } + f.write_str(" AS ")?; + self.query.write_sql(f, dialect)?; + if self.with_no_schema_binding { + write!(f, " WITH NO SCHEMA BINDING")?; + } + Ok(()) + } +} + /// CREATE EXTENSION statement /// Note: this is a PostgreSQL-specific statement #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -4145,6 +4739,33 @@ impl fmt::Display for AlterTable { } } +impl ToSql for AlterTable { + fn write_sql(&self, f: &mut dyn Write, dialect: &dyn Dialect) -> fmt::Result { + match &self.table_type { + Some(AlterTableType::Iceberg) => write!(f, "ALTER ICEBERG TABLE ")?, + Some(AlterTableType::Dynamic) => write!(f, "ALTER DYNAMIC TABLE ")?, + Some(AlterTableType::External) => write!(f, "ALTER EXTERNAL TABLE ")?, + None => write!(f, "ALTER TABLE ")?, + } + + if self.if_exists { + write!(f, "IF EXISTS ")?; + } + if self.only { + write!(f, "ONLY ")?; + } + write!(f, "{} ", &self.name)?; + if let Some(cluster) = &self.on_cluster { + write!(f, "ON CLUSTER {cluster} ")?; + } + crate::ast::write_comma_separated_tosql(f, &self.operations, dialect)?; + if let Some(loc) = &self.location { + write!(f, " {loc}")? + } + Ok(()) + } +} + /// DROP FUNCTION statement #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c8d9c6be3..9c5be07d3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -47,6 +47,7 @@ use crate::{ tokenizer::{Span, Token}, }; use crate::{ + dialect::Dialect, display_utils::{Indent, NewLine}, keywords::Keyword, }; @@ -139,9 +140,12 @@ mod spans; pub use spans::Spanned; pub mod comments; +mod to_sql; mod trigger; mod value; +pub use to_sql::{write_comma_separated_tosql, ToSql}; + #[cfg(feature = "visitor")] mod visitor; @@ -525,6 +529,22 @@ impl fmt::Display for StructField { } } +impl ToSql for StructField { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + if let Some(name) = &self.field_name { + write!(f, "{name} ")?; + self.field_type.write_sql(f, dialect)?; + } else { + self.field_type.write_sql(f, dialect)?; + } + if let Some(options) = &self.options { + write!(f, " OPTIONS({})", display_separated(options, ", ")) + } else { + Ok(()) + } + } +} + /// A field definition within a union /// /// [DuckDB]: https://duckdb.org/docs/sql/data_types/union.html @@ -542,6 +562,13 @@ impl fmt::Display for UnionField { } } +impl ToSql for UnionField { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + write!(f, "{} ", self.field_name)?; + self.field_type.write_sql(f, dialect) + } +} + /// A dictionary field within a dictionary. /// /// [DuckDB]: https://duckdb.org/docs/sql/data_types/struct#creating-structs @@ -753,6 +780,15 @@ impl fmt::Display for CaseWhen { } } +impl ToSql for CaseWhen { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + f.write_str("WHEN ")?; + self.condition.write_sql(f, dialect)?; + f.write_str(" THEN ")?; + self.result.write_sql(f, dialect) + } +} + /// An SQL expression of any type. /// /// # Semantics / Type Checking @@ -1970,14 +2006,652 @@ impl fmt::Display for Expr { write!(f, "({match_expr})")?; } - Ok(()) + Ok(()) + } + Expr::OuterJoin(expr) => { + write!(f, "{expr} (+)") + } + Expr::Prior(expr) => write!(f, "PRIOR {expr}"), + Expr::Lambda(lambda) => write!(f, "{lambda}"), + Expr::MemberOf(member_of) => write!(f, "{member_of}"), + } + } +} + +impl ToSql for Expr { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + match self { + // Handle expressions that contain DataType + Expr::Cast { + kind, + expr, + data_type, + format, + } => { + match kind { + CastKind::Cast => { + write!(f, "CAST(")?; + expr.write_sql(f, dialect)?; + write!(f, " AS ")?; + data_type.write_sql(f, dialect)?; + if let Some(format) = format { + write!(f, " FORMAT {format}")?; + } + write!(f, ")") + } + CastKind::TryCast => { + write!(f, "TRY_CAST(")?; + expr.write_sql(f, dialect)?; + write!(f, " AS ")?; + data_type.write_sql(f, dialect)?; + if let Some(format) = format { + write!(f, " FORMAT {format}")?; + } + write!(f, ")") + } + CastKind::SafeCast => { + write!(f, "SAFE_CAST(")?; + expr.write_sql(f, dialect)?; + write!(f, " AS ")?; + data_type.write_sql(f, dialect)?; + if let Some(format) = format { + write!(f, " FORMAT {format}")?; + } + write!(f, ")") + } + CastKind::DoubleColon => { + expr.write_sql(f, dialect)?; + write!(f, "::")?; + data_type.write_sql(f, dialect) + } + } + } + Expr::Convert { + is_try, + expr, + target_before_value, + data_type, + charset, + styles, + } => { + write!(f, "{}CONVERT(", if *is_try { "TRY_" } else { "" })?; + if let Some(data_type) = data_type { + if let Some(charset) = charset { + expr.write_sql(f, dialect)?; + write!(f, ", ")?; + data_type.write_sql(f, dialect)?; + write!(f, " CHARACTER SET {charset}") + } else if *target_before_value { + data_type.write_sql(f, dialect)?; + write!(f, ", ")?; + expr.write_sql(f, dialect) + } else { + expr.write_sql(f, dialect)?; + write!(f, ", ")?; + data_type.write_sql(f, dialect) + } + } else if let Some(charset) = charset { + expr.write_sql(f, dialect)?; + write!(f, " USING {charset}") + } else { + expr.write_sql(f, dialect) // This should never happen + }?; + if !styles.is_empty() { + write!(f, ", ")?; + write_comma_separated_tosql(f, styles, dialect)?; + } + write!(f, ")") + } + Expr::TypedString(ts) => ts.write_sql(f, dialect), + // Struct can contain StructFields which have DataType + Expr::Struct { values, fields } => { + if !fields.is_empty() { + write!(f, "STRUCT<")?; + write_comma_separated_tosql(f, fields, dialect)?; + write!(f, ">(")?; + write_comma_separated_tosql(f, values, dialect)?; + write!(f, ")") + } else { + write!(f, "STRUCT(")?; + write_comma_separated_tosql(f, values, dialect)?; + write!(f, ")") + } + } + // Binary and unary ops recurse into expressions + Expr::BinaryOp { left, op, right } => { + left.write_sql(f, dialect)?; + write!(f, " {op} ")?; + right.write_sql(f, dialect) + } + Expr::UnaryOp { op, expr } => { + if op == &UnaryOperator::PGPostfixFactorial { + expr.write_sql(f, dialect)?; + write!(f, "{op}") + } else if matches!( + op, + UnaryOperator::Not + | UnaryOperator::Hash + | UnaryOperator::AtDashAt + | UnaryOperator::DoubleAt + | UnaryOperator::QuestionDash + | UnaryOperator::QuestionPipe + ) { + write!(f, "{op} ")?; + expr.write_sql(f, dialect) + } else { + write!(f, "{op}")?; + expr.write_sql(f, dialect) + } + } + // Nested expressions recurse + Expr::Nested(ast) => { + write!(f, "(")?; + ast.write_sql(f, dialect)?; + write!(f, ")") + } + // IS operators recurse into expression + Expr::IsTrue(ast) => { + ast.write_sql(f, dialect)?; + write!(f, " IS TRUE") + } + Expr::IsNotTrue(ast) => { + ast.write_sql(f, dialect)?; + write!(f, " IS NOT TRUE") + } + Expr::IsFalse(ast) => { + ast.write_sql(f, dialect)?; + write!(f, " IS FALSE") + } + Expr::IsNotFalse(ast) => { + ast.write_sql(f, dialect)?; + write!(f, " IS NOT FALSE") + } + Expr::IsNull(ast) => { + ast.write_sql(f, dialect)?; + write!(f, " IS NULL") + } + Expr::IsNotNull(ast) => { + ast.write_sql(f, dialect)?; + write!(f, " IS NOT NULL") + } + Expr::IsUnknown(ast) => { + ast.write_sql(f, dialect)?; + write!(f, " IS UNKNOWN") + } + Expr::IsNotUnknown(ast) => { + ast.write_sql(f, dialect)?; + write!(f, " IS NOT UNKNOWN") + } + Expr::IsDistinctFrom(a, b) => { + a.write_sql(f, dialect)?; + write!(f, " IS DISTINCT FROM ")?; + b.write_sql(f, dialect) + } + Expr::IsNotDistinctFrom(a, b) => { + a.write_sql(f, dialect)?; + write!(f, " IS NOT DISTINCT FROM ")?; + b.write_sql(f, dialect) + } + // Between recursively handles expressions + Expr::Between { + expr, + negated, + low, + high, + } => { + expr.write_sql(f, dialect)?; + write!(f, " {}BETWEEN ", if *negated { "NOT " } else { "" })?; + low.write_sql(f, dialect)?; + write!(f, " AND ")?; + high.write_sql(f, dialect) + } + // IN list recursively handles expressions + Expr::InList { + expr, + list, + negated, + } => { + expr.write_sql(f, dialect)?; + write!(f, " {}IN (", if *negated { "NOT " } else { "" })?; + write_comma_separated_tosql(f, list, dialect)?; + write!(f, ")") + } + // InSubquery - the subquery will use its own ToSql + Expr::InSubquery { + expr, + subquery, + negated, + } => { + expr.write_sql(f, dialect)?; + write!(f, " {}IN (", if *negated { "NOT " } else { "" })?; + subquery.write_sql(f, dialect)?; + write!(f, ")") + } + // InUnnest + Expr::InUnnest { + expr, + array_expr, + negated, + } => { + expr.write_sql(f, dialect)?; + write!(f, " {}IN UNNEST(", if *negated { "NOT " } else { "" })?; + array_expr.write_sql(f, dialect)?; + write!(f, ")") + } + // Like expressions recurse + Expr::Like { + negated, + expr, + pattern, + escape_char, + any, + } => { + expr.write_sql(f, dialect)?; + write!(f, " {}LIKE ", if *negated { "NOT " } else { "" })?; + if *any { + write!(f, "ANY ")?; + } + pattern.write_sql(f, dialect)?; + if let Some(ch) = escape_char { + write!(f, " ESCAPE {ch}")?; + } + Ok(()) + } + Expr::ILike { + negated, + expr, + pattern, + escape_char, + any, + } => { + expr.write_sql(f, dialect)?; + write!(f, " {}ILIKE ", if *negated { "NOT " } else { "" })?; + if *any { + write!(f, "ANY ")?; + } + pattern.write_sql(f, dialect)?; + if let Some(ch) = escape_char { + write!(f, " ESCAPE {ch}")?; + } + Ok(()) + } + Expr::SimilarTo { + negated, + expr, + pattern, + escape_char, + } => { + expr.write_sql(f, dialect)?; + write!(f, " {}SIMILAR TO ", if *negated { "NOT " } else { "" })?; + pattern.write_sql(f, dialect)?; + if let Some(ch) = escape_char { + write!(f, " ESCAPE {ch}")?; + } + Ok(()) + } + Expr::RLike { + negated, + expr, + pattern, + regexp, + } => { + expr.write_sql(f, dialect)?; + write!( + f, + " {}{} ", + if *negated { "NOT " } else { "" }, + if *regexp { "REGEXP" } else { "RLIKE" } + )?; + pattern.write_sql(f, dialect) + } + // Case expression + Expr::Case { + case_token: _, + end_token: _, + operand, + conditions, + else_result, + } => { + f.write_str("CASE")?; + if let Some(operand) = operand { + f.write_str(" ")?; + operand.write_sql(f, dialect)?; + } + for when in conditions { + f.write_str(" ")?; + when.write_sql(f, dialect)?; + } + if let Some(else_result) = else_result { + f.write_str(" ELSE ")?; + else_result.write_sql(f, dialect)?; + } + f.write_str(" END") + } + // Exists with subquery + Expr::Exists { subquery, negated } => { + write!(f, "{}EXISTS (", if *negated { "NOT " } else { "" })?; + subquery.write_sql(f, dialect)?; + write!(f, ")") + } + // Subquery + Expr::Subquery(s) => { + write!(f, "(")?; + s.write_sql(f, dialect)?; + write!(f, ")") + } + // Function calls can contain expressions - delegate to Display for now + // as Function has complex structure with many nested types + Expr::Function(fun) => write!(f, "{fun}"), + // Tuple + Expr::Tuple(exprs) => { + write!(f, "(")?; + write_comma_separated_tosql(f, exprs, dialect)?; + write!(f, ")") + } + // Array - delegate to Display + Expr::Array(arr) => write!(f, "{arr}"), + // CompoundFieldAccess - delegate to Display for access chain + Expr::CompoundFieldAccess { root, access_chain } => { + root.write_sql(f, dialect)?; + for field in access_chain { + write!(f, "{field}")?; + } + Ok(()) + } + // AnyOp and AllOp + Expr::AnyOp { + left, + compare_op, + right, + is_some, + } => { + let add_parens = !matches!(right.as_ref(), Expr::Subquery(_)); + left.write_sql(f, dialect)?; + write!( + f, + " {compare_op} {}{}", + if *is_some { "SOME" } else { "ANY" }, + if add_parens { "(" } else { "" }, + )?; + right.write_sql(f, dialect)?; + if add_parens { + write!(f, ")")?; + } + Ok(()) + } + Expr::AllOp { + left, + compare_op, + right, + } => { + let add_parens = !matches!(right.as_ref(), Expr::Subquery(_)); + left.write_sql(f, dialect)?; + write!( + f, + " {compare_op} ALL{}", + if add_parens { "(" } else { "" }, + )?; + right.write_sql(f, dialect)?; + if add_parens { + write!(f, ")")?; + } + Ok(()) + } + // Extract + Expr::Extract { + field, + syntax, + expr, + } => { + match syntax { + ExtractSyntax::From => { + write!(f, "EXTRACT({field} FROM ")?; + expr.write_sql(f, dialect)?; + write!(f, ")") + } + ExtractSyntax::Comma => { + write!(f, "EXTRACT({field}, ")?; + expr.write_sql(f, dialect)?; + write!(f, ")") + } + } + } + // Ceil and Floor + Expr::Ceil { expr, field } => { + match field { + CeilFloorKind::DateTimeField(DateTimeField::NoDateTime) => { + write!(f, "CEIL(")?; + expr.write_sql(f, dialect)?; + write!(f, ")") + } + CeilFloorKind::DateTimeField(dt_field) => { + write!(f, "CEIL(")?; + expr.write_sql(f, dialect)?; + write!(f, " TO {dt_field})") + } + CeilFloorKind::Scale(s) => { + write!(f, "CEIL(")?; + expr.write_sql(f, dialect)?; + write!(f, ", {s})") + } + } + } + Expr::Floor { expr, field } => { + match field { + CeilFloorKind::DateTimeField(DateTimeField::NoDateTime) => { + write!(f, "FLOOR(")?; + expr.write_sql(f, dialect)?; + write!(f, ")") + } + CeilFloorKind::DateTimeField(dt_field) => { + write!(f, "FLOOR(")?; + expr.write_sql(f, dialect)?; + write!(f, " TO {dt_field})") + } + CeilFloorKind::Scale(s) => { + write!(f, "FLOOR(")?; + expr.write_sql(f, dialect)?; + write!(f, ", {s})") + } + } + } + // Position + Expr::Position { expr, r#in } => { + write!(f, "POSITION(")?; + expr.write_sql(f, dialect)?; + write!(f, " IN ")?; + r#in.write_sql(f, dialect)?; + write!(f, ")") + } + // Substring + Expr::Substring { + expr, + substring_from, + substring_for, + special, + shorthand, + } => { + f.write_str("SUBSTR")?; + if !*shorthand { + f.write_str("ING")?; + } + write!(f, "(")?; + expr.write_sql(f, dialect)?; + if let Some(from_part) = substring_from { + if *special { + write!(f, ", ")?; + } else { + write!(f, " FROM ")?; + } + from_part.write_sql(f, dialect)?; + } + if let Some(for_part) = substring_for { + if *special { + write!(f, ", ")?; + } else { + write!(f, " FOR ")?; + } + for_part.write_sql(f, dialect)?; + } + write!(f, ")") + } + // Overlay + Expr::Overlay { + expr, + overlay_what, + overlay_from, + overlay_for, + } => { + write!(f, "OVERLAY(")?; + expr.write_sql(f, dialect)?; + write!(f, " PLACING ")?; + overlay_what.write_sql(f, dialect)?; + write!(f, " FROM ")?; + overlay_from.write_sql(f, dialect)?; + if let Some(for_part) = overlay_for { + write!(f, " FOR ")?; + for_part.write_sql(f, dialect)?; + } + write!(f, ")") + } + // Trim + Expr::Trim { + expr, + trim_where, + trim_what, + trim_characters, + } => { + write!(f, "TRIM(")?; + if let Some(ident) = trim_where { + write!(f, "{ident} ")?; + } + if let Some(trim_char) = trim_what { + trim_char.write_sql(f, dialect)?; + write!(f, " FROM ")?; + expr.write_sql(f, dialect)?; + } else { + expr.write_sql(f, dialect)?; + } + if let Some(characters) = trim_characters { + write!(f, ", ")?; + write_comma_separated_tosql(f, characters, dialect)?; + } + write!(f, ")") + } + // Collate + Expr::Collate { expr, collation } => { + expr.write_sql(f, dialect)?; + write!(f, " COLLATE {collation}") + } + // AtTimeZone + Expr::AtTimeZone { + timestamp, + time_zone, + } => { + timestamp.write_sql(f, dialect)?; + write!(f, " AT TIME ZONE ")?; + time_zone.write_sql(f, dialect) + } + // Named expression + Expr::Named { expr, name } => { + expr.write_sql(f, dialect)?; + write!(f, " AS {name}") + } + // OuterJoin + Expr::OuterJoin(expr) => { + expr.write_sql(f, dialect)?; + write!(f, " (+)") + } + // Prior + Expr::Prior(expr) => { + write!(f, "PRIOR ")?; + expr.write_sql(f, dialect) + } + // IsNormalized + Expr::IsNormalized { + expr, + form, + negated, + } => { + expr.write_sql(f, dialect)?; + let not_ = if *negated { "NOT " } else { "" }; + if let Some(form) = form { + write!(f, " IS {not_}{form} NORMALIZED") + } else { + write!(f, " IS {not_}NORMALIZED") + } + } + // Prefixed + Expr::Prefixed { prefix, value } => { + write!(f, "{prefix} ")?; + value.write_sql(f, dialect) + } + // GroupingSets, Cube, Rollup + Expr::GroupingSets(sets) => { + write!(f, "GROUPING SETS (")?; + let mut sep = ""; + for set in sets { + write!(f, "{sep}(")?; + sep = ", "; + write_comma_separated_tosql(f, set, dialect)?; + write!(f, ")")?; + } + write!(f, ")") + } + Expr::Cube(sets) => { + write!(f, "CUBE (")?; + let mut sep = ""; + for set in sets { + write!(f, "{sep}")?; + sep = ", "; + if set.len() == 1 { + set[0].write_sql(f, dialect)?; + } else { + write!(f, "(")?; + write_comma_separated_tosql(f, set, dialect)?; + write!(f, ")")?; + } + } + write!(f, ")") + } + Expr::Rollup(sets) => { + write!(f, "ROLLUP (")?; + let mut sep = ""; + for set in sets { + write!(f, "{sep}")?; + sep = ", "; + if set.len() == 1 { + set[0].write_sql(f, dialect)?; + } else { + write!(f, "(")?; + write_comma_separated_tosql(f, set, dialect)?; + write!(f, ")")?; + } + } + write!(f, ")") + } + // All other expression types delegate to Display + _ => write!(f, "{}", self), + } + } +} + +impl ToSql for TypedString { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + match self.uses_odbc_syntax { + false => { + self.data_type.write_sql(f, dialect)?; + write!(f, " {}", self.value) } - Expr::OuterJoin(expr) => { - write!(f, "{expr} (+)") + true => { + let prefix = match &self.data_type { + DataType::Date => "d", + DataType::Time(..) => "t", + DataType::Timestamp(..) => "ts", + _ => "?", + }; + write!(f, "{{{prefix} {}}}", self.value) } - Expr::Prior(expr) => write!(f, "PRIOR {expr}"), - Expr::Lambda(lambda) => write!(f, "{lambda}"), - Expr::MemberOf(member_of) => write!(f, "{member_of}"), } } } @@ -2005,6 +2679,19 @@ impl Display for WindowType { } } +impl ToSql for WindowType { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + match self { + WindowType::WindowSpec(spec) => { + f.write_str("(")?; + spec.write_sql(f, dialect)?; + f.write_str(")") + } + WindowType::NamedWindow(name) => write!(f, "{name}"), + } + } +} + /// A window specification (i.e. `OVER ([window_name] PARTITION BY .. ORDER BY .. etc.)`) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -2072,6 +2759,42 @@ impl fmt::Display for WindowSpec { } } +impl ToSql for WindowSpec { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + let mut is_first = true; + if let Some(window_name) = &self.window_name { + if !is_first { + write!(f, " ")?; + } + is_first = false; + write!(f, "{window_name}")?; + } + if !self.partition_by.is_empty() { + if !is_first { + write!(f, " ")?; + } + is_first = false; + write!(f, "PARTITION BY ")?; + write_comma_separated_tosql(f, &self.partition_by, dialect)?; + } + if !self.order_by.is_empty() { + if !is_first { + write!(f, " ")?; + } + is_first = false; + write!(f, "ORDER BY ")?; + write_comma_separated_tosql(f, &self.order_by, dialect)?; + } + if let Some(window_frame) = &self.window_frame { + if !is_first { + write!(f, " ")?; + } + window_frame.write_sql(f, dialect)?; + } + Ok(()) + } +} + /// Specifies the data processed by a window function, e.g. /// `RANGE UNBOUNDED PRECEDING` or `ROWS BETWEEN 5 PRECEDING AND CURRENT ROW`. /// @@ -2103,6 +2826,20 @@ impl Default for WindowFrame { } } +impl ToSql for WindowFrame { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + if let Some(end_bound) = &self.end_bound { + write!(f, "{} BETWEEN ", self.units)?; + self.start_bound.write_sql(f, dialect)?; + write!(f, " AND ")?; + end_bound.write_sql(f, dialect) + } else { + write!(f, "{} ", self.units)?; + self.start_bound.write_sql(f, dialect) + } + } +} + #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -2167,6 +2904,24 @@ impl fmt::Display for WindowFrameBound { } } +impl ToSql for WindowFrameBound { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + match self { + WindowFrameBound::CurrentRow => f.write_str("CURRENT ROW"), + WindowFrameBound::Preceding(None) => f.write_str("UNBOUNDED PRECEDING"), + WindowFrameBound::Following(None) => f.write_str("UNBOUNDED FOLLOWING"), + WindowFrameBound::Preceding(Some(n)) => { + n.write_sql(f, dialect)?; + f.write_str(" PRECEDING") + } + WindowFrameBound::Following(Some(n)) => { + n.write_sql(f, dialect)?; + f.write_str(" FOLLOWING") + } + } + } +} + #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -2802,6 +3557,70 @@ impl fmt::Display for Declare { } } +impl ToSql for Declare { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + let Declare { + names, + data_type, + assignment, + declare_type, + binary, + sensitive, + scroll, + hold, + for_query, + } = self; + write!(f, "{}", display_comma_separated(names))?; + + if let Some(true) = binary { + write!(f, " BINARY")?; + } + + if let Some(sensitive) = sensitive { + if *sensitive { + write!(f, " INSENSITIVE")?; + } else { + write!(f, " ASENSITIVE")?; + } + } + + if let Some(scroll) = scroll { + if *scroll { + write!(f, " SCROLL")?; + } else { + write!(f, " NO SCROLL")?; + } + } + + if let Some(declare_type) = declare_type { + write!(f, " {declare_type}")?; + } + + if let Some(hold) = hold { + if *hold { + write!(f, " WITH HOLD")?; + } else { + write!(f, " WITHOUT HOLD")?; + } + } + + if let Some(query) = for_query { + write!(f, " FOR ")?; + query.write_sql(f, dialect)?; + } + + if let Some(data_type) = data_type { + write!(f, " ")?; + data_type.write_sql(f, dialect)?; + } + + if let Some(expr) = assignment { + write!(f, " {expr}")?; + } + Ok(()) + } +} + /// Sql options of a `CREATE TABLE` statement. #[derive(Debug, Default, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -5821,6 +6640,55 @@ impl fmt::Display for Statement { } } +impl ToSql for Statement { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + match self { + // Route to custom ToSql implementations for types containing DataType + Statement::CreateTable(create_table) => create_table.write_sql(f, dialect), + Statement::CreateFunction(create_function) => create_function.write_sql(f, dialect), + Statement::CreateDomain(create_domain) => create_domain.write_sql(f, dialect), + Statement::CreateView(create_view) => create_view.write_sql(f, dialect), + Statement::AlterTable(alter_table) => alter_table.write_sql(f, dialect), + Statement::Query(query) => query.write_sql(f, dialect), + Statement::Declare { stmts } => { + write!(f, "DECLARE ")?; + let mut first = true; + for stmt in stmts { + if !first { + write!(f, "; ")?; + } + first = false; + stmt.write_sql(f, dialect)?; + } + Ok(()) + } + Statement::Prepare { + name, + data_types, + statement, + } => { + write!(f, "PREPARE {name} ")?; + if !data_types.is_empty() { + write!(f, "(")?; + let mut first = true; + for dt in data_types { + if !first { + write!(f, ", ")?; + } + first = false; + dt.write_sql(f, dialect)?; + } + write!(f, ") ")?; + } + write!(f, "AS ")?; + statement.write_sql(f, dialect) + } + // All other statement types delegate to Display + _ => write!(f, "{}", self), + } + } +} + /// Can use to describe options in create sequence or table column type identity /// ```sql /// [ INCREMENT [ BY ] increment ] @@ -6958,6 +7826,16 @@ impl fmt::Display for FunctionArgExpr { } } +impl ToSql for FunctionArgExpr { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + match self { + FunctionArgExpr::Expr(expr) => expr.write_sql(f, dialect), + FunctionArgExpr::QualifiedWildcard(prefix) => write!(f, "{prefix}.*"), + FunctionArgExpr::Wildcard => f.write_str("*"), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -7028,6 +7906,31 @@ impl fmt::Display for FunctionArg { } } +impl ToSql for FunctionArg { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + match self { + FunctionArg::Named { + name, + arg, + operator, + } => { + write!(f, "{name} {operator} ")?; + arg.write_sql(f, dialect) + } + FunctionArg::ExprNamed { + name, + arg, + operator, + } => { + name.write_sql(f, dialect)?; + write!(f, " {operator} ")?; + arg.write_sql(f, dialect) + } + FunctionArg::Unnamed(unnamed_arg) => unnamed_arg.write_sql(f, dialect), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -7192,6 +8095,45 @@ impl fmt::Display for Function { } } +impl ToSql for Function { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + if self.uses_odbc_syntax { + write!(f, "{{fn ")?; + } + + write!(f, "{}", self.name)?; + self.parameters.write_sql(f, dialect)?; + self.args.write_sql(f, dialect)?; + + if !self.within_group.is_empty() { + write!(f, " WITHIN GROUP (ORDER BY ")?; + write_comma_separated_tosql(f, &self.within_group, dialect)?; + write!(f, ")")?; + } + + if let Some(filter_cond) = &self.filter { + write!(f, " FILTER (WHERE ")?; + filter_cond.write_sql(f, dialect)?; + write!(f, ")")?; + } + + if let Some(null_treatment) = &self.null_treatment { + write!(f, " {null_treatment}")?; + } + + if let Some(o) = &self.over { + write!(f, " OVER ")?; + o.write_sql(f, dialect)?; + } + + if self.uses_odbc_syntax { + write!(f, "}}")?; + } + + Ok(()) + } +} + /// The arguments passed to a function call. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -7218,6 +8160,24 @@ impl fmt::Display for FunctionArguments { } } +impl ToSql for FunctionArguments { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + match self { + FunctionArguments::None => Ok(()), + FunctionArguments::Subquery(query) => { + write!(f, "(")?; + query.write_sql(f, dialect)?; + write!(f, ")") + } + FunctionArguments::List(args) => { + write!(f, "(")?; + args.write_sql(f, dialect)?; + write!(f, ")") + } + } + } +} + /// This represents everything inside the parentheses when calling a function. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -7247,6 +8207,29 @@ impl fmt::Display for FunctionArgumentList { } } +impl ToSql for FunctionArgumentList { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + if let Some(duplicate_treatment) = self.duplicate_treatment { + write!(f, "{duplicate_treatment} ")?; + } + write_comma_separated_tosql(f, &self.args, dialect)?; + if !self.clauses.is_empty() { + if !self.args.is_empty() { + write!(f, " ")?; + } + let mut first = true; + for clause in &self.clauses { + if !first { + write!(f, " ")?; + } + first = false; + clause.write_sql(f, dialect)?; + } + } + Ok(()) + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -7316,6 +8299,31 @@ impl fmt::Display for FunctionArgumentClause { } } +impl ToSql for FunctionArgumentClause { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + match self { + FunctionArgumentClause::IgnoreOrRespectNulls(null_treatment) => { + write!(f, "{null_treatment}") + } + FunctionArgumentClause::OrderBy(order_by) => { + write!(f, "ORDER BY ")?; + write_comma_separated_tosql(f, order_by, dialect) + } + FunctionArgumentClause::Limit(limit) => { + write!(f, "LIMIT ")?; + limit.write_sql(f, dialect) + } + FunctionArgumentClause::OnOverflow(on_overflow) => write!(f, "{on_overflow}"), + FunctionArgumentClause::Having(bound) => write!(f, "{bound}"), + FunctionArgumentClause::Separator(sep) => write!(f, "SEPARATOR {sep}"), + FunctionArgumentClause::JsonNullClause(null_clause) => write!(f, "{null_clause}"), + FunctionArgumentClause::JsonReturningClause(returning_clause) => { + write!(f, "{returning_clause}") + } + } + } +} + /// A method call #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -8741,6 +9749,22 @@ impl fmt::Display for OperateFunctionArg { } } +impl ToSql for OperateFunctionArg { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + if let Some(mode) = &self.mode { + write!(f, "{mode} ")?; + } + if let Some(name) = &self.name { + write!(f, "{name} ")?; + } + self.data_type.write_sql(f, dialect)?; + if let Some(default_expr) = &self.default_expr { + write!(f, " = {default_expr}")?; + } + Ok(()) + } +} + /// The mode of an argument in CREATE FUNCTION. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/ast/query.rs b/src/ast/query.rs index efec56ffd..84d616dd0 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -25,8 +25,11 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; +use core::fmt::Write; + use crate::{ ast::*, + dialect::Dialect, display_utils::{indented_list, SpaceOrNewline}, tokenizer::{Token, TokenWithSpan}, }; @@ -111,6 +114,40 @@ impl fmt::Display for Query { } } +impl ToSql for Query { + fn write_sql(&self, f: &mut dyn Write, dialect: &dyn Dialect) -> fmt::Result { + if let Some(ref with) = self.with { + write!(f, "{with} ")?; + } + self.body.write_sql(f, dialect)?; + if let Some(ref order_by) = self.order_by { + write!(f, " {order_by}")?; + } + if let Some(ref limit_clause) = self.limit_clause { + write!(f, "{limit_clause}")?; + } + if let Some(ref settings) = self.settings { + write!(f, " SETTINGS {}", display_comma_separated(settings))?; + } + if let Some(ref fetch) = self.fetch { + write!(f, " {fetch}")?; + } + if !self.locks.is_empty() { + write!(f, " {}", display_separated(&self.locks, " "))?; + } + if let Some(ref for_clause) = self.for_clause { + write!(f, " {for_clause}")?; + } + if let Some(ref format) = self.format_clause { + write!(f, " {format}")?; + } + for pipe_operator in &self.pipe_operators { + write!(f, " |> {pipe_operator}")?; + } + Ok(()) + } +} + /// Query syntax for ClickHouse ADD PROJECTION statement. /// Its syntax is similar to SELECT statement, but it is used to add a new projection to a table. /// Syntax is `SELECT [GROUP BY] [ORDER BY]` @@ -219,6 +256,46 @@ impl fmt::Display for SetExpr { } } +impl ToSql for SetExpr { + fn write_sql(&self, f: &mut dyn Write, dialect: &dyn Dialect) -> fmt::Result { + match self { + SetExpr::Select(s) => s.write_sql(f, dialect), + SetExpr::Query(q) => { + f.write_str("(")?; + q.write_sql(f, dialect)?; + f.write_str(")") + } + SetExpr::Values(v) => write!(f, "{v}"), + SetExpr::Insert(v) => v.write_sql(f, dialect), + SetExpr::Update(v) => v.write_sql(f, dialect), + SetExpr::Delete(v) => v.write_sql(f, dialect), + SetExpr::Merge(v) => v.write_sql(f, dialect), + SetExpr::Table(t) => write!(f, "{t}"), + SetExpr::SetOperation { + left, + right, + op, + set_quantifier, + } => { + left.write_sql(f, dialect)?; + write!(f, " {op}")?; + match set_quantifier { + SetQuantifier::All + | SetQuantifier::Distinct + | SetQuantifier::ByName + | SetQuantifier::AllByName + | SetQuantifier::DistinctByName => { + write!(f, " {set_quantifier}")?; + } + SetQuantifier::None => {} + } + f.write_str(" ")?; + right.write_sql(f, dialect) + } + } + } +} + #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -508,6 +585,116 @@ impl fmt::Display for Select { } } +impl ToSql for Select { + fn write_sql(&self, f: &mut dyn Write, dialect: &dyn Dialect) -> fmt::Result { + match self.flavor { + SelectFlavor::Standard => { + write!(f, "SELECT")?; + } + SelectFlavor::FromFirst => { + write!(f, "FROM {} SELECT", display_comma_separated(&self.from))?; + } + SelectFlavor::FromFirstNoSelect => { + write!(f, "FROM {}", display_comma_separated(&self.from))?; + } + } + + if let Some(value_table_mode) = self.value_table_mode { + write!(f, " {value_table_mode}")?; + } + + if let Some(ref top) = self.top { + if self.top_before_distinct { + write!(f, " {top}")?; + } + } + if let Some(ref distinct) = self.distinct { + write!(f, " {distinct}")?; + } + if let Some(ref top) = self.top { + if !self.top_before_distinct { + write!(f, " {top}")?; + } + } + + if !self.projection.is_empty() { + f.write_str(" ")?; + write_comma_separated_tosql(f, &self.projection, dialect)?; + } + + if let Some(exclude) = &self.exclude { + write!(f, " {exclude}")?; + } + + if let Some(ref into) = self.into { + write!(f, " {into}")?; + } + + if self.flavor == SelectFlavor::Standard && !self.from.is_empty() { + write!(f, " FROM {}", display_comma_separated(&self.from))?; + } + if !self.lateral_views.is_empty() { + for lv in &self.lateral_views { + write!(f, "{lv}")?; + } + } + if let Some(ref prewhere) = self.prewhere { + write!(f, " PREWHERE ")?; + prewhere.write_sql(f, dialect)?; + } + if let Some(ref selection) = self.selection { + write!(f, " WHERE ")?; + selection.write_sql(f, dialect)?; + } + match &self.group_by { + GroupByExpr::All(_) => { + write!(f, " {}", self.group_by)?; + } + GroupByExpr::Expressions(exprs, _) => { + if !exprs.is_empty() { + write!(f, " {}", self.group_by)?; + } + } + } + if !self.cluster_by.is_empty() { + write!(f, " CLUSTER BY ")?; + write_comma_separated_tosql(f, &self.cluster_by, dialect)?; + } + if !self.distribute_by.is_empty() { + write!(f, " DISTRIBUTE BY ")?; + write_comma_separated_tosql(f, &self.distribute_by, dialect)?; + } + if !self.sort_by.is_empty() { + write!(f, " SORT BY {}", display_comma_separated(&self.sort_by))?; + } + if let Some(ref having) = self.having { + write!(f, " HAVING ")?; + having.write_sql(f, dialect)?; + } + if self.window_before_qualify { + if !self.named_window.is_empty() { + write!(f, " WINDOW {}", display_comma_separated(&self.named_window))?; + } + if let Some(ref qualify) = self.qualify { + write!(f, " QUALIFY ")?; + qualify.write_sql(f, dialect)?; + } + } else { + if let Some(ref qualify) = self.qualify { + write!(f, " QUALIFY ")?; + qualify.write_sql(f, dialect)?; + } + if !self.named_window.is_empty() { + write!(f, " WINDOW {}", display_comma_separated(&self.named_window))?; + } + } + if let Some(ref connect_by) = self.connect_by { + write!(f, " {connect_by}")?; + } + Ok(()) + } +} + /// A hive LATERAL VIEW with potential column aliases #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -1010,6 +1197,29 @@ impl fmt::Display for SelectItem { } } +impl ToSql for SelectItem { + fn write_sql(&self, f: &mut dyn Write, dialect: &dyn Dialect) -> fmt::Result { + match &self { + SelectItem::UnnamedExpr(expr) => expr.write_sql(f, dialect), + SelectItem::ExprWithAlias { expr, alias } => { + expr.write_sql(f, dialect)?; + write!(f, " AS {alias}") + } + SelectItem::QualifiedWildcard(kind, additional_options) => { + // QualifiedWildcard may contain expressions for Struct + match kind { + SelectItemQualifiedWildcardKind::ObjectName(name) => write!(f, "{name}")?, + SelectItemQualifiedWildcardKind::Expr(expr) => expr.write_sql(f, dialect)?, + } + write!(f, ".* {additional_options}") + } + SelectItem::Wildcard(additional_options) => { + write!(f, "* {additional_options}") + } + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -2535,6 +2745,18 @@ impl fmt::Display for OrderByExpr { } } +impl ToSql for OrderByExpr { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + self.expr.write_sql(f, dialect)?; + write!(f, "{}", self.options)?; + if let Some(ref with_fill) = self.with_fill { + write!(f, " ")?; + with_fill.write_sql(f, dialect)?; + } + Ok(()) + } +} + /// ClickHouse `WITH FILL` modifier for `ORDER BY` clause. /// Supported by [ClickHouse syntax] /// @@ -2564,6 +2786,25 @@ impl fmt::Display for WithFill { } } +impl ToSql for WithFill { + fn write_sql(&self, f: &mut dyn fmt::Write, dialect: &dyn Dialect) -> fmt::Result { + write!(f, "WITH FILL")?; + if let Some(ref from) = self.from { + write!(f, " FROM ")?; + from.write_sql(f, dialect)?; + } + if let Some(ref to) = self.to { + write!(f, " TO ")?; + to.write_sql(f, dialect)?; + } + if let Some(ref step) = self.step { + write!(f, " STEP ")?; + step.write_sql(f, dialect)?; + } + Ok(()) + } +} + /// ClickHouse `INTERPOLATE` clause for use in `ORDER BY` clause when using `WITH FILL` modifier. /// Supported by [ClickHouse syntax] /// @@ -3472,6 +3713,20 @@ impl fmt::Display for JsonTableColumn { } } +impl ToSql for JsonTableColumn { + fn write_sql(&self, f: &mut dyn Write, dialect: &dyn Dialect) -> fmt::Result { + match self { + JsonTableColumn::Named(json_table_named_column) => { + json_table_named_column.write_sql(f, dialect) + } + JsonTableColumn::ForOrdinality(ident) => write!(f, "{ident} FOR ORDINALITY"), + JsonTableColumn::Nested(json_table_nested_column) => { + json_table_nested_column.write_sql(f, dialect) + } + } + } +} + /// A nested column in a JSON_TABLE column list /// /// See @@ -3494,6 +3749,14 @@ impl fmt::Display for JsonTableNestedColumn { } } +impl ToSql for JsonTableNestedColumn { + fn write_sql(&self, f: &mut dyn Write, dialect: &dyn Dialect) -> fmt::Result { + write!(f, "NESTED PATH {} COLUMNS (", self.path)?; + write_comma_separated_tosql(f, &self.columns, dialect)?; + write!(f, ")") + } +} + /// A single column definition in MySQL's `JSON_TABLE` table valued function. /// /// See @@ -3539,6 +3802,24 @@ impl fmt::Display for JsonTableNamedColumn { } } +impl ToSql for JsonTableNamedColumn { + fn write_sql(&self, f: &mut dyn Write, dialect: &dyn Dialect) -> fmt::Result { + write!(f, "{} ", self.name)?; + self.r#type.write_sql(f, dialect)?; + if self.exists { + write!(f, " EXISTS")?; + } + write!(f, " PATH {}", self.path)?; + if let Some(on_empty) = &self.on_empty { + write!(f, " {on_empty} ON EMPTY")?; + } + if let Some(on_error) = &self.on_error { + write!(f, " {on_error} ON ERROR")?; + } + Ok(()) + } +} + /// Stores the error handling clause of a `JSON_TABLE` table valued function: /// {NULL | DEFAULT json_string | ERROR} ON {ERROR | EMPTY } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -3596,6 +3877,20 @@ impl fmt::Display for OpenJsonTableColumn { } } +impl ToSql for OpenJsonTableColumn { + fn write_sql(&self, f: &mut dyn Write, dialect: &dyn Dialect) -> fmt::Result { + write!(f, "{} ", self.name)?; + self.r#type.write_sql(f, dialect)?; + if let Some(path) = &self.path { + write!(f, " '{}'", value::escape_single_quote_string(path))?; + } + if self.as_json { + write!(f, " AS JSON")?; + } + Ok(()) + } +} + /// BigQuery supports ValueTables which have 2 modes: /// `SELECT [ALL | DISTINCT] AS STRUCT` /// `SELECT [ALL | DISTINCT] AS VALUE` diff --git a/src/ast/to_sql.rs b/src/ast/to_sql.rs new file mode 100644 index 000000000..dce2d9b32 --- /dev/null +++ b/src/ast/to_sql.rs @@ -0,0 +1,171 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +//! Dialect-aware SQL serialization traits and utilities. +//! +//! This module provides the [`ToSql`] trait for converting AST nodes to SQL strings +//! with dialect-specific formatting. This is necessary because some SQL dialects +//! (like ClickHouse) require specific casing for type names that differs from the +//! standard uppercase convention. +//! +//! # Example +//! +//! ``` +//! use sqlparser::ast::ToSql; +//! use sqlparser::dialect::ClickHouseDialect; +//! use sqlparser::parser::Parser; +//! +//! let sql = "CREATE TABLE t (col String)"; +//! let dialect = ClickHouseDialect {}; +//! let ast = Parser::parse_sql(&dialect, sql).unwrap(); +//! +//! // Dialect-aware serialization preserves ClickHouse's PascalCase types +//! let regenerated = ast[0].to_sql(&dialect); +//! assert!(regenerated.contains("String")); +//! ``` +//! +//! # Design Rationale +//! +//! The existing `Display` trait implementation cannot be made dialect-aware because +//! `fmt::Display::fmt` has a fixed signature that doesn't accept dialect context. +//! Rather than changing `Display` (which would be a breaking change), we introduce +//! `ToSql` as a separate trait that accepts a `&dyn Dialect` parameter. +//! +//! ## Key Design Decisions +//! +//! 1. **Coexistence with Display**: `ToSql` and `Display` coexist. `Display` continues +//! to provide standard SQL formatting (uppercase types), while `ToSql` enables +//! dialect-specific formatting. +//! +//! 2. **Macro for Delegation**: Types that don't contain `DataType` fields use the +//! [`impl_to_sql_display!`] macro to delegate `ToSql::write_sql` to their `Display` +//! implementation. This avoids code duplication (DRY principle). +//! +//! 3. **Recursive Propagation**: Types containing `DataType` fields implement `write_sql` +//! explicitly, calling `write_sql` recursively on nested types to propagate dialect +//! context through the AST tree. +//! +//! # Coverage +//! +//! The `ToSql` trait is implemented for all major AST types that users are likely to +//! serialize, including: +//! +//! - `Statement`, `Query`, `Select`, `Expr` +//! - `CreateTable`, `AlterTable`, `CreateView`, `CreateFunction` +//! - `DataType`, `ColumnDef`, `ViewColumnDef` +//! - `Function`, `WindowSpec`, `OrderByExpr` +//! - And many more supporting types +//! +//! # Migration from Display +//! +//! If you currently use `format!("{}", statement)` or `statement.to_string()` and need +//! dialect-aware formatting, change to: +//! +//! ```ignore +//! use sqlparser::ast::ToSql; +//! let sql = statement.to_sql(&dialect); +//! ``` + +#[cfg(not(feature = "std"))] +use alloc::string::String; +use core::fmt::{self, Write}; + +use crate::dialect::Dialect; + +/// Trait for dialect-aware SQL serialization. +/// +/// Types implementing this trait can be converted to SQL strings while respecting +/// dialect-specific formatting rules, such as ClickHouse's requirement for +/// PascalCase type names. +/// +/// The default `to_sql` implementation calls `write_sql` with a string buffer. +/// Types should implement `write_sql` to perform the actual formatting. +pub trait ToSql { + /// Converts this AST node to a SQL string using dialect-specific formatting. + /// + /// # Example + /// + /// ``` + /// use sqlparser::ast::{DataType, ToSql}; + /// use sqlparser::dialect::{ClickHouseDialect, GenericDialect}; + /// + /// let dt = DataType::Int64; + /// + /// // ClickHouse requires PascalCase + /// assert_eq!(dt.to_sql(&ClickHouseDialect {}), "Int64"); + /// + /// // Other dialects use uppercase + /// assert_eq!(dt.to_sql(&GenericDialect {}), "INT64"); + /// ``` + fn to_sql(&self, dialect: &dyn Dialect) -> String { + let mut s = String::new(); + // write_sql should not fail when writing to a String + self.write_sql(&mut s, dialect).unwrap(); + s + } + + /// Writes this AST node as SQL to the given formatter using dialect-specific formatting. + /// + /// Implementors should use this method to perform the actual SQL generation, + /// calling `write_sql` on nested types that contain dialect-sensitive elements + /// (like `DataType`). + fn write_sql(&self, f: &mut dyn Write, dialect: &dyn Dialect) -> fmt::Result; +} + +/// Macro to implement `ToSql` by delegating to `Display`. +/// +/// Use this macro for types that don't contain `DataType` fields and can +/// safely use their existing `Display` implementation for all dialects. +/// +/// # Example +/// +/// ```ignore +/// impl_to_sql_display!(CreateDatabase, CreateSchema, CreateIndex); +/// ``` +#[macro_export] +macro_rules! impl_to_sql_display { + ($($t:ty),+ $(,)?) => { + $( + impl $crate::ast::ToSql for $t { + fn write_sql( + &self, + f: &mut dyn ::core::fmt::Write, + _dialect: &dyn $crate::dialect::Dialect, + ) -> ::core::fmt::Result { + write!(f, "{}", self) + } + } + )+ + }; +} + +/// Helper to write a comma-separated list of items using dialect-aware formatting. +pub fn write_comma_separated_tosql( + f: &mut dyn Write, + items: &[T], + dialect: &dyn Dialect, +) -> fmt::Result { + let mut first = true; + for item in items { + if !first { + write!(f, ", ")?; + } + first = false; + item.write_sql(f, dialect)?; + } + Ok(()) +} diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index bdac1f57b..2067b176f 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -100,4 +100,12 @@ impl Dialect for ClickHouseDialect { fn supports_nested_comments(&self) -> bool { true } + + /// ClickHouse requires PascalCase for type names (e.g., `String`, `Int64`, `Nullable`). + /// Using uppercase like `STRING` or `INT64` results in `UNKNOWN_TYPE` errors. + /// + /// See + fn requires_pascalcase_types(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 1a416e4df..63b7fece4 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1216,6 +1216,18 @@ pub trait Dialect: Debug + Any { fn supports_quote_delimited_string(&self) -> bool { false } + + /// Returns true if the dialect requires PascalCase for type names. + /// + /// ClickHouse requires PascalCase type names (e.g., `String`, `Int64`, `Nullable`). + /// Other dialects like BigQuery and PostgreSQL use uppercase (e.g., `STRING`, `INT64`). + /// + /// This affects how data types are formatted when using dialect-aware SQL generation. + /// + /// [ClickHouse data types](https://clickhouse.com/docs/en/sql-reference/data-types) + fn requires_pascalcase_types(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 44bfcda42..a74d08997 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -1737,3 +1737,345 @@ fn clickhouse_and_generic() -> TestedDialects { Box::new(GenericDialect {}), ]) } + +#[test] +fn test_clickhouse_data_type_to_sql_pascalcase() { + // Test that DataType::to_sql() outputs PascalCase for ClickHouse dialect + // This is critical because ClickHouse requires PascalCase type names + // (e.g., String, Int64) and will error with UNKNOWN_TYPE if given uppercase. + use sqlparser::ast::DataType; + use sqlparser::dialect::BigQueryDialect; + + let ch = ClickHouseDialect {}; + let generic = GenericDialect {}; + let bigquery = BigQueryDialect {}; + + // Test Int64 - shared between BigQuery (INT64) and ClickHouse (Int64) + let int64 = DataType::Int64; + assert_eq!(int64.to_sql(&ch), "Int64"); + assert_eq!(int64.to_sql(&generic), "INT64"); + assert_eq!(int64.to_sql(&bigquery), "INT64"); + + // Test Float64 - shared between BigQuery (FLOAT64) and ClickHouse (Float64) + let float64 = DataType::Float64; + assert_eq!(float64.to_sql(&ch), "Float64"); + assert_eq!(float64.to_sql(&generic), "FLOAT64"); + + // Test Int8 - PostgreSQL uses INT8 (8 bytes), ClickHouse uses Int8 (8 bits) + let int8 = DataType::Int8(None); + assert_eq!(int8.to_sql(&ch), "Int8"); + assert_eq!(int8.to_sql(&generic), "INT8"); + + // Test String - ClickHouse requires String, not STRING + let string = DataType::String(None); + assert_eq!(string.to_sql(&ch), "String"); + assert_eq!(string.to_sql(&generic), "STRING"); + + // Test Bool/Boolean + let bool_type = DataType::Bool; + assert_eq!(bool_type.to_sql(&ch), "Bool"); + assert_eq!(bool_type.to_sql(&generic), "BOOL"); + + let boolean_type = DataType::Boolean; + assert_eq!(boolean_type.to_sql(&ch), "Boolean"); + assert_eq!(boolean_type.to_sql(&generic), "BOOLEAN"); + + // Test Date + let date = DataType::Date; + assert_eq!(date.to_sql(&ch), "Date"); + assert_eq!(date.to_sql(&generic), "DATE"); + + // Test DateTime + let datetime = DataType::Datetime(None); + assert_eq!(datetime.to_sql(&ch), "DateTime"); + assert_eq!(datetime.to_sql(&generic), "DATETIME"); + + // Test Nullable(String) - nested type + let nullable_string = DataType::Nullable(Box::new(DataType::String(None))); + assert_eq!(nullable_string.to_sql(&ch), "Nullable(String)"); + assert_eq!(nullable_string.to_sql(&generic), "Nullable(STRING)"); + + // Test LowCardinality(String) + let lowcard_string = DataType::LowCardinality(Box::new(DataType::String(None))); + assert_eq!(lowcard_string.to_sql(&ch), "LowCardinality(String)"); + assert_eq!(lowcard_string.to_sql(&generic), "LowCardinality(STRING)"); + + // Test Array(Int64) + let array_int64 = DataType::Array(ArrayElemTypeDef::Parenthesis(Box::new(DataType::Int64))); + assert_eq!(array_int64.to_sql(&ch), "Array(Int64)"); + assert_eq!(array_int64.to_sql(&generic), "Array(INT64)"); + + // Test Map(String, Int64) + let map_type = DataType::Map( + Box::new(DataType::String(None)), + Box::new(DataType::Int64), + ); + assert_eq!(map_type.to_sql(&ch), "Map(String, Int64)"); + assert_eq!(map_type.to_sql(&generic), "Map(STRING, INT64)"); + + // Test deeply nested: Nullable(Array(String)) + let nested = DataType::Nullable(Box::new(DataType::Array(ArrayElemTypeDef::Parenthesis( + Box::new(DataType::String(None)), + )))); + assert_eq!(nested.to_sql(&ch), "Nullable(Array(String))"); + assert_eq!(nested.to_sql(&generic), "Nullable(Array(STRING))"); + + // Types that are already PascalCase in Display should remain unchanged + let uint64 = DataType::UInt64; + assert_eq!(uint64.to_sql(&ch), "UInt64"); + assert_eq!(uint64.to_sql(&generic), "UInt64"); + + let int32 = DataType::Int32; + assert_eq!(int32.to_sql(&ch), "Int32"); + assert_eq!(int32.to_sql(&generic), "Int32"); + + let float32 = DataType::Float32; + assert_eq!(float32.to_sql(&ch), "Float32"); + assert_eq!(float32.to_sql(&generic), "Float32"); + + let date32 = DataType::Date32; + assert_eq!(date32.to_sql(&ch), "Date32"); + assert_eq!(date32.to_sql(&generic), "Date32"); +} + +#[test] +fn test_clickhouse_create_table_to_sql_pascalcase() { + // Test that Statement::to_sql() produces PascalCase types for ClickHouse + // This is the main use case: round-tripping CREATE TABLE statements + use sqlparser::ast::ToSql; + use sqlparser::parser::Parser; + + let dialect = ClickHouseDialect {}; + + // Parse a CREATE TABLE with ClickHouse types + let sql = "CREATE TABLE t (id Int64, name String, created DateTime)"; + let statements = Parser::parse_sql(&dialect, sql).unwrap(); + + // Using to_sql should preserve PascalCase for ClickHouse + let regenerated = statements[0].to_sql(&dialect); + assert!( + regenerated.contains("Int64"), + "Expected Int64, got: {}", + regenerated + ); + assert!( + regenerated.contains("String"), + "Expected String, got: {}", + regenerated + ); + assert!( + regenerated.contains("DateTime"), + "Expected DateTime, got: {}", + regenerated + ); + + // Contrast with to_string() which uses uppercase + let display_output = statements[0].to_string(); + assert!( + display_output.contains("INT64"), + "Expected INT64 in display output, got: {}", + display_output + ); + assert!( + display_output.contains("STRING"), + "Expected STRING in display output, got: {}", + display_output + ); +} + +#[test] +fn test_clickhouse_create_table_nested_types_to_sql() { + // Test nested ClickHouse types in CREATE TABLE + use sqlparser::ast::ToSql; + use sqlparser::parser::Parser; + + let dialect = ClickHouseDialect {}; + + let sql = "CREATE TABLE t (col Nullable(String), arr Array(Int64), map Map(String, Int64))"; + let statements = Parser::parse_sql(&dialect, sql).unwrap(); + + let regenerated = statements[0].to_sql(&dialect); + + // Should have PascalCase for all types + assert!( + regenerated.contains("Nullable(String)"), + "Expected Nullable(String), got: {}", + regenerated + ); + assert!( + regenerated.contains("Array(Int64)"), + "Expected Array(Int64), got: {}", + regenerated + ); + assert!( + regenerated.contains("Map(String, Int64)"), + "Expected Map(String, Int64), got: {}", + regenerated + ); +} + +#[test] +fn test_clickhouse_alter_table_to_sql_pascalcase() { + // Test that ALTER TABLE with type changes preserves PascalCase + use sqlparser::ast::ToSql; + use sqlparser::parser::Parser; + + let dialect = ClickHouseDialect {}; + + // Test ADD COLUMN + let sql = "ALTER TABLE t ADD COLUMN col String"; + let statements = Parser::parse_sql(&dialect, sql).unwrap(); + let regenerated = statements[0].to_sql(&dialect); + assert!( + regenerated.contains("String"), + "Expected String in ALTER TABLE ADD COLUMN, got: {}", + regenerated + ); + + // Test MODIFY COLUMN + let sql = "ALTER TABLE t MODIFY COLUMN col Nullable(Int64)"; + let statements = Parser::parse_sql(&dialect, sql).unwrap(); + let regenerated = statements[0].to_sql(&dialect); + assert!( + regenerated.contains("Nullable(Int64)"), + "Expected Nullable(Int64), got: {}", + regenerated + ); +} + +#[test] +fn test_clickhouse_select_cast_to_sql_pascalcase() { + // Test that SELECT with CAST preserves PascalCase types + use sqlparser::ast::ToSql; + use sqlparser::parser::Parser; + + let dialect = ClickHouseDialect {}; + + // Simple CAST + let sql = "SELECT CAST(x AS String) FROM t"; + let statements = Parser::parse_sql(&dialect, sql).unwrap(); + let regenerated = statements[0].to_sql(&dialect); + assert!( + regenerated.contains("AS String"), + "Expected 'AS String' in SELECT CAST, got: {}", + regenerated + ); + + // Nested CAST + let sql = "SELECT CAST(CAST(x AS Int64) AS String) FROM t"; + let statements = Parser::parse_sql(&dialect, sql).unwrap(); + let regenerated = statements[0].to_sql(&dialect); + assert!( + regenerated.contains("AS Int64"), + "Expected 'AS Int64' in nested CAST, got: {}", + regenerated + ); + assert!( + regenerated.contains("AS String"), + "Expected 'AS String' in nested CAST, got: {}", + regenerated + ); + + // CAST in WHERE clause + let sql = "SELECT x FROM t WHERE CAST(y AS DateTime) > '2020-01-01'"; + let statements = Parser::parse_sql(&dialect, sql).unwrap(); + let regenerated = statements[0].to_sql(&dialect); + assert!( + regenerated.contains("AS DateTime"), + "Expected 'AS DateTime' in WHERE CAST, got: {}", + regenerated + ); +} + +#[test] +fn test_clickhouse_complex_expressions_to_sql() { + // Test complex expressions with types + use sqlparser::ast::ToSql; + use sqlparser::parser::Parser; + + let dialect = ClickHouseDialect {}; + + // CASE expression with CAST + let sql = "SELECT CASE WHEN x > 0 THEN CAST(x AS String) ELSE 'default' END FROM t"; + let statements = Parser::parse_sql(&dialect, sql).unwrap(); + let regenerated = statements[0].to_sql(&dialect); + assert!( + regenerated.contains("AS String"), + "Expected 'AS String' in CASE expression, got: {}", + regenerated + ); + + // Binary operation with CAST + let sql = "SELECT CAST(a AS Int64) + CAST(b AS Int64) FROM t"; + let statements = Parser::parse_sql(&dialect, sql).unwrap(); + let regenerated = statements[0].to_sql(&dialect); + // Count occurrences of "AS Int64" + let count = regenerated.matches("AS Int64").count(); + assert!( + count == 2, + "Expected 2 occurrences of 'AS Int64', found {}: {}", + count, + regenerated + ); +} + +#[test] +fn test_clickhouse_create_view_to_sql_pascalcase() { + // Test CREATE VIEW with typed columns + use sqlparser::ast::ToSql; + use sqlparser::parser::Parser; + + let dialect = ClickHouseDialect {}; + + let sql = "CREATE VIEW v AS SELECT CAST(x AS String) AS col FROM t"; + let statements = Parser::parse_sql(&dialect, sql).unwrap(); + let regenerated = statements[0].to_sql(&dialect); + assert!( + regenerated.contains("AS String"), + "Expected 'AS String' in CREATE VIEW, got: {}", + regenerated + ); +} + +/// Test that AST round-trips correctly: parse -> to_sql -> parse should produce equivalent AST. +/// This validates: +/// - Orthogonality: The same information is preserved across transformations +/// - ETC (Easy to Change): Changes to formatting don't break parsing +#[test] +fn test_clickhouse_roundtrip_ast_preservation() { + use sqlparser::ast::ToSql; + use sqlparser::parser::Parser; + + let dialect = ClickHouseDialect {}; + + // Test cases that exercise various code paths + let test_cases = [ + "SELECT CAST(x AS String) FROM t", + "SELECT CAST(CAST(x AS Int64) AS String) FROM t", + "CREATE TABLE t (id Int64, name Nullable(String))", + "ALTER TABLE t ADD COLUMN x Array(Int32)", + "SELECT CASE WHEN x > 0 THEN CAST(x AS String) ELSE 'default' END FROM t", + "SELECT x FROM t WHERE CAST(y AS DateTime) > '2020-01-01'", + // Window function with CAST + "SELECT SUM(CAST(x AS Float64)) OVER (PARTITION BY y ORDER BY z) FROM t", + ]; + + for sql in test_cases { + // First round-trip: parse -> to_sql + let ast1 = Parser::parse_sql(&dialect, sql).expect(&format!("Failed to parse: {}", sql)); + let sql2 = ast1[0].to_sql(&dialect); + + // Second round-trip: to_sql -> parse + let ast2 = Parser::parse_sql(&dialect, &sql2) + .expect(&format!("Failed to parse regenerated SQL: {}", sql2)); + + // The AST should be equivalent (modulo formatting differences) + // We verify by generating SQL again and ensuring it's identical + let sql3 = ast2[0].to_sql(&dialect); + assert_eq!( + sql2, sql3, + "Round-trip failed for: {}\nFirst: {}\nSecond: {}", + sql, sql2, sql3 + ); + } +}