From ab8c66eaa5089ecbe7b7ac3a46bb17bee68e37fc Mon Sep 17 00:00:00 2001 From: osipovartem Date: Fri, 11 Jul 2025 19:11:29 +0300 Subject: [PATCH 1/8] Rebase --- src/ast/ddl.rs | 114 ++++++++- src/ast/helpers/mod.rs | 1 + src/ast/helpers/stmt_create_database.rs | 307 ++++++++++++++++++++++++ src/ast/mod.rs | 52 +++- src/ast/spans.rs | 12 +- src/dialect/snowflake.rs | 121 +++++++++- src/keywords.rs | 6 + src/parser/mod.rs | 12 +- tests/sqlparser_mysql.rs | 1 + tests/sqlparser_postgres.rs | 2 - tests/sqlparser_snowflake.rs | 66 +++++ 11 files changed, 670 insertions(+), 24 deletions(-) create mode 100644 src/ast/helpers/stmt_create_database.rs diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index bb85eb06c..d41b187c5 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -29,13 +29,7 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; -use crate::ast::{ - display_comma_separated, display_separated, CommentDef, CreateFunctionBody, - CreateFunctionUsing, DataType, Expr, FunctionBehavior, FunctionCalledOnNull, - FunctionDeterminismSpecifier, FunctionParallel, Ident, MySQLColumnPosition, ObjectName, - OperateFunctionArg, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, Tag, Value, - ValueWithSpan, -}; +use crate::ast::{display_comma_separated, display_separated, CatalogSyncNamespaceMode, CommentDef, CreateFunctionBody, CreateFunctionUsing, DataType, Expr, FunctionBehavior, FunctionCalledOnNull, FunctionDeterminismSpecifier, FunctionParallel, Ident, MySQLColumnPosition, ObjectName, OperateFunctionArg, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, StorageSerializationPolicy, Tag, Value, ValueWithSpan}; use crate::keywords::Keyword; use crate::tokenizer::Token; @@ -2272,3 +2266,109 @@ impl fmt::Display for CreateConnector { Ok(()) } } + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateSnowflakeDatabase { + pub or_replace: bool, + pub transient: bool, + pub if_not_exists: bool, + pub name: ObjectName, + pub clone: Option, + pub data_retention_time_in_days: Option, + pub max_data_extension_time_in_days: Option, + pub external_volume: Option, + pub catalog: Option, + pub replace_invalid_characters: Option, + pub default_ddl_collation: Option, + pub storage_serialization_policy: Option, + pub comment: Option, + pub catalog_sync: Option, + pub catalog_sync_namespace_mode: Option, + pub catalog_sync_namespace_flatten_delimiter: Option, + pub with_tags: Option>, + pub with_contacts: Option>, +} + +impl fmt::Display for CreateSnowflakeDatabase { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE {or_replace}{transient}DATABASE {if_not_exists}{name}", + or_replace = if self.or_replace { "OR REPLACE " } else { "" }, + transient = if self.transient { "TRANSIENT " } else { "" }, + if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" }, + name = self.name, + )?; + + if let Some(clone) = &self.clone { + write!(f, " CLONE {clone}")?; + } + + if let Some(value) = self.data_retention_time_in_days { + write!(f, " DATA_RETENTION_TIME_IN_DAYS = {value}")?; + } + + if let Some(value) = self.max_data_extension_time_in_days { + write!(f, " MAX_DATA_EXTENSION_TIME_IN_DAYS = {value}")?; + } + + if let Some(vol) = &self.external_volume { + write!(f, " EXTERNAL_VOLUME = '{vol}'")?; + } + + if let Some(cat) = &self.catalog { + write!(f, " CATALOG = '{cat}'")?; + } + + if let Some(true) = self.replace_invalid_characters { + write!(f, " REPLACE_INVALID_CHARACTERS = TRUE")?; + } else if let Some(false) = self.replace_invalid_characters { + write!(f, " REPLACE_INVALID_CHARACTERS = FALSE")?; + } + + if let Some(collation) = &self.default_ddl_collation { + write!(f, " DEFAULT_DDL_COLLATION = '{collation}'")?; + } + + if let Some(policy) = &self.storage_serialization_policy { + write!(f, " STORAGE_SERIALIZATION_POLICY = {policy}")?; + } + + if let Some(comment) = &self.comment { + write!(f, " COMMENT = '{comment}'")?; + } + + if let Some(sync) = &self.catalog_sync { + write!(f, " CATALOG_SYNC = '{sync}'")?; + } + + if let Some(mode) = &self.catalog_sync_namespace_mode { + write!(f, " CATALOG_SYNC_NAMESPACE_MODE = {mode}")?; + } + + if let Some(delim) = &self.catalog_sync_namespace_flatten_delimiter { + write!(f, " CATALOG_SYNC_NAMESPACE_FLATTEN_DELIMITER = '{delim}'")?; + } + + if let Some(tags) = &self.with_tags { + write!(f, " WITH TAG ({})", display_comma_separated(tags))?; + } + + if let Some(contacts) = &self.with_contacts { + write!( + f, + " WITH CONTACT ({})", + display_comma_separated( + &contacts + .iter() + .map(|(purpose, contact)| format!("{purpose} = {contact}")) + .collect::>() + ) + )?; + } + + Ok(()) + } +} diff --git a/src/ast/helpers/mod.rs b/src/ast/helpers/mod.rs index 55831220d..3efbcf7b0 100644 --- a/src/ast/helpers/mod.rs +++ b/src/ast/helpers/mod.rs @@ -16,5 +16,6 @@ // under the License. pub mod attached_token; pub mod key_value_options; +pub mod stmt_create_database; pub mod stmt_create_table; pub mod stmt_data_loading; diff --git a/src/ast/helpers/stmt_create_database.rs b/src/ast/helpers/stmt_create_database.rs new file mode 100644 index 000000000..17893aeb8 --- /dev/null +++ b/src/ast/helpers/stmt_create_database.rs @@ -0,0 +1,307 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#[cfg(not(feature = "std"))] +use alloc::{boxed::Box, format, string::String, vec, vec::Vec}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "visitor")] +use sqlparser_derive::{Visit, VisitMut}; + +use crate::ast::ddl::CreateSnowflakeDatabase; +use crate::ast::{ + CatalogSyncNamespaceMode, ObjectName, Statement, StorageSerializationPolicy, Tag, +}; +use crate::parser::ParserError; + +/// Builder for create database statement variant ([1]). +/// +/// This structure helps building and accessing a create database with more ease, without needing to: +/// - Match the enum itself a lot of times; or +/// - Moving a lot of variables around the code. +/// +/// # Example +/// ```rust +/// use sqlparser::ast::helpers::stmt_create_database::CreateDatabaseBuilder; +/// use sqlparser::ast::{ColumnDef, Ident, ObjectName}; +/// let builder = CreateDatabaseBuilder::new(ObjectName::from(vec![Ident::new("database_name")])) +/// .if_not_exists(true); +/// // You can access internal elements with ease +/// assert!(builder.if_not_exists); +/// // Convert to a statement +/// assert_eq!( +/// builder.build().to_string(), +/// "CREATE DATABASE IF NOT EXISTS database_name" +/// ) +/// ``` +/// +/// [1]: crate::ast::Statement::CreateSnowflakeDatabase +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateDatabaseBuilder { + pub or_replace: bool, + pub transient: bool, + pub if_not_exists: bool, + pub name: ObjectName, + pub clone: Option, + pub data_retention_time_in_days: Option, + pub max_data_extension_time_in_days: Option, + pub external_volume: Option, + pub catalog: Option, + pub replace_invalid_characters: Option, + pub default_ddl_collation: Option, + pub storage_serialization_policy: Option, + pub comment: Option, + pub catalog_sync: Option, + pub catalog_sync_namespace_mode: Option, + pub catalog_sync_namespace_flatten_delimiter: Option, + pub with_tags: Option>, + pub with_contacts: Option>, +} + +impl CreateDatabaseBuilder { + pub fn new(name: ObjectName) -> Self { + Self { + or_replace: false, + transient: false, + if_not_exists: false, + name, + clone: None, + data_retention_time_in_days: None, + max_data_extension_time_in_days: None, + external_volume: None, + catalog: None, + replace_invalid_characters: None, + default_ddl_collation: None, + storage_serialization_policy: None, + comment: None, + catalog_sync: None, + catalog_sync_namespace_mode: None, + catalog_sync_namespace_flatten_delimiter: None, + with_tags: None, + with_contacts: None, + } + } + + pub fn or_replace(mut self, or_replace: bool) -> Self { + self.or_replace = or_replace; + self + } + + pub fn transient(mut self, transient: bool) -> Self { + self.transient = transient; + self + } + + pub fn if_not_exists(mut self, if_not_exists: bool) -> Self { + self.if_not_exists = if_not_exists; + self + } + + pub fn clone_clause(mut self, clone: Option) -> Self { + self.clone = clone; + 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 external_volume(mut self, external_volume: Option) -> Self { + self.external_volume = external_volume; + self + } + + pub fn catalog(mut self, catalog: Option) -> Self { + self.catalog = catalog; + self + } + + pub fn replace_invalid_characters(mut self, replace_invalid_characters: Option) -> Self { + self.replace_invalid_characters = replace_invalid_characters; + self + } + + pub fn default_ddl_collation(mut self, default_ddl_collation: Option) -> Self { + self.default_ddl_collation = default_ddl_collation; + self + } + + pub fn storage_serialization_policy( + mut self, + storage_serialization_policy: Option, + ) -> Self { + self.storage_serialization_policy = storage_serialization_policy; + self + } + + pub fn comment(mut self, comment: Option) -> Self { + self.comment = comment; + self + } + + pub fn catalog_sync(mut self, catalog_sync: Option) -> Self { + self.catalog_sync = catalog_sync; + self + } + + pub fn catalog_sync_namespace_mode( + mut self, + catalog_sync_namespace_mode: Option, + ) -> Self { + self.catalog_sync_namespace_mode = catalog_sync_namespace_mode; + self + } + + pub fn catalog_sync_namespace_flatten_delimiter( + mut self, + catalog_sync_namespace_flatten_delimiter: Option, + ) -> Self { + self.catalog_sync_namespace_flatten_delimiter = catalog_sync_namespace_flatten_delimiter; + self + } + + pub fn with_tags(mut self, with_tags: Option>) -> Self { + self.with_tags = with_tags; + self + } + + pub fn with_contacts(mut self, with_contacts: Option>) -> Self { + self.with_contacts = with_contacts; + self + } + + pub fn build(self) -> Statement { + Statement::CreateSnowflakeDatabase(CreateSnowflakeDatabase { + or_replace: self.or_replace, + transient: self.transient, + if_not_exists: self.if_not_exists, + name: self.name, + clone: self.clone, + data_retention_time_in_days: self.data_retention_time_in_days, + max_data_extension_time_in_days: self.max_data_extension_time_in_days, + external_volume: self.external_volume, + catalog: self.catalog, + replace_invalid_characters: self.replace_invalid_characters, + default_ddl_collation: self.default_ddl_collation, + storage_serialization_policy: self.storage_serialization_policy, + comment: self.comment, + catalog_sync: self.catalog_sync, + catalog_sync_namespace_mode: self.catalog_sync_namespace_mode, + catalog_sync_namespace_flatten_delimiter: self.catalog_sync_namespace_flatten_delimiter, + with_tags: self.with_tags, + with_contacts: self.with_contacts, + }) + } +} + +impl TryFrom for CreateDatabaseBuilder { + type Error = ParserError; + + fn try_from(stmt: Statement) -> Result { + match stmt { + Statement::CreateSnowflakeDatabase(CreateSnowflakeDatabase { + or_replace, + transient, + if_not_exists, + name, + clone, + data_retention_time_in_days, + max_data_extension_time_in_days, + external_volume, + catalog, + replace_invalid_characters, + default_ddl_collation, + storage_serialization_policy, + comment, + catalog_sync, + catalog_sync_namespace_mode, + catalog_sync_namespace_flatten_delimiter, + with_tags, + with_contacts, + }) => Ok(Self { + or_replace, + transient, + if_not_exists, + name, + clone, + data_retention_time_in_days, + max_data_extension_time_in_days, + external_volume, + catalog, + replace_invalid_characters, + default_ddl_collation, + storage_serialization_policy, + comment, + catalog_sync, + catalog_sync_namespace_mode, + catalog_sync_namespace_flatten_delimiter, + with_tags, + with_contacts, + }), + _ => Err(ParserError::ParserError(format!( + "Expected create database statement, but received: {stmt}" + ))), + } + } +} + + +#[cfg(test)] +mod tests { + use crate::ast::helpers::stmt_create_table::CreateTableBuilder; + use crate::ast::{Ident, ObjectName, Statement}; + use crate::ast::helpers::stmt_create_database::CreateDatabaseBuilder; + use crate::parser::ParserError; + + #[test] + pub fn test_from_valid_statement() { + let builder = CreateDatabaseBuilder::new(ObjectName::from(vec![Ident::new("db_name")])); + + let stmt = builder.clone().build(); + + assert_eq!(builder, CreateDatabaseBuilder::try_from(stmt).unwrap()); + } + + #[test] + pub fn test_from_invalid_statement() { + let stmt = Statement::Commit { + chain: false, + end: false, + modifier: None, + }; + + assert_eq!( + CreateDatabaseBuilder::try_from(stmt).unwrap_err(), + ParserError::ParserError( + "Expected create database statement, but received: COMMIT".to_owned() + ) + ); + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 47840ce00..301857629 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -105,6 +105,7 @@ mod operator; mod query; mod spans; pub use spans::Spanned; +use crate::ast::ddl::CreateSnowflakeDatabase; mod trigger; mod value; @@ -3156,9 +3157,7 @@ pub enum Statement { schema_name: SchemaName, if_not_exists: bool, }, - /// ```sql /// CREATE DATABASE - /// ``` CreateDatabase { db_name: ObjectName, if_not_exists: bool, @@ -3166,6 +3165,31 @@ pub enum Statement { managed_location: Option, }, /// ```sql + /// CREATE [ OR REPLACE ] [ TRANSIENT ] DATABASE [ IF NOT EXISTS ] + /// [ CLONE + /// [ { AT | BEFORE } ( { TIMESTAMP => | OFFSET => | STATEMENT => } ) ] + /// [ IGNORE TABLES WITH INSUFFICIENT DATA RETENTION ] + /// [ IGNORE HYBRID TABLES ] ] + /// [ DATA_RETENTION_TIME_IN_DAYS = ] + /// [ MAX_DATA_EXTENSION_TIME_IN_DAYS = ] + /// [ EXTERNAL_VOLUME = ] + /// [ CATALOG = ] + /// [ REPLACE_INVALID_CHARACTERS = { TRUE | FALSE } ] + /// [ DEFAULT_DDL_COLLATION = '' ] + /// [ STORAGE_SERIALIZATION_POLICY = { COMPATIBLE | OPTIMIZED } ] + /// [ COMMENT = '' ] + /// [ CATALOG_SYNC = '' ] + /// [ CATALOG_SYNC_NAMESPACE_MODE = { NEST | FLATTEN } ] + /// [ CATALOG_SYNC_NAMESPACE_FLATTEN_DELIMITER = '' ] + /// [ [ WITH ] TAG ( = '' [ , = '' , ... ] ) ] + /// [ WITH CONTACT ( = [ , = ... ] ) ] + /// ``` + /// See: + /// + /// + /// Creates a new database in the system. + CreateSnowflakeDatabase(CreateSnowflakeDatabase), + /// ```sql /// CREATE FUNCTION /// ``` /// @@ -4006,6 +4030,7 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::CreateSnowflakeDatabase(create_database) => create_database.fmt(f), Statement::CreateFunction(create_function) => create_function.fmt(f), Statement::CreateTrigger { or_replace, @@ -8696,6 +8721,29 @@ impl Display for StorageSerializationPolicy { } } +/// Snowflake CatalogSyncNamespaceMode +/// ```sql +/// [ CATALOG_SYNC_NAMESPACE_MODE = { NEST | FLATTEN } ] +/// ``` +/// +/// +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CatalogSyncNamespaceMode { + Nest, + Flatten, +} + +impl Display for CatalogSyncNamespaceMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CatalogSyncNamespaceMode::Nest => write!(f, "NEST"), + CatalogSyncNamespaceMode::Flatten => write!(f, "FLATTEN"), + } + } +} + /// Variants of the Snowflake `COPY INTO` statement #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 762ade68a..5e79bc9c2 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -17,7 +17,7 @@ use crate::ast::query::SelectItemQualifiedWildcardKind; use core::iter; - +use crate::ast::ddl::CreateSnowflakeDatabase; use crate::tokenizer::Span; use super::{ @@ -366,6 +366,7 @@ impl Spanned for Statement { .chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span()))), ), Statement::Delete(delete) => delete.span(), + Statement::CreateSnowflakeDatabase(create_database) => create_database.span(), Statement::CreateView { or_replace: _, materialized: _, @@ -601,6 +602,15 @@ impl Spanned for CreateTable { } } +impl Spanned for CreateSnowflakeDatabase { + fn span(&self) -> Span { + union_spans( + core::iter::once(self.name.span()) + .chain(self.clone.iter().map(|c| c.span())) + ) + } +} + impl Spanned for ColumnDef { fn span(&self) -> Span { let ColumnDef { diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 2954dce0a..e7566a3e7 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -22,12 +22,7 @@ use crate::ast::helpers::stmt_create_table::CreateTableBuilder; use crate::ast::helpers::stmt_data_loading::{ FileStagingCommand, StageLoadSelectItem, StageParamsObject, }; -use crate::ast::{ - ColumnOption, ColumnPolicy, ColumnPolicyProperty, CopyIntoSnowflakeKind, Ident, - IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, - IdentityPropertyOrder, ObjectName, RowAccessPolicy, ShowObjects, Statement, TagsColumnOption, - WrappedCollection, -}; +use crate::ast::{CatalogSyncNamespaceMode, ColumnOption, ColumnPolicy, ColumnPolicyProperty, CopyIntoSnowflakeKind, Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, ObjectName, RowAccessPolicy, ShowObjects, Statement, TagsColumnOption, WrappedCollection}; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -43,6 +38,7 @@ use alloc::{format, vec}; use super::keywords::RESERVED_FOR_IDENTIFIER; use sqlparser::ast::StorageSerializationPolicy; +use crate::ast::helpers::stmt_create_database::CreateDatabaseBuilder; /// A [`Dialect`] for [Snowflake](https://www.snowflake.com/) #[derive(Debug, Default)] @@ -177,6 +173,8 @@ impl Dialect for SnowflakeDialect { return Some(parse_create_table( or_replace, global, temporary, volatile, transient, iceberg, parser, )); + } else if parser.parse_keyword(Keyword::DATABASE) { + return Some(parse_create_database(or_replace, transient, parser)); } else { // need to go back with the cursor let mut back = 1; @@ -593,6 +591,109 @@ pub fn parse_create_table( Ok(builder.build()) } +pub fn parse_create_database( + or_replace: bool, + transient: bool, + parser: &mut Parser, +) -> Result { + let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = parser.parse_object_name(false)?; + + let mut builder = CreateDatabaseBuilder::new(name) + .or_replace(or_replace) + .transient(transient) + .if_not_exists(if_not_exists); + + loop { + let next_token = parser.next_token(); + match &next_token.token { + Token::Word(word) => match word.keyword { + Keyword::CLONE => { + builder = builder.clone_clause(Some(parser.parse_object_name(false)?)); + } + Keyword::DATA_RETENTION_TIME_IN_DAYS => { + parser.expect_token(&Token::Eq)?; + builder = builder.data_retention_time_in_days(Some(parser.parse_literal_uint()?)); + } + Keyword::MAX_DATA_EXTENSION_TIME_IN_DAYS => { + parser.expect_token(&Token::Eq)?; + builder = builder.max_data_extension_time_in_days(Some(parser.parse_literal_uint()?)); + } + Keyword::EXTERNAL_VOLUME => { + parser.expect_token(&Token::Eq)?; + builder = builder.external_volume(Some(parser.parse_literal_string()?)); + } + Keyword::CATALOG => { + parser.expect_token(&Token::Eq)?; + builder = builder.catalog(Some(parser.parse_literal_string()?)); + } + Keyword::REPLACE_INVALID_CHARACTERS => { + parser.expect_token(&Token::Eq)?; + builder = builder.replace_invalid_characters(Some(parser.parse_boolean_string()?)); + } + Keyword::DEFAULT_DDL_COLLATION => { + parser.expect_token(&Token::Eq)?; + builder = builder.default_ddl_collation(Some(parser.parse_literal_string()?)); + } + Keyword::STORAGE_SERIALIZATION_POLICY => { + parser.expect_token(&Token::Eq)?; + let policy = parse_storage_serialization_policy(parser)?; + builder = builder.storage_serialization_policy(Some(policy)); + } + Keyword::COMMENT => { + parser.expect_token(&Token::Eq)?; + builder = builder.comment(Some(parser.parse_literal_string()?)); + } + Keyword::CATALOG_SYNC => { + parser.expect_token(&Token::Eq)?; + builder = builder.catalog_sync(Some(parser.parse_literal_string()?)); + } + Keyword::CATALOG_SYNC_NAMESPACE_FLATTEN_DELIMITER => { + parser.expect_token(&Token::Eq)?; + builder = builder.catalog_sync_namespace_flatten_delimiter(Some(parser.parse_literal_string()?)); + } + Keyword::CATALOG_SYNC_NAMESPACE_MODE => { + parser.expect_token(&Token::Eq)?; + let mode = + match parser.parse_one_of_keywords(&[Keyword::NEST, Keyword::FLATTEN]) { + Some(Keyword::NEST) => CatalogSyncNamespaceMode::Nest, + Some(Keyword::FLATTEN) => CatalogSyncNamespaceMode::Flatten, + _ => { + return parser.expected("NEST or FLATTEN", next_token); + } + }; + builder = builder.catalog_sync_namespace_mode(Some(mode)); + } + Keyword::WITH => { + if parser.parse_keyword(Keyword::TAG) { + parser.expect_token(&Token::LParen)?; + let tags = parser.parse_comma_separated(Parser::parse_tag)?; + parser.expect_token(&Token::RParen)?; + builder = builder.with_tags(Some(tags)); + } else if parser.parse_keyword(Keyword::CONTACT) { + parser.expect_token(&Token::LParen)?; + let contacts = parser.parse_comma_separated(|p| { + let purpose = p.parse_identifier()?.value; + p.expect_token(&Token::Eq)?; + let contact = p.parse_identifier()?.value; + Ok((purpose, contact)) + })?; + parser.expect_token(&Token::RParen)?; + builder = builder.with_contacts(Some(contacts)); + } else { + return parser.expected("TAG or CONTACT", next_token); + } + } + _ => return parser.expected("end of statementrrr", next_token), + }, + Token::SemiColon | Token::EOF => break, + _ => return parser.expected("end of statement", next_token), + } + } + Ok(builder.build()) +} + + pub fn parse_storage_serialization_policy( parser: &mut Parser, ) -> Result { @@ -1038,14 +1139,12 @@ fn parse_session_options( } } } - options - .is_empty() - .then(|| { + if options + .is_empty() { { Err(ParserError::ParserError( "expected at least one option".to_string(), )) - }) - .unwrap_or(Ok(options)) + } } else { Ok(options) } } /// Parses options provided within parentheses like: diff --git a/src/keywords.rs b/src/keywords.rs index a6854f073..acaa542d2 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -161,6 +161,8 @@ define_keywords!( CAST, CATALOG, CATALOG_SYNC, + CATALOG_SYNC_NAMESPACE_FLATTEN_DELIMITER, + CATALOG_SYNC_NAMESPACE_MODE, CATCH, CEIL, CEILING, @@ -205,6 +207,7 @@ define_keywords!( CONNECTION, CONNECTOR, CONSTRAINT, + CONTACT, CONTAINS, CONTINUE, CONVERT, @@ -352,6 +355,7 @@ define_keywords!( FIRST, FIRST_VALUE, FIXEDSTRING, + FLATTEN, FLOAT, FLOAT32, FLOAT4, @@ -559,6 +563,7 @@ define_keywords!( NATURAL, NCHAR, NCLOB, + NEST, NESTED, NETWORK, NEW, @@ -724,6 +729,7 @@ define_keywords!( REPAIR, REPEATABLE, REPLACE, + REPLACE_INVALID_CHARACTERS, REPLICA, REPLICATE, REPLICATION, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4a4e4b106..d7855da1e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8793,6 +8793,7 @@ impl<'a> Parser<'a> { /// Parse a literal string pub fn parse_literal_string(&mut self) -> Result { + let tokens = self.tokens.clone().iter().map(|t| t.to_string()).collect::>().join(" "); let next_token = self.next_token(); match next_token.token { Token::Word(Word { @@ -8806,7 +8807,16 @@ impl<'a> Parser<'a> { Ok(s) } Token::UnicodeStringLiteral(s) => Ok(s), - _ => self.expected("literal string", next_token), + _ => self.expected(&format!("literal string {tokens}"), next_token) + } + } + + /// Parse a boolean string + pub fn parse_boolean_string(&mut self) -> Result { + match self.parse_one_of_keywords(&[Keyword::TRUE, Keyword::FALSE]) { + Some(Keyword::TRUE) => Ok(true), + Some(Keyword::FALSE) => Ok(false), + _ => self.expected("TRUE or FALSE", self.peek_token()), } } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 7e99eb017..14507c9a3 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2161,6 +2161,7 @@ fn parse_alter_table_add_column() { iceberg, location: _, on_cluster: _, + .. } => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index bec60c20f..0191b0848 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -905,8 +905,6 @@ fn parse_alter_table_owner_to() { match pg_and_generic().verified_stmt(case.sql) { Statement::AlterTable { name, - if_exists: _, - only: _, operations, .. } => { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 4d2b0f988..7ca45d39f 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3548,3 +3548,69 @@ fn test_snowflake_fetch_clause_syntax() { canonical, ); } + +#[test] +fn test_create_database_basic() { + snowflake().verified_stmt("CREATE DATABASE my_db"); + snowflake().verified_stmt("CREATE OR REPLACE DATABASE my_db"); + snowflake().verified_stmt("CREATE TRANSIENT DATABASE IF NOT EXISTS my_db"); +} + +#[test] +fn test_create_database_clone() { + snowflake().verified_stmt("CREATE DATABASE my_db CLONE src_db"); + snowflake().verified_stmt( + "CREATE OR REPLACE DATABASE my_db CLONE src_db DATA_RETENTION_TIME_IN_DAYS = 1", + ); +} + +#[test] +fn test_create_database_with_all_options() { + snowflake().one_statement_parses_to( + r#" + CREATE OR REPLACE TRANSIENT DATABASE IF NOT EXISTS my_db + CLONE src_db + DATA_RETENTION_TIME_IN_DAYS = 1 + MAX_DATA_EXTENSION_TIME_IN_DAYS = 5 + EXTERNAL_VOLUME = 'volume1' + CATALOG = 'my_catalog' + REPLACE_INVALID_CHARACTERS = TRUE + DEFAULT_DDL_COLLATION = 'en-ci' + STORAGE_SERIALIZATION_POLICY = COMPATIBLE + COMMENT = 'This is my database' + CATALOG_SYNC = 'sync_integration' + CATALOG_SYNC_NAMESPACE_FLATTEN_DELIMITER = '/' + WITH TAG (env = 'prod', team = 'data') + WITH CONTACT (owner = 'admin', dpo = 'compliance') + "#, + "CREATE OR REPLACE TRANSIENT DATABASE IF NOT EXISTS \ + my_db CLONE src_db DATA_RETENTION_TIME_IN_DAYS = 1 MAX_DATA_EXTENSION_TIME_IN_DAYS = 5 \ + EXTERNAL_VOLUME = 'volume1' CATALOG = 'my_catalog' \ + REPLACE_INVALID_CHARACTERS = TRUE DEFAULT_DDL_COLLATION = 'en-ci' \ + STORAGE_SERIALIZATION_POLICY = COMPATIBLE COMMENT = 'This is my database' \ + CATALOG_SYNC = 'sync_integration' CATALOG_SYNC_NAMESPACE_FLATTEN_DELIMITER = '/' \ + WITH TAG (env='prod', team='data') \ + WITH CONTACT (owner = admin, dpo = compliance)" + ); +} + +#[test] +fn test_create_database_errors() { + let err = snowflake() + .parse_sql_statements("CREATE DATABASE") + .unwrap_err() + .to_string(); + assert!( + err.contains("Expected"), + "Unexpected error: {err}" + ); + + let err = snowflake() + .parse_sql_statements("CREATE DATABASE my_db CLONE") + .unwrap_err() + .to_string(); + assert!( + err.contains("Expected"), + "Unexpected error: {err}" + ); +} From 45329bd1a8ab08f4c690e42c1724b91bcc42c626 Mon Sep 17 00:00:00 2001 From: osipovartem Date: Fri, 11 Jul 2025 19:12:17 +0300 Subject: [PATCH 2/8] Rebase --- src/ast/ddl.rs | 14 ++++++++-- src/ast/helpers/stmt_create_database.rs | 3 +-- src/ast/mod.rs | 2 +- src/ast/spans.rs | 9 +++---- src/dialect/snowflake.rs | 34 +++++++++++++++++-------- src/parser/mod.rs | 10 ++++++-- tests/sqlparser_snowflake.rs | 12 +++------ 7 files changed, 51 insertions(+), 33 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index d41b187c5..44d38f8a7 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -29,7 +29,13 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; -use crate::ast::{display_comma_separated, display_separated, CatalogSyncNamespaceMode, CommentDef, CreateFunctionBody, CreateFunctionUsing, DataType, Expr, FunctionBehavior, FunctionCalledOnNull, FunctionDeterminismSpecifier, FunctionParallel, Ident, MySQLColumnPosition, ObjectName, OperateFunctionArg, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, StorageSerializationPolicy, Tag, Value, ValueWithSpan}; +use crate::ast::{ + display_comma_separated, display_separated, CatalogSyncNamespaceMode, CommentDef, + CreateFunctionBody, CreateFunctionUsing, DataType, Expr, FunctionBehavior, + FunctionCalledOnNull, FunctionDeterminismSpecifier, FunctionParallel, Ident, + MySQLColumnPosition, ObjectName, OperateFunctionArg, OrderByExpr, ProjectionSelect, + SequenceOptions, SqlOption, StorageSerializationPolicy, Tag, Value, ValueWithSpan, +}; use crate::keywords::Keyword; use crate::tokenizer::Token; @@ -2298,7 +2304,11 @@ impl fmt::Display for CreateSnowflakeDatabase { "CREATE {or_replace}{transient}DATABASE {if_not_exists}{name}", or_replace = if self.or_replace { "OR REPLACE " } else { "" }, transient = if self.transient { "TRANSIENT " } else { "" }, - if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" }, + if_not_exists = if self.if_not_exists { + "IF NOT EXISTS " + } else { + "" + }, name = self.name, )?; diff --git a/src/ast/helpers/stmt_create_database.rs b/src/ast/helpers/stmt_create_database.rs index 17893aeb8..6f3bd1621 100644 --- a/src/ast/helpers/stmt_create_database.rs +++ b/src/ast/helpers/stmt_create_database.rs @@ -272,12 +272,11 @@ impl TryFrom for CreateDatabaseBuilder { } } - #[cfg(test)] mod tests { + use crate::ast::helpers::stmt_create_database::CreateDatabaseBuilder; use crate::ast::helpers::stmt_create_table::CreateTableBuilder; use crate::ast::{Ident, ObjectName, Statement}; - use crate::ast::helpers::stmt_create_database::CreateDatabaseBuilder; use crate::parser::ParserError; #[test] diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 301857629..245a1c7a0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -104,8 +104,8 @@ pub mod helpers; mod operator; mod query; mod spans; -pub use spans::Spanned; use crate::ast::ddl::CreateSnowflakeDatabase; +pub use spans::Spanned; mod trigger; mod value; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 5e79bc9c2..af8212dd3 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -15,10 +15,10 @@ // specific language governing permissions and limitations // under the License. -use crate::ast::query::SelectItemQualifiedWildcardKind; -use core::iter; use crate::ast::ddl::CreateSnowflakeDatabase; +use crate::ast::query::SelectItemQualifiedWildcardKind; use crate::tokenizer::Span; +use core::iter; use super::{ dcl::SecondaryRoles, value::ValueWithSpan, AccessExpr, AlterColumnOperation, @@ -604,10 +604,7 @@ impl Spanned for CreateTable { impl Spanned for CreateSnowflakeDatabase { fn span(&self) -> Span { - union_spans( - core::iter::once(self.name.span()) - .chain(self.clone.iter().map(|c| c.span())) - ) + union_spans(core::iter::once(self.name.span()).chain(self.clone.iter().map(|c| c.span()))) } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index e7566a3e7..1c9884231 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -22,7 +22,12 @@ use crate::ast::helpers::stmt_create_table::CreateTableBuilder; use crate::ast::helpers::stmt_data_loading::{ FileStagingCommand, StageLoadSelectItem, StageParamsObject, }; -use crate::ast::{CatalogSyncNamespaceMode, ColumnOption, ColumnPolicy, ColumnPolicyProperty, CopyIntoSnowflakeKind, Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, ObjectName, RowAccessPolicy, ShowObjects, Statement, TagsColumnOption, WrappedCollection}; +use crate::ast::{ + CatalogSyncNamespaceMode, ColumnOption, ColumnPolicy, ColumnPolicyProperty, + CopyIntoSnowflakeKind, Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, + IdentityPropertyKind, IdentityPropertyOrder, ObjectName, RowAccessPolicy, ShowObjects, + Statement, TagsColumnOption, WrappedCollection, +}; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -37,8 +42,8 @@ use alloc::vec::Vec; use alloc::{format, vec}; use super::keywords::RESERVED_FOR_IDENTIFIER; -use sqlparser::ast::StorageSerializationPolicy; use crate::ast::helpers::stmt_create_database::CreateDatabaseBuilder; +use sqlparser::ast::StorageSerializationPolicy; /// A [`Dialect`] for [Snowflake](https://www.snowflake.com/) #[derive(Debug, Default)] @@ -613,11 +618,13 @@ pub fn parse_create_database( } Keyword::DATA_RETENTION_TIME_IN_DAYS => { parser.expect_token(&Token::Eq)?; - builder = builder.data_retention_time_in_days(Some(parser.parse_literal_uint()?)); + builder = + builder.data_retention_time_in_days(Some(parser.parse_literal_uint()?)); } Keyword::MAX_DATA_EXTENSION_TIME_IN_DAYS => { parser.expect_token(&Token::Eq)?; - builder = builder.max_data_extension_time_in_days(Some(parser.parse_literal_uint()?)); + builder = + builder.max_data_extension_time_in_days(Some(parser.parse_literal_uint()?)); } Keyword::EXTERNAL_VOLUME => { parser.expect_token(&Token::Eq)?; @@ -629,7 +636,8 @@ pub fn parse_create_database( } Keyword::REPLACE_INVALID_CHARACTERS => { parser.expect_token(&Token::Eq)?; - builder = builder.replace_invalid_characters(Some(parser.parse_boolean_string()?)); + builder = + builder.replace_invalid_characters(Some(parser.parse_boolean_string()?)); } Keyword::DEFAULT_DDL_COLLATION => { parser.expect_token(&Token::Eq)?; @@ -650,13 +658,15 @@ pub fn parse_create_database( } Keyword::CATALOG_SYNC_NAMESPACE_FLATTEN_DELIMITER => { parser.expect_token(&Token::Eq)?; - builder = builder.catalog_sync_namespace_flatten_delimiter(Some(parser.parse_literal_string()?)); + builder = builder.catalog_sync_namespace_flatten_delimiter(Some( + parser.parse_literal_string()?, + )); } Keyword::CATALOG_SYNC_NAMESPACE_MODE => { parser.expect_token(&Token::Eq)?; let mode = match parser.parse_one_of_keywords(&[Keyword::NEST, Keyword::FLATTEN]) { - Some(Keyword::NEST) => CatalogSyncNamespaceMode::Nest, + Some(Keyword::NEST) => CatalogSyncNamespaceMode::Nest, Some(Keyword::FLATTEN) => CatalogSyncNamespaceMode::Flatten, _ => { return parser.expected("NEST or FLATTEN", next_token); @@ -693,7 +703,6 @@ pub fn parse_create_database( Ok(builder.build()) } - pub fn parse_storage_serialization_policy( parser: &mut Parser, ) -> Result { @@ -1139,12 +1148,15 @@ fn parse_session_options( } } } - if options - .is_empty() { { + if options.is_empty() { + { Err(ParserError::ParserError( "expected at least one option".to_string(), )) - } } else { Ok(options) } + } + } else { + Ok(options) + } } /// Parses options provided within parentheses like: diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d7855da1e..24f4b08fb 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8793,7 +8793,13 @@ impl<'a> Parser<'a> { /// Parse a literal string pub fn parse_literal_string(&mut self) -> Result { - let tokens = self.tokens.clone().iter().map(|t| t.to_string()).collect::>().join(" "); + let tokens = self + .tokens + .clone() + .iter() + .map(|t| t.to_string()) + .collect::>() + .join(" "); let next_token = self.next_token(); match next_token.token { Token::Word(Word { @@ -8807,7 +8813,7 @@ impl<'a> Parser<'a> { Ok(s) } Token::UnicodeStringLiteral(s) => Ok(s), - _ => self.expected(&format!("literal string {tokens}"), next_token) + _ => self.expected(&format!("literal string {tokens}"), next_token), } } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 7ca45d39f..3c925a080 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3590,7 +3590,7 @@ fn test_create_database_with_all_options() { STORAGE_SERIALIZATION_POLICY = COMPATIBLE COMMENT = 'This is my database' \ CATALOG_SYNC = 'sync_integration' CATALOG_SYNC_NAMESPACE_FLATTEN_DELIMITER = '/' \ WITH TAG (env='prod', team='data') \ - WITH CONTACT (owner = admin, dpo = compliance)" + WITH CONTACT (owner = admin, dpo = compliance)", ); } @@ -3600,17 +3600,11 @@ fn test_create_database_errors() { .parse_sql_statements("CREATE DATABASE") .unwrap_err() .to_string(); - assert!( - err.contains("Expected"), - "Unexpected error: {err}" - ); + assert!(err.contains("Expected"), "Unexpected error: {err}"); let err = snowflake() .parse_sql_statements("CREATE DATABASE my_db CLONE") .unwrap_err() .to_string(); - assert!( - err.contains("Expected"), - "Unexpected error: {err}" - ); + assert!(err.contains("Expected"), "Unexpected error: {err}"); } From 32795dc7813a5114b90be28af11e2055dea0c223 Mon Sep 17 00:00:00 2001 From: osipovartem Date: Fri, 11 Jul 2025 19:09:22 +0300 Subject: [PATCH 3/8] Fix typo --- src/parser/mod.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 24f4b08fb..dc940fc04 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8793,13 +8793,6 @@ impl<'a> Parser<'a> { /// Parse a literal string pub fn parse_literal_string(&mut self) -> Result { - let tokens = self - .tokens - .clone() - .iter() - .map(|t| t.to_string()) - .collect::>() - .join(" "); let next_token = self.next_token(); match next_token.token { Token::Word(Word { @@ -8813,7 +8806,7 @@ impl<'a> Parser<'a> { Ok(s) } Token::UnicodeStringLiteral(s) => Ok(s), - _ => self.expected(&format!("literal string {tokens}"), next_token), + _ => self.expected("literal string", next_token), } } From b70864f6afde567634f003d60a6001bf80027edd Mon Sep 17 00:00:00 2001 From: osipovartem Date: Fri, 11 Jul 2025 19:34:01 +0300 Subject: [PATCH 4/8] Snowflake create database --- src/ast/mod.rs | 15 ++++++++------- src/ast/query.rs | 1 - src/ast/spans.rs | 32 ++++++++++++++++---------------- src/dialect/snowflake.rs | 5 ++--- tests/sqlparser_postgres.rs | 4 +--- 5 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 245a1c7a0..03485c119 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -51,12 +51,12 @@ pub use self::ddl::{ AlterTableAlgorithm, AlterTableOperation, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnPolicy, ColumnPolicyProperty, - ConstraintCharacteristics, CreateConnector, CreateFunction, Deduplicate, DeferrableInitial, - DropBehavior, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, - IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexOption, - IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, Partition, ProcedureParam, - ReferentialAction, TableConstraint, TagsColumnOption, UserDefinedTypeCompositeAttributeDef, - UserDefinedTypeRepresentation, ViewColumnDef, + ConstraintCharacteristics, CreateConnector, CreateFunction, CreateSnowflakeDatabase, + Deduplicate, DeferrableInitial, DropBehavior, GeneratedAs, GeneratedExpressionMode, + IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, + IdentityPropertyOrder, IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, + Partition, ProcedureParam, ReferentialAction, TableConstraint, TagsColumnOption, + UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; pub use self::dml::{CreateIndex, CreateTable, Delete, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; @@ -104,7 +104,6 @@ pub mod helpers; mod operator; mod query; mod spans; -use crate::ast::ddl::CreateSnowflakeDatabase; pub use spans::Spanned; mod trigger; @@ -3157,7 +3156,9 @@ pub enum Statement { schema_name: SchemaName, if_not_exists: bool, }, + /// ```sql /// CREATE DATABASE + /// ``` CreateDatabase { db_name: ObjectName, if_not_exists: bool, diff --git a/src/ast/query.rs b/src/ast/query.rs index 4e6b4bb95..fa5cfb789 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1287,7 +1287,6 @@ pub enum TableFactor { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] - pub enum TableSampleKind { /// Table sample located before the table alias option BeforeTableAlias(Box), diff --git a/src/ast/spans.rs b/src/ast/spans.rs index af8212dd3..bfcf591a1 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -15,27 +15,27 @@ // specific language governing permissions and limitations // under the License. -use crate::ast::ddl::CreateSnowflakeDatabase; use crate::ast::query::SelectItemQualifiedWildcardKind; use crate::tokenizer::Span; use core::iter; use super::{ - dcl::SecondaryRoles, value::ValueWithSpan, AccessExpr, AlterColumnOperation, - AlterIndexOperation, AlterTableOperation, Array, Assignment, AssignmentTarget, CloseCursor, - ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, ConflictTarget, ConnectBy, - ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, CreateTableOptions, Cte, - Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, FromTable, - Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, - FunctionArguments, GroupByExpr, HavingBound, IlikeSelectItem, Insert, Interpolate, - InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, - MatchRecognizePattern, Measure, NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, - OnConflict, OnConflictAction, OnInsert, OrderBy, OrderByExpr, OrderByKind, Partition, - PivotValueSource, ProjectionSelect, Query, ReferentialAction, RenameSelectItem, - ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, - Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, - TableFactor, TableObject, TableOptionsClustered, TableWithJoins, UpdateTableFromKind, Use, - Value, Values, ViewColumnDef, WildcardAdditionalOptions, With, WithFill, + dcl::SecondaryRoles, ddl::CreateSnowflakeDatabase, value::ValueWithSpan, AccessExpr, + AlterColumnOperation, AlterIndexOperation, AlterTableOperation, Array, Assignment, + AssignmentTarget, CloseCursor, ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, + ConflictTarget, ConnectBy, ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, + CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr, + ExprWithAlias, Fetch, FromTable, Function, FunctionArg, FunctionArgExpr, + FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, HavingBound, + IlikeSelectItem, Insert, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, + JsonPath, JsonPathElem, LateralView, MatchRecognizePattern, Measure, NamedWindowDefinition, + ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, OnInsert, OrderBy, + OrderByExpr, OrderByKind, Partition, PivotValueSource, ProjectionSelect, Query, + ReferentialAction, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, + SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, + TableAliasColumnDef, TableConstraint, TableFactor, TableObject, TableOptionsClustered, + TableWithJoins, UpdateTableFromKind, Use, Value, Values, ViewColumnDef, + WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 1c9884231..c7e41ae28 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -18,6 +18,7 @@ #[cfg(not(feature = "std"))] use crate::alloc::string::ToString; use crate::ast::helpers::key_value_options::{KeyValueOption, KeyValueOptionType, KeyValueOptions}; +use crate::ast::helpers::stmt_create_database::CreateDatabaseBuilder; use crate::ast::helpers::stmt_create_table::CreateTableBuilder; use crate::ast::helpers::stmt_data_loading::{ FileStagingCommand, StageLoadSelectItem, StageParamsObject, @@ -26,7 +27,7 @@ use crate::ast::{ CatalogSyncNamespaceMode, ColumnOption, ColumnPolicy, ColumnPolicyProperty, CopyIntoSnowflakeKind, Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, ObjectName, RowAccessPolicy, ShowObjects, - Statement, TagsColumnOption, WrappedCollection, + Statement, StorageSerializationPolicy, TagsColumnOption, WrappedCollection, }; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; @@ -42,8 +43,6 @@ use alloc::vec::Vec; use alloc::{format, vec}; use super::keywords::RESERVED_FOR_IDENTIFIER; -use crate::ast::helpers::stmt_create_database::CreateDatabaseBuilder; -use sqlparser::ast::StorageSerializationPolicy; /// A [`Dialect`] for [Snowflake](https://www.snowflake.com/) #[derive(Debug, Default)] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 0191b0848..3943b4085 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -904,9 +904,7 @@ fn parse_alter_table_owner_to() { for case in test_cases { match pg_and_generic().verified_stmt(case.sql) { Statement::AlterTable { - name, - operations, - .. + name, operations, .. } => { assert_eq!(name.to_string(), "tab"); assert_eq!( From 1c35f13d6dee490d5ccf19e3a36e49c673828e50 Mon Sep 17 00:00:00 2001 From: osipovartem Date: Fri, 11 Jul 2025 20:00:16 +0300 Subject: [PATCH 5/8] Fix cargo clippy --- src/ast/ddl.rs | 15 +++------------ src/ast/helpers/stmt_create_database.rs | 6 +++--- src/ast/mod.rs | 23 +++++++++++++++++++++++ src/dialect/snowflake.rs | 4 ++-- 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 44d38f8a7..4abbff297 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -30,7 +30,7 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ - display_comma_separated, display_separated, CatalogSyncNamespaceMode, CommentDef, + display_comma_separated, display_separated, CatalogSyncNamespaceMode, CommentDef, ContactEntry, CreateFunctionBody, CreateFunctionUsing, DataType, Expr, FunctionBehavior, FunctionCalledOnNull, FunctionDeterminismSpecifier, FunctionParallel, Ident, MySQLColumnPosition, ObjectName, OperateFunctionArg, OrderByExpr, ProjectionSelect, @@ -2294,7 +2294,7 @@ pub struct CreateSnowflakeDatabase { pub catalog_sync_namespace_mode: Option, pub catalog_sync_namespace_flatten_delimiter: Option, pub with_tags: Option>, - pub with_contacts: Option>, + pub with_contacts: Option>, } impl fmt::Display for CreateSnowflakeDatabase { @@ -2367,16 +2367,7 @@ impl fmt::Display for CreateSnowflakeDatabase { } if let Some(contacts) = &self.with_contacts { - write!( - f, - " WITH CONTACT ({})", - display_comma_separated( - &contacts - .iter() - .map(|(purpose, contact)| format!("{purpose} = {contact}")) - .collect::>() - ) - )?; + write!(f, " WITH CONTACT ({})", display_comma_separated(contacts))?; } Ok(()) diff --git a/src/ast/helpers/stmt_create_database.rs b/src/ast/helpers/stmt_create_database.rs index 6f3bd1621..e24806e35 100644 --- a/src/ast/helpers/stmt_create_database.rs +++ b/src/ast/helpers/stmt_create_database.rs @@ -26,7 +26,7 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::ddl::CreateSnowflakeDatabase; use crate::ast::{ - CatalogSyncNamespaceMode, ObjectName, Statement, StorageSerializationPolicy, Tag, + CatalogSyncNamespaceMode, ContactEntry, ObjectName, Statement, StorageSerializationPolicy, Tag, }; use crate::parser::ParserError; @@ -73,7 +73,7 @@ pub struct CreateDatabaseBuilder { pub catalog_sync_namespace_mode: Option, pub catalog_sync_namespace_flatten_delimiter: Option, pub with_tags: Option>, - pub with_contacts: Option>, + pub with_contacts: Option>, } impl CreateDatabaseBuilder { @@ -192,7 +192,7 @@ impl CreateDatabaseBuilder { self } - pub fn with_contacts(mut self, with_contacts: Option>) -> Self { + pub fn with_contacts(mut self, with_contacts: Option>) -> Self { self.with_contacts = with_contacts; self } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 03485c119..ea30488c4 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8290,6 +8290,29 @@ impl Display for Tag { } } +/// Snowflake `WITH CONTACT ( purpose = contact [ , purpose = contact ...] )` +/// +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ContactEntry { + pub purpose: String, + pub contact: String, +} + +impl ContactEntry { + pub fn new(purpose: String, contact: String) -> Self { + Self { purpose, contact } + } +} + +impl Display for ContactEntry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} = {}", self.purpose, self.contact) + } +} + /// 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))] diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index c7e41ae28..d455fdf11 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -24,7 +24,7 @@ use crate::ast::helpers::stmt_data_loading::{ FileStagingCommand, StageLoadSelectItem, StageParamsObject, }; use crate::ast::{ - CatalogSyncNamespaceMode, ColumnOption, ColumnPolicy, ColumnPolicyProperty, + CatalogSyncNamespaceMode, ColumnOption, ColumnPolicy, ColumnPolicyProperty, ContactEntry, CopyIntoSnowflakeKind, Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, ObjectName, RowAccessPolicy, ShowObjects, Statement, StorageSerializationPolicy, TagsColumnOption, WrappedCollection, @@ -685,7 +685,7 @@ pub fn parse_create_database( let purpose = p.parse_identifier()?.value; p.expect_token(&Token::Eq)?; let contact = p.parse_identifier()?.value; - Ok((purpose, contact)) + Ok(ContactEntry::new(purpose, contact)) })?; parser.expect_token(&Token::RParen)?; builder = builder.with_contacts(Some(contacts)); From a6490983c093f8d03ae4082adaad3eafabc7fa75 Mon Sep 17 00:00:00 2001 From: osipovartem Date: Fri, 11 Jul 2025 20:10:13 +0300 Subject: [PATCH 6/8] Update tests --- src/ast/ddl.rs | 2 +- src/ast/helpers/stmt_create_database.rs | 6 ++---- tests/sqlparser_common.rs | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 4abbff297..67e2f62b4 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2277,10 +2277,10 @@ impl fmt::Display for CreateConnector { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct CreateSnowflakeDatabase { + pub name: ObjectName, pub or_replace: bool, pub transient: bool, pub if_not_exists: bool, - pub name: ObjectName, pub clone: Option, pub data_retention_time_in_days: Option, pub max_data_extension_time_in_days: Option, diff --git a/src/ast/helpers/stmt_create_database.rs b/src/ast/helpers/stmt_create_database.rs index e24806e35..76b39f25d 100644 --- a/src/ast/helpers/stmt_create_database.rs +++ b/src/ast/helpers/stmt_create_database.rs @@ -51,15 +51,14 @@ use crate::parser::ParserError; /// ) /// ``` /// -/// [1]: crate::ast::Statement::CreateSnowflakeDatabase #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct CreateDatabaseBuilder { + pub name: ObjectName, pub or_replace: bool, pub transient: bool, pub if_not_exists: bool, - pub name: ObjectName, pub clone: Option, pub data_retention_time_in_days: Option, pub max_data_extension_time_in_days: Option, @@ -79,10 +78,10 @@ pub struct CreateDatabaseBuilder { impl CreateDatabaseBuilder { pub fn new(name: ObjectName) -> Self { Self { + name, or_replace: false, transient: false, if_not_exists: false, - name, clone: None, data_retention_time_in_days: None, max_data_extension_time_in_days: None, @@ -275,7 +274,6 @@ impl TryFrom for CreateDatabaseBuilder { #[cfg(test)] mod tests { use crate::ast::helpers::stmt_create_database::CreateDatabaseBuilder; - use crate::ast::helpers::stmt_create_table::CreateTableBuilder; use crate::ast::{Ident, ObjectName, Statement}; use crate::parser::ParserError; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 3c58d216c..bef4fafa7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7714,7 +7714,7 @@ fn parse_exists_subquery() { #[test] fn parse_create_database() { let sql = "CREATE DATABASE mydb"; - match verified_stmt(sql) { + match all_dialects_except(|d| d.is::()).verified_stmt(sql) { Statement::CreateDatabase { db_name, if_not_exists, @@ -7733,7 +7733,7 @@ fn parse_create_database() { #[test] fn parse_create_database_ine() { let sql = "CREATE DATABASE IF NOT EXISTS mydb"; - match verified_stmt(sql) { + match all_dialects_except(|d| d.is::()).verified_stmt(sql) { Statement::CreateDatabase { db_name, if_not_exists, From 1d07c4f5c68c93f3d1ad86e5063cc3b5797b514e Mon Sep 17 00:00:00 2001 From: osipovartem Date: Fri, 11 Jul 2025 20:15:08 +0300 Subject: [PATCH 7/8] Fix doc --- src/ast/helpers/stmt_create_database.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ast/helpers/stmt_create_database.rs b/src/ast/helpers/stmt_create_database.rs index 76b39f25d..fbde9382d 100644 --- a/src/ast/helpers/stmt_create_database.rs +++ b/src/ast/helpers/stmt_create_database.rs @@ -51,6 +51,7 @@ use crate::parser::ParserError; /// ) /// ``` /// +/// [1]: Statement::CreateTable #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] From 8e2fb6e67da05d4f29a1b119f93bc0d6b4d11388 Mon Sep 17 00:00:00 2001 From: osipovartem Date: Fri, 11 Jul 2025 20:15:34 +0300 Subject: [PATCH 8/8] Fix doc --- src/ast/helpers/stmt_create_database.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/helpers/stmt_create_database.rs b/src/ast/helpers/stmt_create_database.rs index fbde9382d..bbe753b9d 100644 --- a/src/ast/helpers/stmt_create_database.rs +++ b/src/ast/helpers/stmt_create_database.rs @@ -51,7 +51,7 @@ use crate::parser::ParserError; /// ) /// ``` /// -/// [1]: Statement::CreateTable +/// [1]: Statement::CreateSnowflakeDatabase #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]