From c4c93b0ae1344a5bc50c84306f8b071f5fa9065a Mon Sep 17 00:00:00 2001 From: Ilson Roberto Balliego Junior Date: Wed, 24 Apr 2024 14:42:32 +0200 Subject: [PATCH 01/14] add support for snowflake exclusive create table properties --- src/ast/dml.rs | 73 ++++++++- src/ast/helpers/stmt_create_table.rs | 99 +++++++++++- src/ast/mod.rs | 45 ++++++ src/dialect/snowflake.rs | 179 ++++++++++++++++++++- src/keywords.rs | 10 ++ src/parser/mod.rs | 6 +- tests/sqlparser_postgres.rs | 23 +++ tests/sqlparser_snowflake.rs | 230 +++++++++++++++++++++++++++ 8 files changed, 658 insertions(+), 7 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 7238785ca..2b9b7ca59 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -24,8 +24,8 @@ pub use super::ddl::{ColumnDef, TableConstraint}; use super::{ display_comma_separated, display_separated, Expr, FileFormat, FromTable, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, Ident, InsertAliases, MysqlInsertPriority, ObjectName, - OnCommit, OnInsert, OneOrManyWithParens, OrderByExpr, Query, SelectItem, SqlOption, - SqliteOnConflict, TableEngine, TableWithJoins, + OnCommit, OnInsert, OneOrManyWithParens, OrderByExpr, Query, RowAccessPolicy, SelectItem, + SqlOption, SqliteOnConflict, TableEngine, TableWithJoins, Tag, }; /// CREATE INDEX statement. @@ -102,6 +102,24 @@ pub struct CreateTable { /// if the "STRICT" table-option keyword is added to the end, after the closing ")", /// then strict typing rules apply to that table. pub strict: bool, + /// Snowflake "COPY GRANTS" clause + pub copy_grants: bool, + /// Snowflake "ENABLE_SCHEMA_EVOLUTION" clause + pub enable_schema_evolution: Option, + /// Snowflake "CHANGE_TRACKING" clause + pub change_tracking: Option, + /// Snowflake "DATA_RETENTION_TIME_IN_DAYS" clause + pub data_retention_time_in_days: Option, + /// Snowflake "MAX_DATA_EXTENSION_TIME_IN_DAYS" clause + pub max_data_extension_time_in_days: Option, + /// Snowflake "DEFAULT_DDL_COLLATION" clause + pub default_ddl_collation: Option, + /// Snowflake "WITH AGGREGATION POLICY" clause + pub with_aggregation_policy: Option, + /// Snowflake "WITH ROW ACCESS POLICY" clause + pub with_row_access_policy: Option, + /// Snowflake "WITH TAG" clause + pub with_tags: Option>, } impl Display for CreateTable { @@ -289,6 +307,57 @@ impl Display for CreateTable { display_comma_separated(options.as_slice()) )?; } + + if self.copy_grants { + write!(f, " COPY GRANTS")?; + } + + if let Some(is_enabled) = self.enable_schema_evolution { + write!( + f, + " ENABLE_SCHEMA_EVOLUTION={}", + is_enabled.to_string().to_uppercase() + )?; + } + + if let Some(is_enabled) = self.change_tracking { + write!( + f, + " CHANGE_TRACKING={}", + is_enabled.to_string().to_uppercase() + )?; + } + + 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(query) = &self.query { write!(f, " AS {query}")?; } diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index b2b3f5688..1dacc5b7d 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -10,7 +10,8 @@ use sqlparser_derive::{Visit, VisitMut}; use super::super::dml::CreateTable; use crate::ast::{ ColumnDef, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, OnCommit, - OneOrManyWithParens, Query, SqlOption, Statement, TableConstraint, TableEngine, + OneOrManyWithParens, Query, RowAccessPolicy, SqlOption, Statement, TableConstraint, + TableEngine, Tag, }; use crate::parser::ParserError; @@ -78,6 +79,15 @@ pub struct CreateTableBuilder { pub cluster_by: Option>, pub options: Option>, pub strict: bool, + pub copy_grants: bool, + pub enable_schema_evolution: Option, + pub change_tracking: Option, + pub data_retention_time_in_days: Option, + pub max_data_extension_time_in_days: Option, + pub default_ddl_collation: Option, + pub with_aggregation_policy: Option, + pub with_row_access_policy: Option, + pub with_tags: Option>, } impl CreateTableBuilder { @@ -115,6 +125,15 @@ impl CreateTableBuilder { cluster_by: None, options: None, strict: false, + copy_grants: false, + enable_schema_evolution: None, + change_tracking: None, + data_retention_time_in_days: None, + max_data_extension_time_in_days: None, + default_ddl_collation: None, + with_aggregation_policy: None, + with_row_access_policy: None, + with_tags: None, } } pub fn or_replace(mut self, or_replace: bool) -> Self { @@ -270,6 +289,57 @@ impl CreateTableBuilder { self } + pub fn copy_grants(mut self, copy_grants: bool) -> Self { + self.copy_grants = copy_grants; + self + } + + pub fn enable_schema_evolution(mut self, enable_schema_evolution: Option) -> Self { + self.enable_schema_evolution = enable_schema_evolution; + self + } + + pub fn change_tracking(mut self, change_tracking: Option) -> Self { + self.change_tracking = change_tracking; + self + } + + pub fn data_retention_time_in_days(mut self, data_retention_time_in_days: Option) -> Self { + self.data_retention_time_in_days = data_retention_time_in_days; + self + } + + pub fn max_data_extension_time_in_days( + mut self, + max_data_extension_time_in_days: Option, + ) -> Self { + self.max_data_extension_time_in_days = max_data_extension_time_in_days; + self + } + + pub fn default_ddl_collation(mut self, default_ddl_collation: Option) -> Self { + self.default_ddl_collation = default_ddl_collation; + self + } + + pub fn with_aggregation_policy(mut self, with_aggregation_policy: Option) -> Self { + self.with_aggregation_policy = with_aggregation_policy; + self + } + + pub fn with_row_access_policy( + mut self, + with_row_access_policy: Option, + ) -> Self { + self.with_row_access_policy = with_row_access_policy; + self + } + + pub fn with_tags(mut self, with_tags: Option>) -> Self { + self.with_tags = with_tags; + self + } + pub fn build(self) -> Statement { Statement::CreateTable(CreateTable { or_replace: self.or_replace, @@ -304,6 +374,15 @@ impl CreateTableBuilder { cluster_by: self.cluster_by, options: self.options, strict: self.strict, + copy_grants: self.copy_grants, + enable_schema_evolution: self.enable_schema_evolution, + change_tracking: self.change_tracking, + data_retention_time_in_days: self.data_retention_time_in_days, + max_data_extension_time_in_days: self.max_data_extension_time_in_days, + default_ddl_collation: self.default_ddl_collation, + with_aggregation_policy: self.with_aggregation_policy, + with_row_access_policy: self.with_row_access_policy, + with_tags: self.with_tags, }) } } @@ -348,6 +427,15 @@ impl TryFrom for CreateTableBuilder { cluster_by, options, strict, + copy_grants, + enable_schema_evolution, + change_tracking, + data_retention_time_in_days, + max_data_extension_time_in_days, + default_ddl_collation, + with_aggregation_policy, + with_row_access_policy, + with_tags, }) => Ok(Self { or_replace, temporary, @@ -381,6 +469,15 @@ impl TryFrom for CreateTableBuilder { cluster_by, options, strict, + copy_grants, + enable_schema_evolution, + change_tracking, + data_retention_time_in_days, + max_data_extension_time_in_days, + default_ddl_collation, + with_aggregation_policy, + with_row_access_policy, + with_tags, }), _ => Err(ParserError::ParserError(format!( "Expected create table statement, but received: {stmt}" diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1747d677e..d5935077d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6338,6 +6338,51 @@ impl Display for TableEngine { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct RowAccessPolicy { + pub policy: ObjectName, + pub on: Vec, +} + +impl RowAccessPolicy { + pub fn new(policy: ObjectName, on: Vec) -> Self { + Self { policy, on } + } +} + +impl Display for RowAccessPolicy { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "WITH ROW ACCESS POLICY {} ON {}", + self.policy, + display_comma_separated(self.on.as_slice()) + ) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Tag { + pub key: Ident, + pub value: String, +} + +impl Tag { + pub fn new(key: Ident, value: String) -> Self { + Self { key, value } + } +} + +impl Display for Tag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}='{}'", self.key, self.value) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 894b00438..67bdb9e22 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -12,11 +12,12 @@ #[cfg(not(feature = "std"))] use crate::alloc::string::ToString; +use crate::ast::helpers::stmt_create_table::CreateTableBuilder; use crate::ast::helpers::stmt_data_loading::{ DataLoadingOption, DataLoadingOptionType, DataLoadingOptions, StageLoadSelectItem, StageParamsObject, }; -use crate::ast::{Ident, ObjectName, Statement}; +use crate::ast::{Ident, ObjectName, RowAccessPolicy, Statement, Tag}; use crate::dialect::Dialect; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -91,12 +92,20 @@ impl Dialect for SnowflakeDialect { // possibly CREATE STAGE //[ OR REPLACE ] let or_replace = parser.parse_keywords(&[Keyword::OR, Keyword::REPLACE]); - //[ TEMPORARY ] - let temporary = parser.parse_keyword(Keyword::TEMPORARY); + let local = + parser.parse_keyword(Keyword::LOCAL) || !parser.parse_keyword(Keyword::GLOBAL); + let temporary = + parser.parse_keyword(Keyword::TEMP) || parser.parse_keyword(Keyword::TEMPORARY); + let volatile = parser.parse_keyword(Keyword::VOLATILE); + let transient = parser.parse_keyword(Keyword::TRANSIENT); if parser.parse_keyword(Keyword::STAGE) { // OK - this is CREATE STAGE statement return Some(parse_create_stage(or_replace, temporary, parser)); + } else if parser.parse_keyword(Keyword::TABLE) { + return Some(parse_create_table( + or_replace, local, temporary, volatile, transient, parser, + )); } else { // need to go back with the cursor let mut back = 1; @@ -120,6 +129,170 @@ impl Dialect for SnowflakeDialect { } } +pub fn parse_create_table( + or_replace: bool, + local: bool, + temporary: bool, + _volatile: bool, + transient: bool, + parser: &mut Parser, +) -> Result { + let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let table_name = parser.parse_object_name(false)?; + + let mut builder = CreateTableBuilder::new(table_name) + .or_replace(or_replace) + .if_not_exists(if_not_exists) + .temporary(temporary) + .transient(transient) + .hive_formats(Some(Default::default())); + + if !local { + builder = builder.global(Some(local)); + } + + loop { + let next_token = parser.next_token(); + match &next_token.token { + Token::Word(word) => match word.keyword { + Keyword::COPY => { + parser.expect_keyword(Keyword::GRANTS)?; + builder = builder.copy_grants(true); + } + Keyword::COMMENT => { + parser.expect_token(&Token::Eq)?; + let next_token = parser.next_token(); + let comment = match next_token.token { + Token::SingleQuotedString(str) => Some(str), + _ => parser.expected("comment", next_token)?, + }; + builder = builder.comment(comment); + } + Keyword::AS => { + let query = parser.parse_boxed_query()?; + builder = builder.query(Some(query)); + break; + } + Keyword::CLONE => { + let clone = parser.parse_object_name(false).ok(); + builder = builder.clone_clause(clone); + break; + } + Keyword::LIKE => { + let like = parser.parse_object_name(false).ok(); + builder = builder.like(like); + break; + } + Keyword::CLUSTER => { + parser.expect_keyword(Keyword::BY)?; + let cluster_by = + Some(parser.parse_comma_separated(|p| p.parse_identifier(false))?); + builder = builder.cluster_by(cluster_by) + } + Keyword::ENABLE_SCHEMA_EVOLUTION => { + parser.expect_token(&Token::Eq)?; + let enable_schema_evolution = + match parser.parse_one_of_keywords(&[Keyword::TRUE, Keyword::FALSE]) { + Some(Keyword::TRUE) => true, + Some(Keyword::FALSE) => false, + _ => { + return parser.expected("TRUE or FALSE", next_token); + } + }; + + builder = builder.enable_schema_evolution(Some(enable_schema_evolution)); + } + Keyword::CHANGE_TRACKING => { + parser.expect_token(&Token::Eq)?; + let change_tracking = + match parser.parse_one_of_keywords(&[Keyword::TRUE, Keyword::FALSE]) { + Some(Keyword::TRUE) => true, + Some(Keyword::FALSE) => false, + _ => { + return parser.expected("TRUE or FALSE", next_token); + } + }; + + builder = builder.change_tracking(Some(change_tracking)); + } + Keyword::DATA_RETENTION_TIME_IN_DAYS => { + parser.expect_token(&Token::Eq)?; + let data_retention_time_in_days = parser.parse_literal_uint()?; + builder = + builder.data_retention_time_in_days(Some(data_retention_time_in_days)); + } + Keyword::MAX_DATA_EXTENSION_TIME_IN_DAYS => { + parser.expect_token(&Token::Eq)?; + let max_data_extension_time_in_days = parser.parse_literal_uint()?; + builder = builder + .max_data_extension_time_in_days(Some(max_data_extension_time_in_days)); + } + Keyword::DEFAULT_DDL_COLLATION => { + parser.expect_token(&Token::Eq)?; + let default_ddl_collation = parser.parse_literal_string()?; + builder = builder.default_ddl_collation(Some(default_ddl_collation)); + } + // WITH is optional, we just verify that next token is one of the expected ones and + // fallback to the default match statement + Keyword::WITH => { + parser.expect_one_of_keywords(&[ + Keyword::AGGREGATION, + Keyword::TAG, + Keyword::ROW, + ])?; + parser.prev_token(); + } + Keyword::AGGREGATION => { + parser.expect_keyword(Keyword::POLICY)?; + let aggregation_policy = parser.parse_object_name(false)?; + builder = builder.with_aggregation_policy(Some(aggregation_policy)); + } + Keyword::ROW => { + parser.expect_keywords(&[Keyword::ACCESS, Keyword::POLICY])?; + let policy = parser.parse_object_name(false)?; + parser.expect_keyword(Keyword::ON)?; + let columns = parser.parse_comma_separated(|p| p.parse_identifier(false))?; + + builder = + builder.with_row_access_policy(Some(RowAccessPolicy::new(policy, columns))) + } + Keyword::TAG => { + fn parse_tag(parser: &mut Parser) -> Result { + let name = parser.parse_identifier(false)?; + parser.expect_token(&Token::Eq)?; + let value = parser.parse_literal_string()?; + + Ok(Tag::new(name, value)) + } + + let tags = parser.parse_comma_separated(parse_tag)?; + builder = builder.with_tags(Some(tags)); + } + _ => { + return parser.expected("end of statement", next_token); + } + }, + Token::LParen => { + parser.prev_token(); + let (columns, constraints) = parser.parse_columns()?; + builder = builder.columns(columns).constraints(constraints); + } + Token::EOF => { + break; + } + Token::SemiColon => { + parser.prev_token(); + break; + } + _ => { + return parser.expected("end of statement", next_token); + } + } + } + + Ok(builder.build()) +} + pub fn parse_create_stage( or_replace: bool, temporary: bool, diff --git a/src/keywords.rs b/src/keywords.rs index 1b204a8d5..e75d45e44 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -70,11 +70,13 @@ define_keywords!( ABORT, ABS, ABSOLUTE, + ACCESS, ACTION, ADD, ADMIN, AFTER, AGAINST, + AGGREGATION, ALL, ALLOCATE, ALTER, @@ -138,6 +140,7 @@ define_keywords!( CENTURY, CHAIN, CHANGE, + CHANGE_TRACKING, CHANNEL, CHAR, CHARACTER, @@ -201,6 +204,7 @@ define_keywords!( CYCLE, DATA, DATABASE, + DATA_RETENTION_TIME_IN_DAYS, DATE, DATE32, DATETIME, @@ -214,6 +218,7 @@ define_keywords!( DECIMAL, DECLARE, DEFAULT, + DEFAULT_DDL_COLLATION, DEFERRABLE, DEFERRED, DEFINE, @@ -251,6 +256,7 @@ define_keywords!( ELSE, EMPTY, ENABLE, + ENABLE_SCHEMA_EVOLUTION, ENCODING, ENCRYPTION, END, @@ -330,6 +336,7 @@ define_keywords!( GLOBAL, GRANT, GRANTED, + GRANTS, GRAPHVIZ, GROUP, GROUPING, @@ -433,6 +440,7 @@ define_keywords!( MATERIALIZED, MAX, MAXVALUE, + MAX_DATA_EXTENSION_TIME_IN_DAYS, MEASURES, MEDIUMINT, MEMBER, @@ -539,6 +547,7 @@ define_keywords!( PIVOT, PLACING, PLANS, + POLICY, PORTION, POSITION, POSITION_REGEX, @@ -690,6 +699,7 @@ define_keywords!( TABLE, TABLES, TABLESAMPLE, + TAG, TARGET, TBLPROPERTIES, TEMP, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6406bd4e5..223ea7bf9 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5320,6 +5320,9 @@ impl<'a> Parser<'a> { Default::default() }; + let copy_grants = dialect_of!(self is SnowflakeDialect) + && self.parse_keywords(&[Keyword::COPY, Keyword::GRANTS]); + // Parse optional `AS ( query )` let query = if self.parse_keyword(Keyword::AS) { Some(self.parse_boxed_query()?) @@ -5408,6 +5411,7 @@ impl<'a> Parser<'a> { .options(big_query_config.options) .primary_key(primary_key) .strict(strict) + .copy_grants(copy_grants) .build()) } @@ -7783,7 +7787,7 @@ impl<'a> Parser<'a> { /// This function can be used to reduce the stack size required in debug /// builds. Instead of `sizeof(Query)` only a pointer (`Box`) /// is used. - fn parse_boxed_query(&mut self) -> Result, ParserError> { + pub fn parse_boxed_query(&mut self) -> Result, ParserError> { self.parse_query().map(Box::new) } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 93b3c044a..5343fe5e0 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4136,3 +4136,26 @@ fn parse_at_time_zone() { expr ); } + +#[test] +fn parse_create_table_with_options() { + let sql = "CREATE TABLE t (c INT) WITH (foo = 'bar', a = 123)"; + match pg().verified_stmt(sql) { + Statement::CreateTable(CreateTable { with_options, .. }) => { + assert_eq!( + vec![ + SqlOption { + name: "foo".into(), + value: Expr::Value(Value::SingleQuotedString("bar".into())), + }, + SqlOption { + name: "a".into(), + value: Expr::Value(number("123")), + }, + ], + with_options + ); + } + _ => unreachable!(), + } +} diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index a21e9d5d6..13e8b3481 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -40,6 +40,213 @@ fn test_snowflake_create_table() { } } +#[test] +fn test_snowflake_create_or_replace_table() { + let sql = "CREATE OR REPLACE TABLE my_table (a number)"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, or_replace, .. + }) => { + assert_eq!("my_table", name.to_string()); + assert!(or_replace); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_or_replace_table_copy_grants() { + let sql = "CREATE OR REPLACE TABLE my_table (a number) COPY GRANTS"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + or_replace, + copy_grants, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert!(or_replace); + assert!(copy_grants); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_or_replace_table_copy_grants_at_end() { + let sql = "CREATE OR REPLACE TABLE my_table COPY GRANTS (a number) "; + let parsed = "CREATE OR REPLACE TABLE my_table (a number) COPY GRANTS"; + match snowflake().one_statement_parses_to(sql, parsed) { + Statement::CreateTable(CreateTable { + name, + or_replace, + copy_grants, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert!(or_replace); + assert!(copy_grants); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_or_replace_table_copy_grants_cta() { + let sql = "CREATE OR REPLACE TABLE my_table COPY GRANTS AS SELECT 1 AS a"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + or_replace, + copy_grants, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert!(or_replace); + assert!(copy_grants); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_enable_schema_evolution() { + let sql = "CREATE TABLE my_table (a number) ENABLE_SCHEMA_EVOLUTION=TRUE"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + enable_schema_evolution, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!(Some(true), enable_schema_evolution); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_change_tracking() { + let sql = "CREATE TABLE my_table (a number) CHANGE_TRACKING=TRUE"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + change_tracking, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!(Some(true), change_tracking); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_data_retention_time_in_days() { + let sql = "CREATE TABLE my_table (a number) DATA_RETENTION_TIME_IN_DAYS=5"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + data_retention_time_in_days, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!(Some(5), data_retention_time_in_days); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_max_data_extension_time_in_days() { + let sql = "CREATE TABLE my_table (a number) MAX_DATA_EXTENSION_TIME_IN_DAYS=5"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + max_data_extension_time_in_days, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!(Some(5), max_data_extension_time_in_days); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_with_aggregation_policy() { + let sql = "CREATE TABLE my_table (a number) WITH AGGREGATION POLICY policy_name"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + with_aggregation_policy, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!( + Some("policy_name".to_string()), + with_aggregation_policy.map(|name| name.to_string()) + ); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_with_row_access_policy() { + let sql = "CREATE TABLE my_table (a number, b number) WITH ROW ACCESS POLICY policy_name ON a"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + with_row_access_policy, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!( + Some("WITH ROW ACCESS POLICY policy_name ON a".to_string()), + with_row_access_policy.map(|policy| policy.to_string()) + ); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_with_tag() { + let sql = "CREATE TABLE my_table (a number) WITH TAG A='TAG A', B='TAG B'"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, with_tags, .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!( + Some(vec![ + Tag::new("A".into(), "TAG A".to_string()), + Tag::new("B".into(), "TAG B".to_string()) + ]), + with_tags + ); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_default_ddl_collation() { + let sql = "CREATE TABLE my_table (a number) DEFAULT_DDL_COLLATION='de'"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + default_ddl_collation, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!(Some("de".to_string()), default_ddl_collation); + } + _ => unreachable!(), + } +} + #[test] fn test_snowflake_create_transient_table() { let sql = "CREATE TRANSIENT TABLE CUSTOMER (id INT, name VARCHAR(255))"; @@ -54,6 +261,29 @@ fn test_snowflake_create_transient_table() { } } +#[test] +fn test_snowflake_create_table_column_comment() { + let sql = "CREATE TABLE my_table (a STRING COMMENT 'some comment')"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { name, columns, .. }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!( + vec![ColumnDef { + name: "a".into(), + data_type: DataType::String(None), + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::Comment("some comment".to_string()) + }], + collation: None + }], + columns + ) + } + _ => unreachable!(), + } +} + #[test] fn test_snowflake_single_line_tokenize() { let sql = "CREATE TABLE# this is a comment \ntable_1"; From d6f15f15d51137edb2a838468282ae31a4a856db Mon Sep 17 00:00:00 2001 From: Ilson Roberto Balliego Junior Date: Mon, 29 Apr 2024 15:02:07 +0200 Subject: [PATCH 02/14] fix parentesis parsing for TAG and ROW ACCESS POLICY --- src/ast/dml.rs | 2 +- src/ast/mod.rs | 2 +- src/dialect/snowflake.rs | 4 ++++ src/parser/mod.rs | 4 ---- tests/sqlparser_snowflake.rs | 7 ++++--- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 2b9b7ca59..2716e31ff 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -355,7 +355,7 @@ impl Display for CreateTable { } if let Some(tag) = &self.with_tags { - write!(f, " WITH TAG {}", display_comma_separated(tag.as_slice()))?; + write!(f, " WITH TAG ({})", display_comma_separated(tag.as_slice()))?; } if let Some(query) = &self.query { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d5935077d..eb3844082 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6356,7 +6356,7 @@ impl Display for RowAccessPolicy { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "WITH ROW ACCESS POLICY {} ON {}", + "WITH ROW ACCESS POLICY {} ON ({})", self.policy, display_comma_separated(self.on.as_slice()) ) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 67bdb9e22..ec642f79a 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -251,7 +251,9 @@ pub fn parse_create_table( parser.expect_keywords(&[Keyword::ACCESS, Keyword::POLICY])?; let policy = parser.parse_object_name(false)?; parser.expect_keyword(Keyword::ON)?; + parser.expect_token(&Token::LParen)?; let columns = parser.parse_comma_separated(|p| p.parse_identifier(false))?; + parser.expect_token(&Token::RParen)?; builder = builder.with_row_access_policy(Some(RowAccessPolicy::new(policy, columns))) @@ -265,7 +267,9 @@ pub fn parse_create_table( Ok(Tag::new(name, value)) } + parser.expect_token(&Token::LParen)?; let tags = parser.parse_comma_separated(parse_tag)?; + parser.expect_token(&Token::RParen)?; builder = builder.with_tags(Some(tags)); } _ => { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 223ea7bf9..ed366754d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5320,9 +5320,6 @@ impl<'a> Parser<'a> { Default::default() }; - let copy_grants = dialect_of!(self is SnowflakeDialect) - && self.parse_keywords(&[Keyword::COPY, Keyword::GRANTS]); - // Parse optional `AS ( query )` let query = if self.parse_keyword(Keyword::AS) { Some(self.parse_boxed_query()?) @@ -5411,7 +5408,6 @@ impl<'a> Parser<'a> { .options(big_query_config.options) .primary_key(primary_key) .strict(strict) - .copy_grants(copy_grants) .build()) } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 13e8b3481..677ea9c4e 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -194,7 +194,8 @@ fn test_snowflake_create_table_with_aggregation_policy() { #[test] fn test_snowflake_create_table_with_row_access_policy() { - let sql = "CREATE TABLE my_table (a number, b number) WITH ROW ACCESS POLICY policy_name ON a"; + let sql = + "CREATE TABLE my_table (a number, b number) WITH ROW ACCESS POLICY policy_name ON (a)"; match snowflake().verified_stmt(sql) { Statement::CreateTable(CreateTable { name, @@ -203,7 +204,7 @@ fn test_snowflake_create_table_with_row_access_policy() { }) => { assert_eq!("my_table", name.to_string()); assert_eq!( - Some("WITH ROW ACCESS POLICY policy_name ON a".to_string()), + Some("WITH ROW ACCESS POLICY policy_name ON (a)".to_string()), with_row_access_policy.map(|policy| policy.to_string()) ); } @@ -213,7 +214,7 @@ fn test_snowflake_create_table_with_row_access_policy() { #[test] fn test_snowflake_create_table_with_tag() { - let sql = "CREATE TABLE my_table (a number) WITH TAG A='TAG A', B='TAG B'"; + let sql = "CREATE TABLE my_table (a number) WITH TAG (A='TAG A', B='TAG B')"; match snowflake().verified_stmt(sql) { Statement::CreateTable(CreateTable { name, with_tags, .. From 9dda92db1c0fcf0ab852bf4e3cdaeca240a49374 Mon Sep 17 00:00:00 2001 From: Ilson Roberto Balliego Junior Date: Mon, 29 Apr 2024 15:11:23 +0200 Subject: [PATCH 03/14] add docs/comments --- src/ast/dml.rs | 9 +++++++++ src/dialect/snowflake.rs | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 2716e31ff..410069eb6 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -103,22 +103,31 @@ pub struct CreateTable { /// then strict typing rules apply to that table. pub strict: bool, /// Snowflake "COPY GRANTS" clause + /// pub copy_grants: bool, /// Snowflake "ENABLE_SCHEMA_EVOLUTION" clause + /// pub enable_schema_evolution: Option, /// Snowflake "CHANGE_TRACKING" clause + /// pub change_tracking: Option, /// Snowflake "DATA_RETENTION_TIME_IN_DAYS" clause + /// pub data_retention_time_in_days: Option, /// Snowflake "MAX_DATA_EXTENSION_TIME_IN_DAYS" clause + /// pub max_data_extension_time_in_days: Option, /// Snowflake "DEFAULT_DDL_COLLATION" clause + /// pub default_ddl_collation: Option, /// Snowflake "WITH AGGREGATION POLICY" clause + /// pub with_aggregation_policy: Option, /// Snowflake "WITH ROW ACCESS POLICY" clause + /// pub with_row_access_policy: Option, /// Snowflake "WITH TAG" clause + /// pub with_tags: Option>, } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index ec642f79a..fea5af942 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -129,6 +129,8 @@ impl Dialect for SnowflakeDialect { } } +/// Parse snowflake create table statement. +/// pub fn parse_create_table( or_replace: bool, local: bool, @@ -151,6 +153,12 @@ pub fn parse_create_table( builder = builder.global(Some(local)); } + // Snowflake does not enforce order of the parameters in the statement. The parser needs to + // parse the statement in a loop. + // + // "CREATE TABLE x COPY GRANTS (c INT)" and "CREATE TABLE x (c INT) COPY GRANTS" are both + // accepted by Snowflake + loop { let next_token = parser.next_token(); match &next_token.token { From 4f315624180871c4edee0d05578c9474d99d5343 Mon Sep 17 00:00:00 2001 From: Ilson Roberto Balliego Junior Date: Tue, 30 Apr 2024 14:05:12 +0200 Subject: [PATCH 04/14] add volatile support --- src/ast/dml.rs | 4 +++- src/ast/helpers/stmt_create_table.rs | 10 ++++++++++ src/dialect/snowflake.rs | 3 ++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 410069eb6..1a3900d59 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -57,6 +57,7 @@ pub struct CreateTable { pub global: Option, pub if_not_exists: bool, pub transient: bool, + pub volatile: bool, /// Table name #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] pub name: ObjectName, @@ -142,7 +143,7 @@ impl Display for CreateTable { // `CREATE TABLE t (a INT) AS SELECT a from t2` write!( f, - "CREATE {or_replace}{external}{global}{temporary}{transient}TABLE {if_not_exists}{name}", + "CREATE {or_replace}{external}{global}{temporary}{transient}{volatile}TABLE {if_not_exists}{name}", or_replace = if self.or_replace { "OR REPLACE " } else { "" }, external = if self.external { "EXTERNAL " } else { "" }, global = self.global @@ -157,6 +158,7 @@ impl Display for CreateTable { 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 { "" }, name = self.name, )?; if let Some(on_cluster) = &self.on_cluster { diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 1dacc5b7d..ca0e4a8b5 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -53,6 +53,7 @@ pub struct CreateTableBuilder { pub global: Option, pub if_not_exists: bool, pub transient: bool, + pub volatile: bool, pub name: ObjectName, pub columns: Vec, pub constraints: Vec, @@ -99,6 +100,7 @@ impl CreateTableBuilder { global: None, if_not_exists: false, transient: false, + volatile: false, name, columns: vec![], constraints: vec![], @@ -166,6 +168,11 @@ impl CreateTableBuilder { self } + pub fn volatile(mut self, volatile: bool) -> Self { + self.volatile = volatile; + self + } + pub fn columns(mut self, columns: Vec) -> Self { self.columns = columns; self @@ -348,6 +355,7 @@ impl CreateTableBuilder { global: self.global, if_not_exists: self.if_not_exists, transient: self.transient, + volatile: self.volatile, name: self.name, columns: self.columns, constraints: self.constraints, @@ -401,6 +409,7 @@ impl TryFrom for CreateTableBuilder { global, if_not_exists, transient, + volatile, name, columns, constraints, @@ -478,6 +487,7 @@ impl TryFrom for CreateTableBuilder { with_aggregation_policy, with_row_access_policy, with_tags, + volatile, }), _ => Err(ParserError::ParserError(format!( "Expected create table statement, but received: {stmt}" diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index fea5af942..c4db4b4d1 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -135,7 +135,7 @@ pub fn parse_create_table( or_replace: bool, local: bool, temporary: bool, - _volatile: bool, + volatile: bool, transient: bool, parser: &mut Parser, ) -> Result { @@ -147,6 +147,7 @@ pub fn parse_create_table( .if_not_exists(if_not_exists) .temporary(temporary) .transient(transient) + .volatile(volatile) .hive_formats(Some(Default::default())); if !local { From 3eb22d00cbb20fcea2ad07fd186dead4f6ff548c Mon Sep 17 00:00:00 2001 From: Ilson Roberto Balliego Junior Date: Tue, 30 Apr 2024 14:45:56 +0200 Subject: [PATCH 05/14] add tests for snowflake --- src/ast/dml.rs | 2 +- src/dialect/snowflake.rs | 21 ++++-- tests/sqlparser_snowflake.rs | 137 +++++++++++++++++++++++++++++++++-- 3 files changed, 144 insertions(+), 16 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 1a3900d59..224166bea 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -307,7 +307,7 @@ impl Display for CreateTable { if let Some(cluster_by) = self.cluster_by.as_ref() { write!( f, - " CLUSTER BY {}", + " CLUSTER BY ({})", display_comma_separated(cluster_by.as_slice()) )?; } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index c4db4b4d1..79f257380 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -92,8 +92,13 @@ impl Dialect for SnowflakeDialect { // possibly CREATE STAGE //[ OR REPLACE ] let or_replace = parser.parse_keywords(&[Keyword::OR, Keyword::REPLACE]); - let local = - parser.parse_keyword(Keyword::LOCAL) || !parser.parse_keyword(Keyword::GLOBAL); + let mut global = None; + if parser.parse_keyword(Keyword::LOCAL) { + global = Some(false) + } + if parser.parse_keyword(Keyword::GLOBAL) { + global = Some(true) + } let temporary = parser.parse_keyword(Keyword::TEMP) || parser.parse_keyword(Keyword::TEMPORARY); let volatile = parser.parse_keyword(Keyword::VOLATILE); @@ -104,7 +109,7 @@ impl Dialect for SnowflakeDialect { return Some(parse_create_stage(or_replace, temporary, parser)); } else if parser.parse_keyword(Keyword::TABLE) { return Some(parse_create_table( - or_replace, local, temporary, volatile, transient, parser, + or_replace, global, temporary, volatile, transient, parser, )); } else { // need to go back with the cursor @@ -133,7 +138,7 @@ impl Dialect for SnowflakeDialect { /// pub fn parse_create_table( or_replace: bool, - local: bool, + global: Option, temporary: bool, volatile: bool, transient: bool, @@ -148,12 +153,9 @@ pub fn parse_create_table( .temporary(temporary) .transient(transient) .volatile(volatile) + .global(global) .hive_formats(Some(Default::default())); - if !local { - builder = builder.global(Some(local)); - } - // Snowflake does not enforce order of the parameters in the statement. The parser needs to // parse the statement in a loop. // @@ -194,8 +196,11 @@ pub fn parse_create_table( } Keyword::CLUSTER => { parser.expect_keyword(Keyword::BY)?; + parser.expect_token(&Token::LParen)?; let cluster_by = Some(parser.parse_comma_separated(|p| p.parse_identifier(false))?); + parser.expect_token(&Token::RParen)?; + builder = builder.cluster_by(cluster_by) } Keyword::ENABLE_SCHEMA_EVOLUTION => { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 677ea9c4e..01abea597 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -175,8 +175,29 @@ fn test_snowflake_create_table_max_data_extension_time_in_days() { #[test] fn test_snowflake_create_table_with_aggregation_policy() { - let sql = "CREATE TABLE my_table (a number) WITH AGGREGATION POLICY policy_name"; - match snowflake().verified_stmt(sql) { + match snowflake() + .verified_stmt("CREATE TABLE my_table (a number) WITH AGGREGATION POLICY policy_name") + { + Statement::CreateTable(CreateTable { + name, + with_aggregation_policy, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!( + Some("policy_name".to_string()), + with_aggregation_policy.map(|name| name.to_string()) + ); + } + _ => unreachable!(), + } + + match snowflake() + .parse_sql_statements("CREATE TABLE my_table (a number) AGGREGATION POLICY policy_name") + .unwrap() + .pop() + .unwrap() + { Statement::CreateTable(CreateTable { name, with_aggregation_policy, @@ -194,9 +215,31 @@ fn test_snowflake_create_table_with_aggregation_policy() { #[test] fn test_snowflake_create_table_with_row_access_policy() { - let sql = - "CREATE TABLE my_table (a number, b number) WITH ROW ACCESS POLICY policy_name ON (a)"; - match snowflake().verified_stmt(sql) { + match snowflake().verified_stmt( + "CREATE TABLE my_table (a number, b number) WITH ROW ACCESS POLICY policy_name ON (a)", + ) { + Statement::CreateTable(CreateTable { + name, + with_row_access_policy, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!( + Some("WITH ROW ACCESS POLICY policy_name ON (a)".to_string()), + with_row_access_policy.map(|policy| policy.to_string()) + ); + } + _ => unreachable!(), + } + + match snowflake() + .parse_sql_statements( + "CREATE TABLE my_table (a number, b number) ROW ACCESS POLICY policy_name ON (a)", + ) + .unwrap() + .pop() + .unwrap() + { Statement::CreateTable(CreateTable { name, with_row_access_policy, @@ -214,8 +257,30 @@ fn test_snowflake_create_table_with_row_access_policy() { #[test] fn test_snowflake_create_table_with_tag() { - let sql = "CREATE TABLE my_table (a number) WITH TAG (A='TAG A', B='TAG B')"; - match snowflake().verified_stmt(sql) { + match snowflake() + .verified_stmt("CREATE TABLE my_table (a number) WITH TAG (A='TAG A', B='TAG B')") + { + Statement::CreateTable(CreateTable { + name, with_tags, .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!( + Some(vec![ + Tag::new("A".into(), "TAG A".to_string()), + Tag::new("B".into(), "TAG B".to_string()) + ]), + with_tags + ); + } + _ => unreachable!(), + } + + match snowflake() + .parse_sql_statements("CREATE TABLE my_table (a number) TAG (A='TAG A', B='TAG B')") + .unwrap() + .pop() + .unwrap() + { Statement::CreateTable(CreateTable { name, with_tags, .. }) => { @@ -285,6 +350,64 @@ fn test_snowflake_create_table_column_comment() { } } +#[test] +fn test_snowflake_create_local_table() { + match snowflake().verified_stmt("CREATE TABLE my_table (a INT)") { + Statement::CreateTable(CreateTable { name, global, .. }) => { + assert_eq!("my_table", name.to_string()); + assert!(global.is_none()) + } + _ => unreachable!(), + } + + match snowflake().verified_stmt("CREATE LOCAL TABLE my_table (a INT)") { + Statement::CreateTable(CreateTable { name, global, .. }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!(Some(false), global) + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_global_table() { + match snowflake().verified_stmt("CREATE GLOBAL TABLE my_table (a INT)") { + Statement::CreateTable(CreateTable { name, global, .. }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!(Some(true), global) + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_if_not_exists() { + match snowflake().verified_stmt("CREATE TABLE IF NOT EXISTS my_table (a INT)") { + Statement::CreateTable(CreateTable { + name, + if_not_exists, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert!(if_not_exists) + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_cluster_by() { + match snowflake().verified_stmt("CREATE TABLE my_table (a INT) CLUSTER BY (a, b)") { + Statement::CreateTable(CreateTable { + name, cluster_by, .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!(Some(vec![Ident::new("a"), Ident::new("b"),]), cluster_by) + } + _ => unreachable!(), + } +} + #[test] fn test_snowflake_single_line_tokenize() { let sql = "CREATE TABLE# this is a comment \ntable_1"; From 8f4ca601daa572743ab54e075f40c0b742baa64e Mon Sep 17 00:00:00 2001 From: Ilson Roberto Balliego Junior Date: Thu, 2 May 2024 10:38:05 +0200 Subject: [PATCH 06/14] fix comment and cluster by display --- src/ast/dml.rs | 28 +++++++++++++++++----------- src/ast/helpers/stmt_create_table.rs | 16 ++++++++-------- src/ast/mod.rs | 25 +++++++++++++++++++++++++ src/dialect/snowflake.rs | 11 +++++++---- src/parser/mod.rs | 6 ++++-- tests/sqlparser_bigquery.rs | 5 ++++- tests/sqlparser_snowflake.rs | 19 ++++++++++++++++++- 7 files changed, 83 insertions(+), 27 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 224166bea..13e98d27c 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -22,10 +22,11 @@ use sqlparser_derive::{Visit, VisitMut}; pub use super::ddl::{ColumnDef, TableConstraint}; use super::{ - display_comma_separated, display_separated, Expr, FileFormat, FromTable, HiveDistributionStyle, - HiveFormat, HiveIOFormat, HiveRowFormat, Ident, InsertAliases, MysqlInsertPriority, ObjectName, - OnCommit, OnInsert, OneOrManyWithParens, OrderByExpr, Query, RowAccessPolicy, SelectItem, - SqlOption, SqliteOnConflict, TableEngine, TableWithJoins, Tag, + display_comma_separated, display_separated, CommentDef, Expr, FileFormat, FromTable, + HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, Ident, InsertAliases, + MysqlInsertPriority, ObjectName, OnCommit, OnInsert, OneOrManyWithParens, OrderByExpr, Query, + RowAccessPolicy, SelectItem, SqlOption, SqliteOnConflict, TableEngine, TableWithJoins, Tag, + WrappedCollection, }; /// CREATE INDEX statement. @@ -75,7 +76,7 @@ pub struct CreateTable { pub like: Option, pub clone: Option, pub engine: Option, - pub comment: Option, + pub comment: Option, pub auto_increment_offset: Option, pub default_charset: Option, pub collation: Option, @@ -95,7 +96,7 @@ pub struct CreateTable { pub partition_by: Option>, /// BigQuery: Table clustering column list. /// - pub cluster_by: Option>, + pub cluster_by: Option>>, /// BigQuery: Table options list. /// pub options: Option>, @@ -305,12 +306,17 @@ impl Display for CreateTable { write!(f, " PARTITION BY {partition_by}")?; } if let Some(cluster_by) = self.cluster_by.as_ref() { - write!( - f, - " CLUSTER BY ({})", - display_comma_separated(cluster_by.as_slice()) - )?; + write!(f, " CLUSTER BY ")?; + match cluster_by { + WrappedCollection::NoWrapping(cluster_by) => { + write!(f, "{}", display_comma_separated(cluster_by.as_slice()))?; + } + WrappedCollection::Parentheses(cluster_by) => { + write!(f, "({})", display_comma_separated(cluster_by.as_slice()))?; + } + } } + if let Some(options) = self.options.as_ref() { write!( f, diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index ca0e4a8b5..d862a36ae 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -9,9 +9,9 @@ use sqlparser_derive::{Visit, VisitMut}; use super::super::dml::CreateTable; use crate::ast::{ - ColumnDef, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, OnCommit, - OneOrManyWithParens, Query, RowAccessPolicy, SqlOption, Statement, TableConstraint, - TableEngine, Tag, + ColumnDef, CommentDef, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, + OnCommit, OneOrManyWithParens, Query, RowAccessPolicy, SqlOption, Statement, TableConstraint, + TableEngine, Tag, WrappedCollection, }; use crate::parser::ParserError; @@ -68,7 +68,7 @@ pub struct CreateTableBuilder { pub like: Option, pub clone: Option, pub engine: Option, - pub comment: Option, + pub comment: Option, pub auto_increment_offset: Option, pub default_charset: Option, pub collation: Option, @@ -77,7 +77,7 @@ pub struct CreateTableBuilder { pub primary_key: Option>, pub order_by: Option>, pub partition_by: Option>, - pub cluster_by: Option>, + pub cluster_by: Option>>, pub options: Option>, pub strict: bool, pub copy_grants: bool, @@ -236,7 +236,7 @@ impl CreateTableBuilder { self } - pub fn comment(mut self, comment: Option) -> Self { + pub fn comment(mut self, comment: Option) -> Self { self.comment = comment; self } @@ -281,7 +281,7 @@ impl CreateTableBuilder { self } - pub fn cluster_by(mut self, cluster_by: Option>) -> Self { + pub fn cluster_by(mut self, cluster_by: Option>>) -> Self { self.cluster_by = cluster_by; self } @@ -500,7 +500,7 @@ impl TryFrom for CreateTableBuilder { #[derive(Default)] pub(crate) struct BigQueryTableConfiguration { pub partition_by: Option>, - pub cluster_by: Option>, + pub cluster_by: Option>>, pub options: Option>, } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index eb3844082..6442b21f6 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6383,6 +6383,31 @@ impl Display for Tag { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CommentDef { + WithEq(String), + WithoutEq(String), +} + +impl Display for CommentDef { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CommentDef::WithEq(comment) | CommentDef::WithoutEq(comment) => write!(f, "{comment}"), + } + } +} + +/// Helper to indicate if a collection should be wrapped by a symbol when displaying +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum WrappedCollection { + NoWrapping(T), + Parentheses(T), +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 79f257380..d52cc6dc3 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -17,7 +17,9 @@ use crate::ast::helpers::stmt_data_loading::{ DataLoadingOption, DataLoadingOptionType, DataLoadingOptions, StageLoadSelectItem, StageParamsObject, }; -use crate::ast::{Ident, ObjectName, RowAccessPolicy, Statement, Tag}; +use crate::ast::{ + CommentDef, Ident, ObjectName, RowAccessPolicy, Statement, Tag, WrappedCollection, +}; use crate::dialect::Dialect; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -174,7 +176,7 @@ pub fn parse_create_table( parser.expect_token(&Token::Eq)?; let next_token = parser.next_token(); let comment = match next_token.token { - Token::SingleQuotedString(str) => Some(str), + Token::SingleQuotedString(str) => Some(CommentDef::WithEq(str)), _ => parser.expected("comment", next_token)?, }; builder = builder.comment(comment); @@ -197,8 +199,9 @@ pub fn parse_create_table( Keyword::CLUSTER => { parser.expect_keyword(Keyword::BY)?; parser.expect_token(&Token::LParen)?; - let cluster_by = - Some(parser.parse_comma_separated(|p| p.parse_identifier(false))?); + let cluster_by = Some(WrappedCollection::Parentheses( + parser.parse_comma_separated(|p| p.parse_identifier(false))?, + )); parser.expect_token(&Token::RParen)?; builder = builder.cluster_by(cluster_by) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ed366754d..c591b8116 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5372,7 +5372,7 @@ impl<'a> Parser<'a> { let _ = self.consume_token(&Token::Eq); let next_token = self.next_token(); match next_token.token { - Token::SingleQuotedString(str) => Some(str), + Token::SingleQuotedString(str) => Some(CommentDef::WithoutEq(str)), _ => self.expected("comment", next_token)?, } } else { @@ -5423,7 +5423,9 @@ impl<'a> Parser<'a> { let mut cluster_by = None; if self.parse_keywords(&[Keyword::CLUSTER, Keyword::BY]) { - cluster_by = Some(self.parse_comma_separated(|p| p.parse_identifier(false))?); + cluster_by = Some(WrappedCollection::NoWrapping( + self.parse_comma_separated(|p| p.parse_identifier(false))?, + )); }; let mut options = None; diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 3b6d6bfcb..171439d19 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -442,7 +442,10 @@ fn parse_create_table_with_options() { assert_eq!( ( Some(Box::new(Expr::Identifier(Ident::new("_PARTITIONDATE")))), - Some(vec![Ident::new("userid"), Ident::new("age"),]), + Some(WrappedCollection::NoWrapping(vec![ + Ident::new("userid"), + Ident::new("age"), + ])), Some(vec![ SqlOption { name: Ident::new("partition_expiration_days"), diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 01abea597..2e6a3b91b 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -402,7 +402,24 @@ fn test_snowflake_create_table_cluster_by() { name, cluster_by, .. }) => { assert_eq!("my_table", name.to_string()); - assert_eq!(Some(vec![Ident::new("a"), Ident::new("b"),]), cluster_by) + assert_eq!( + Some(WrappedCollection::Parentheses(vec![ + Ident::new("a"), + Ident::new("b"), + ])), + cluster_by + ) + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_comment() { + match snowflake().verified_stmt("CREATE TABLE my_table (a INT) COMMENT = 'some comment'") { + Statement::CreateTable(CreateTable { name, comment, .. }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!("some comment", comment.unwrap().to_string()); } _ => unreachable!(), } From 233245b914a2b9aca0c62cb846c89218efd69510 Mon Sep 17 00:00:00 2001 From: Ilson Roberto Balliego Junior Date: Thu, 2 May 2024 11:23:12 +0200 Subject: [PATCH 07/14] prevent incomplete statements --- src/dialect/snowflake.rs | 12 ++++++++++++ tests/sqlparser_snowflake.rs | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index d52cc6dc3..d9ffa0ba6 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -299,9 +299,21 @@ pub fn parse_create_table( builder = builder.columns(columns).constraints(constraints); } Token::EOF => { + if builder.columns.is_empty() { + return Err(ParserError::ParserError( + "unexpected end of input".to_string(), + )); + } + break; } Token::SemiColon => { + if builder.columns.is_empty() { + return Err(ParserError::ParserError( + "unexpected end of input".to_string(), + )); + } + parser.prev_token(); break; } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 2e6a3b91b..6bc8c930f 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -425,6 +425,23 @@ fn test_snowflake_create_table_comment() { } } +#[test] +fn test_snowflake_create_table_incomplete_statement() { + assert_eq!( + snowflake().parse_sql_statements("CREATE TABLE my_table"), + Err(ParserError::ParserError( + "unexpected end of input".to_string() + )) + ); + + assert_eq!( + snowflake().parse_sql_statements("CREATE TABLE my_table; (c int)"), + Err(ParserError::ParserError( + "unexpected end of input".to_string() + )) + ); +} + #[test] fn test_snowflake_single_line_tokenize() { let sql = "CREATE TABLE# this is a comment \ntable_1"; From 89c2ac6fdb33b50fb40067dd7bc8bb96cc775823 Mon Sep 17 00:00:00 2001 From: Ilson Roberto Balliego Junior Date: Mon, 6 May 2024 10:31:37 +0200 Subject: [PATCH 08/14] change snowflake visibility and temporal keywords to be mutually exclusive --- src/dialect/snowflake.rs | 31 ++++++++++++++++++--------- tests/sqlparser_snowflake.rs | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index d9ffa0ba6..9f1d7f27b 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -94,17 +94,28 @@ impl Dialect for SnowflakeDialect { // possibly CREATE STAGE //[ OR REPLACE ] let or_replace = parser.parse_keywords(&[Keyword::OR, Keyword::REPLACE]); - let mut global = None; - if parser.parse_keyword(Keyword::LOCAL) { - global = Some(false) - } - if parser.parse_keyword(Keyword::GLOBAL) { - global = Some(true) + // LOCAL | GLOBAL + let global = match parser.parse_one_of_keywords(&[Keyword::LOCAL, Keyword::GLOBAL]) { + Some(Keyword::LOCAL) => Some(false), + Some(Keyword::GLOBAL) => Some(true), + _ => None, + }; + + let mut temporary = false; + let mut volatile = false; + let mut transient = false; + + match parser.parse_one_of_keywords(&[ + Keyword::TEMP, + Keyword::TEMPORARY, + Keyword::VOLATILE, + Keyword::TRANSIENT, + ]) { + Some(Keyword::TEMP | Keyword::TEMPORARY) => temporary = true, + Some(Keyword::VOLATILE) => volatile = true, + Some(Keyword::TRANSIENT) => transient = true, + _ => {} } - let temporary = - parser.parse_keyword(Keyword::TEMP) || parser.parse_keyword(Keyword::TEMPORARY); - let volatile = parser.parse_keyword(Keyword::VOLATILE); - let transient = parser.parse_keyword(Keyword::TRANSIENT); if parser.parse_keyword(Keyword::STAGE) { // OK - this is CREATE STAGE statement diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 6bc8c930f..f0a7c7735 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -380,6 +380,47 @@ fn test_snowflake_create_global_table() { } } +#[test] +fn test_snowflake_create_invalid_local_global_table() { + assert_eq!( + snowflake().parse_sql_statements("CREATE LOCAL GLOBAL TABLE my_table (a INT)"), + Err(ParserError::ParserError( + "Expected an SQL statement, found: LOCAL".to_string() + )) + ); + + assert_eq!( + snowflake().parse_sql_statements("CREATE GLOBAL LOCAL TABLE my_table (a INT)"), + Err(ParserError::ParserError( + "Expected an SQL statement, found: GLOBAL".to_string() + )) + ); +} + +#[test] +fn test_snowflake_create_invalid_temporal_table() { + assert_eq!( + snowflake().parse_sql_statements("CREATE TEMP TEMPORARY TABLE my_table (a INT)"), + Err(ParserError::ParserError( + "Expected an object type after CREATE, found: TEMPORARY".to_string() + )) + ); + + assert_eq!( + snowflake().parse_sql_statements("CREATE TEMP VOLATILE TABLE my_table (a INT)"), + Err(ParserError::ParserError( + "Expected an object type after CREATE, found: VOLATILE".to_string() + )) + ); + + assert_eq!( + snowflake().parse_sql_statements("CREATE TEMP TRANSIENT TABLE my_table (a INT)"), + Err(ParserError::ParserError( + "Expected an object type after CREATE, found: TRANSIENT".to_string() + )) + ); +} + #[test] fn test_snowflake_create_table_if_not_exists() { match snowflake().verified_stmt("CREATE TABLE IF NOT EXISTS my_table (a INT)") { From 87d534dfda0dd2c949339a8bbf3dbb552c28c67d Mon Sep 17 00:00:00 2001 From: Ilson Roberto Balliego Junior Date: Mon, 6 May 2024 16:06:45 +0200 Subject: [PATCH 09/14] implement display for WrappedCollection --- src/ast/dml.rs | 10 +--------- src/ast/mod.rs | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 13e98d27c..7c7f00d57 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -306,15 +306,7 @@ impl Display for CreateTable { write!(f, " PARTITION BY {partition_by}")?; } if let Some(cluster_by) = self.cluster_by.as_ref() { - write!(f, " CLUSTER BY ")?; - match cluster_by { - WrappedCollection::NoWrapping(cluster_by) => { - write!(f, "{}", display_comma_separated(cluster_by.as_slice()))?; - } - WrappedCollection::Parentheses(cluster_by) => { - write!(f, "({})", display_comma_separated(cluster_by.as_slice()))?; - } - } + write!(f, " CLUSTER BY {cluster_by}")?; } if let Some(options) = self.options.as_ref() { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6442b21f6..bcb9986ee 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6399,15 +6399,46 @@ impl Display for CommentDef { } } -/// Helper to indicate if a collection should be wrapped by a symbol when displaying +/// Helper to indicate if a collection should be wrapped by a symbol in the display form +/// +/// [`Display`] is implemented for every [Vec] where `T: Display`. +/// The string output is a comma separated list for the vec items +/// +/// # Examples +/// ``` +/// # use sqlparser::ast::WrappedCollection; +/// let items = WrappedCollection::Parentheses(vec!["one", "two", "three"]); +/// assert_eq!("(one, two, three)", items.to_string()); +/// +/// let items = WrappedCollection::NoWrapping(vec!["one", "two", "three"]); +/// assert_eq!("one, two, three", items.to_string()); +/// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum WrappedCollection { + /// Print the collection without wrapping symbols, as `item, item, item` NoWrapping(T), + /// Wraps the collection in Parentheses, as `(item, item, item)` Parentheses(T), } +impl Display for WrappedCollection> +where + T: Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WrappedCollection::NoWrapping(inner) => { + write!(f, "{}", display_comma_separated(inner.as_slice())) + } + WrappedCollection::Parentheses(inner) => { + write!(f, "({})", display_comma_separated(inner.as_slice())) + } + } + } +} + #[cfg(test)] mod tests { use super::*; From c8b0d434fbb04cdd0f23b54f4d9ec3708179f60f Mon Sep 17 00:00:00 2001 From: Ilson Roberto Balliego Junior Date: Mon, 6 May 2024 16:07:12 +0200 Subject: [PATCH 10/14] include comments for Snowflake structs --- src/ast/mod.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index bcb9986ee..351daeffa 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6338,6 +6338,10 @@ impl Display for TableEngine { } } +/// Snowflake `WITH ROW ACCESS POLICY policy_name ON (identifier, ...)` +/// +/// +/// #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -6363,6 +6367,9 @@ impl Display for RowAccessPolicy { } } +/// Snowflake `WITH TAG ( tag_name = '', ...)` +/// +/// #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -6383,10 +6390,13 @@ impl Display for Tag { } } +/// Helper to indicate if a comment includes the `=` in the display form #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum CommentDef { + /// Includes `=` when printing the comment, as `COMMENT = 'comment'` + /// Does not include `=` when printing the comment, as `COMMENT 'comment'` WithEq(String), WithoutEq(String), } From 5ad977fbe2aa6cd1867f737f085ae439efc9b2f1 Mon Sep 17 00:00:00 2001 From: Ilson Roberto Balliego Junior Date: Mon, 6 May 2024 16:07:31 +0200 Subject: [PATCH 11/14] re-add tests in the common tests file --- tests/sqlparser_common.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 8fe7b862c..f6518e276 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3453,9 +3453,14 @@ fn parse_create_table_as_table() { #[test] fn parse_create_table_on_cluster() { + let generic = TestedDialects { + dialects: vec![Box::new(GenericDialect {})], + options: None, + }; + // Using single-quote literal to define current cluster let sql = "CREATE TABLE t ON CLUSTER '{cluster}' (a INT, b INT)"; - match verified_stmt(sql) { + match generic.verified_stmt(sql) { Statement::CreateTable(CreateTable { on_cluster, .. }) => { assert_eq!(on_cluster.unwrap(), "{cluster}".to_string()); } @@ -3464,7 +3469,7 @@ fn parse_create_table_on_cluster() { // Using explicitly declared cluster name let sql = "CREATE TABLE t ON CLUSTER my_cluster (a INT, b INT)"; - match verified_stmt(sql) { + match generic.verified_stmt(sql) { Statement::CreateTable(CreateTable { on_cluster, .. }) => { assert_eq!(on_cluster.unwrap(), "my_cluster".to_string()); } @@ -3517,8 +3522,13 @@ fn parse_create_table_with_on_delete_on_update_2in_any_order() -> Result<(), Par #[test] fn parse_create_table_with_options() { + let generic = TestedDialects { + dialects: vec![Box::new(GenericDialect {})], + options: None, + }; + let sql = "CREATE TABLE t (c INT) WITH (foo = 'bar', a = 123)"; - match verified_stmt(sql) { + match generic.verified_stmt(sql) { Statement::CreateTable(CreateTable { with_options, .. }) => { assert_eq!( vec![ From af435215cab39951e621a9932fd30b0e110b158d Mon Sep 17 00:00:00 2001 From: Ilson Roberto Balliego Junior Date: Wed, 5 Jun 2024 20:53:41 +0200 Subject: [PATCH 12/14] fix comment after rebase --- src/ast/dml.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 7c7f00d57..4d473ff24 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -290,9 +290,17 @@ impl Display for CreateTable { if let Some(engine) = &self.engine { write!(f, " ENGINE={engine}")?; } - if let Some(comment) = &self.comment { - write!(f, " COMMENT '{comment}'")?; + if let Some(comment_def) = &self.comment { + match comment_def { + CommentDef::WithEq(comment) => { + write!(f, " COMMENT = '{comment}'")?; + } + CommentDef::WithoutEq(comment) => { + write!(f, " COMMENT '{comment}'")?; + } + } } + if let Some(auto_increment_offset) = self.auto_increment_offset { write!(f, " AUTO_INCREMENT {auto_increment_offset}")?; } From c2ed07507cd0a310dc0b02caee95f24c8f3eac3b Mon Sep 17 00:00:00 2001 From: Ilson Roberto Balliego Junior Date: Fri, 7 Jun 2024 22:49:36 +0200 Subject: [PATCH 13/14] fix docs --- src/ast/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 351daeffa..49d6499c5 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6411,7 +6411,7 @@ impl Display for CommentDef { /// Helper to indicate if a collection should be wrapped by a symbol in the display form /// -/// [`Display`] is implemented for every [Vec] where `T: Display`. +/// [`Display`] is implemented for every [`Vec`] where `T: Display`. /// The string output is a comma separated list for the vec items /// /// # Examples From e1452bc9946869f8558211c62ec2449c44fadd33 Mon Sep 17 00:00:00 2001 From: Ilson Roberto Balliego Junior Date: Fri, 7 Jun 2024 22:50:00 +0200 Subject: [PATCH 14/14] fix compilation --- src/ast/dml.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 4d473ff24..74bb5435c 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -333,7 +333,7 @@ impl Display for CreateTable { write!( f, " ENABLE_SCHEMA_EVOLUTION={}", - is_enabled.to_string().to_uppercase() + if is_enabled { "TRUE" } else { "FALSE" } )?; } @@ -341,7 +341,7 @@ impl Display for CreateTable { write!( f, " CHANGE_TRACKING={}", - is_enabled.to_string().to_uppercase() + if is_enabled { "TRUE" } else { "FALSE" } )?; }