diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 7238785ca..74bb5435c 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, SelectItem, SqlOption, - SqliteOnConflict, TableEngine, TableWithJoins, + 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. @@ -57,6 +58,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, @@ -74,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, @@ -94,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>, @@ -102,6 +104,33 @@ 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 { @@ -115,7 +144,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 @@ -130,6 +159,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 { @@ -260,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}")?; } @@ -276,12 +314,9 @@ 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 {cluster_by}")?; } + if let Some(options) = self.options.as_ref() { write!( f, @@ -289,6 +324,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={}", + 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(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..d862a36ae 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -9,8 +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, SqlOption, Statement, TableConstraint, TableEngine, + ColumnDef, CommentDef, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, + OnCommit, OneOrManyWithParens, Query, RowAccessPolicy, SqlOption, Statement, TableConstraint, + TableEngine, Tag, WrappedCollection, }; use crate::parser::ParserError; @@ -52,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, @@ -66,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, @@ -75,9 +77,18 @@ 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, + 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 { @@ -89,6 +100,7 @@ impl CreateTableBuilder { global: None, if_not_exists: false, transient: false, + volatile: false, name, columns: vec![], constraints: vec![], @@ -115,6 +127,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 { @@ -147,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 @@ -210,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 } @@ -255,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 } @@ -270,6 +296,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, @@ -278,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, @@ -304,6 +382,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, }) } } @@ -322,6 +409,7 @@ impl TryFrom for CreateTableBuilder { global, if_not_exists, transient, + volatile, name, columns, constraints, @@ -348,6 +436,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 +478,16 @@ 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, + volatile, }), _ => Err(ParserError::ParserError(format!( "Expected create table statement, but received: {stmt}" @@ -393,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 1747d677e..49d6499c5 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6338,6 +6338,117 @@ 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))] +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()) + ) + } +} + +/// 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))] +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) + } +} + +/// 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), +} + +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 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::*; diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 894b00438..9f1d7f27b 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -12,11 +12,14 @@ #[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::{ + CommentDef, Ident, ObjectName, RowAccessPolicy, Statement, Tag, WrappedCollection, +}; use crate::dialect::Dialect; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -91,12 +94,36 @@ 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); + // 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, + _ => {} + } 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, global, temporary, volatile, transient, parser, + )); } else { // need to go back with the cursor let mut back = 1; @@ -120,6 +147,196 @@ impl Dialect for SnowflakeDialect { } } +/// Parse snowflake create table statement. +/// +pub fn parse_create_table( + or_replace: bool, + global: Option, + 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) + .volatile(volatile) + .global(global) + .hive_formats(Some(Default::default())); + + // 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 { + 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(CommentDef::WithEq(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)?; + parser.expect_token(&Token::LParen)?; + 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) + } + 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)?; + 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))) + } + 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)) + } + + parser.expect_token(&Token::LParen)?; + let tags = parser.parse_comma_separated(parse_tag)?; + parser.expect_token(&Token::RParen)?; + 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 => { + 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; + } + _ => { + 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..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; @@ -7783,7 +7785,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_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_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![ 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..f0a7c7735 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -40,6 +40,279 @@ 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() { + 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, + .. + }) => { + 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() { + 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, + .. + }) => { + 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() { + 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, .. + }) => { + 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 +327,162 @@ 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_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_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)") { + 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(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!(), + } +} + +#[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";