From d74df4784a2155c514df5eafb9d120761764be11 Mon Sep 17 00:00:00 2001 From: Elia Perantoni Date: Wed, 11 Jun 2025 12:48:53 +0200 Subject: [PATCH 1/4] feat: Snowflake `WITH` column options --- src/ast/ddl.rs | 7 ++- src/ast/spans.rs | 1 + src/parser/mod.rs | 27 ++++++---- tests/sqlparser_bigquery.rs | 2 + tests/sqlparser_clickhouse.rs | 6 ++- tests/sqlparser_common.rs | 98 ++++++++++++++++++++--------------- tests/sqlparser_snowflake.rs | 18 ++++++- 7 files changed, 100 insertions(+), 59 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 059c61967..6b18ce223 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1422,6 +1422,7 @@ pub struct ViewColumnDef { pub name: Ident, pub data_type: Option, pub options: Option>, + pub options_comma_separated: bool, } impl fmt::Display for ViewColumnDef { @@ -1431,7 +1432,11 @@ impl fmt::Display for ViewColumnDef { write!(f, " {}", data_type)?; } if let Some(options) = self.options.as_ref() { - write!(f, " {}", display_comma_separated(options.as_slice()))?; + if self.options_comma_separated { + write!(f, " {}", display_comma_separated(options.as_slice()))?; + } else { + write!(f, " {}", display_separated(options.as_slice(), " "))?; + } } Ok(()) } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 14664b4cd..abdabedd0 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -982,6 +982,7 @@ impl Spanned for ViewColumnDef { name, data_type: _, // todo, DataType options, + options_comma_separated: _, } = self; union_spans( diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6831d52e0..cb57c1861 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -22,6 +22,7 @@ use alloc::{ }; use core::{ fmt::{self, Display}, + ops::Not, str::FromStr, }; use helpers::attached_token::AttachedToken; @@ -10566,17 +10567,7 @@ impl<'a> Parser<'a> { /// Parses a column definition within a view. fn parse_view_column(&mut self) -> Result { let name = self.parse_identifier()?; - let options = if (dialect_of!(self is BigQueryDialect | GenericDialect) - && self.parse_keyword(Keyword::OPTIONS)) - || (dialect_of!(self is SnowflakeDialect | GenericDialect) - && self.parse_keyword(Keyword::COMMENT)) - { - self.prev_token(); - self.parse_optional_column_option()? - .map(|option| vec![option]) - } else { - None - }; + let options = self.parse_view_column_options()?; let data_type = if dialect_of!(self is ClickHouseDialect) { Some(self.parse_data_type()?) } else { @@ -10586,9 +10577,23 @@ impl<'a> Parser<'a> { name, data_type, options, + options_comma_separated: !dialect_of!(self is SnowflakeDialect), }) } + fn parse_view_column_options(&mut self) -> Result>, ParserError> { + let mut options = Vec::new(); + loop { + let option = self.parse_optional_column_option()?; + if let Some(option) = option { + options.push(option); + } else { + break; + } + } + Ok(options.is_empty().not().then_some(options)) + } + /// Parses a parenthesized comma-separated list of unqualified, possibly quoted identifiers. /// For example: `(col1, "col 2", ...)` pub fn parse_parenthesized_column_list( diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 8f54f3c97..f72c13ba9 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -348,6 +348,7 @@ fn parse_create_view_with_options() { name: Ident::new("name"), data_type: None, options: None, + options_comma_separated: true, }, ViewColumnDef { name: Ident::new("age"), @@ -360,6 +361,7 @@ fn parse_create_view_with_options() { ) ), }])]), + options_comma_separated: true, }, ], columns diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index d0218b6c3..493a313f3 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -910,7 +910,8 @@ fn parse_create_view_with_fields_data_types() { }]), vec![] )), - options: None + options: None, + options_comma_separated: true, }, ViewColumnDef { name: "f".into(), @@ -922,7 +923,8 @@ fn parse_create_view_with_fields_data_types() { }]), vec![] )), - options: None + options: None, + options_comma_separated: true, }, ] ); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index abcadb458..d85512a3a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7960,52 +7960,64 @@ fn parse_create_view_with_options() { #[test] fn parse_create_view_with_columns() { let sql = "CREATE VIEW v (has, cols) AS SELECT 1, 2"; - // TODO: why does this fail for ClickHouseDialect? (#1449) - // match all_dialects().verified_stmt(sql) { - match all_dialects_except(|d| d.is::()).verified_stmt(sql) { - Statement::CreateView { - or_alter, - name, - columns, - or_replace, - options, - query, - materialized, - cluster_by, - comment, - with_no_schema_binding: late_binding, - if_not_exists, - temporary, - to, - params, - } => { - assert_eq!(or_alter, false); - assert_eq!("v", name.to_string()); - assert_eq!( + fn assert_stmt_as_expected(stmt: Statement, options_comma_separated: bool) { + match stmt { + Statement::CreateView { + or_alter, + name, columns, - vec![Ident::new("has"), Ident::new("cols"),] - .into_iter() - .map(|name| ViewColumnDef { - name, - data_type: None, - options: None - }) - .collect::>() - ); - assert_eq!(options, CreateTableOptions::None); - assert_eq!("SELECT 1, 2", query.to_string()); - assert!(!materialized); - assert!(!or_replace); - assert_eq!(cluster_by, vec![]); - assert!(comment.is_none()); - assert!(!late_binding); - assert!(!if_not_exists); - assert!(!temporary); - assert!(to.is_none()); - assert!(params.is_none()); + or_replace, + options, + query, + materialized, + cluster_by, + comment, + with_no_schema_binding: late_binding, + if_not_exists, + temporary, + to, + params, + } => { + assert_eq!(or_alter, false); + assert_eq!("v", name.to_string()); + assert_eq!( + columns, + vec![Ident::new("has"), Ident::new("cols"),] + .into_iter() + .map(|name| ViewColumnDef { + name, + data_type: None, + options: None, + options_comma_separated, + }) + .collect::>() + ); + assert_eq!(options, CreateTableOptions::None); + assert_eq!("SELECT 1, 2", query.to_string()); + assert!(!materialized); + assert!(!or_replace); + assert_eq!(cluster_by, vec![]); + assert!(comment.is_none()); + assert!(!late_binding); + assert!(!if_not_exists); + assert!(!temporary); + assert!(to.is_none()); + assert!(params.is_none()); + } + _ => unreachable!(), } - _ => unreachable!(), } + // TODO: why does this fail for ClickHouseDialect? (#1449) + // match all_dialects().verified_stmt(sql) { + assert_stmt_as_expected( + all_dialects_where(|d| !d.is::() && !d.is::()) + .verified_stmt(sql), + true, + ); + assert_stmt_as_expected( + all_dialects_where(|d| d.is::()).verified_stmt(sql), + false, + ); } #[test] diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index b4d62506d..1d3cd61b6 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3087,7 +3087,7 @@ fn view_comment_option_should_be_after_column_list() { "CREATE OR REPLACE VIEW v (a COMMENT 'a comment', b, c COMMENT 'c comment') COMMENT = 'Comment' AS SELECT a FROM t", "CREATE OR REPLACE VIEW v (a COMMENT 'a comment', b, c COMMENT 'c comment') WITH (foo = bar) COMMENT = 'Comment' AS SELECT a FROM t", ] { - snowflake_and_generic() + snowflake() .verified_stmt(sql); } } @@ -3096,7 +3096,7 @@ fn view_comment_option_should_be_after_column_list() { fn parse_view_column_descriptions() { let sql = "CREATE OR REPLACE VIEW v (a COMMENT 'Comment', b) AS SELECT a, b FROM table1"; - match snowflake_and_generic().verified_stmt(sql) { + match snowflake().verified_stmt(sql) { Statement::CreateView { name, columns, .. } => { assert_eq!(name.to_string(), "v"); assert_eq!( @@ -3106,11 +3106,13 @@ fn parse_view_column_descriptions() { name: Ident::new("a"), data_type: None, options: Some(vec![ColumnOption::Comment("Comment".to_string())]), + options_comma_separated: false, }, ViewColumnDef { name: Ident::new("b"), data_type: None, options: None, + options_comma_separated: false, } ] ); @@ -4045,3 +4047,15 @@ fn parse_connect_by_root_operator() { "sql parser error: Expected an expression, found: FROM" ); } + +#[test] +fn test_snowflake_create_view_with_tag() { + let create_view_with_tag = r#"CREATE VIEW X (COL WITH TAG (pii='email')) AS SELECT * FROM Y"#; + snowflake().verified_stmt(create_view_with_tag); +} + +#[test] +fn test_snowflake_create_view_with_tag_and_comment() { + let create_view_with_tag_and_comment = r#"CREATE VIEW X (COL WITH TAG (pii='email') COMMENT 'foobar') AS SELECT * FROM Y"#; + snowflake().verified_stmt(create_view_with_tag_and_comment); +} From 003a64365cc78a42f8b8e77396c95c7a87358808 Mon Sep 17 00:00:00 2001 From: Elia Perantoni Date: Wed, 18 Jun 2025 09:19:39 +0200 Subject: [PATCH 2/4] chore: format --- tests/sqlparser_snowflake.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 1d3cd61b6..d55fad6a1 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4056,6 +4056,7 @@ fn test_snowflake_create_view_with_tag() { #[test] fn test_snowflake_create_view_with_tag_and_comment() { - let create_view_with_tag_and_comment = r#"CREATE VIEW X (COL WITH TAG (pii='email') COMMENT 'foobar') AS SELECT * FROM Y"#; + let create_view_with_tag_and_comment = + r#"CREATE VIEW X (COL WITH TAG (pii='email') COMMENT 'foobar') AS SELECT * FROM Y"#; snowflake().verified_stmt(create_view_with_tag_and_comment); } From ba000777f15eed7029b308024d033f052de49b2b Mon Sep 17 00:00:00 2001 From: Elia Perantoni Date: Tue, 24 Jun 2025 09:25:53 +0200 Subject: [PATCH 3/4] feat: `ColumnOptions` enum --- src/ast/ddl.rs | 24 +++++++-- src/ast/mod.rs | 15 +++--- src/ast/spans.rs | 12 +++-- src/parser/mod.rs | 12 +++-- tests/sqlparser_bigquery.rs | 20 +++---- tests/sqlparser_clickhouse.rs | 2 - tests/sqlparser_common.rs | 98 +++++++++++++++-------------------- tests/sqlparser_snowflake.rs | 16 ++---- 8 files changed, 103 insertions(+), 96 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 6b18ce223..5b618c972 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1421,8 +1421,24 @@ impl fmt::Display for ColumnDef { pub struct ViewColumnDef { pub name: Ident, pub data_type: Option, - pub options: Option>, - pub options_comma_separated: bool, + pub options: Option, +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ColumnOptions { + CommaSeparated(Vec), + SpaceSeparated(Vec), +} + +impl ColumnOptions { + pub fn as_slice(&self) -> &[ColumnOption] { + match self { + ColumnOptions::CommaSeparated(options) => options.as_slice(), + ColumnOptions::SpaceSeparated(options) => options.as_slice(), + } + } } impl fmt::Display for ViewColumnDef { @@ -1432,10 +1448,10 @@ impl fmt::Display for ViewColumnDef { write!(f, " {}", data_type)?; } if let Some(options) = self.options.as_ref() { - if self.options_comma_separated { + if matches!(options, ColumnOptions::CommaSeparated(_)) { write!(f, " {}", display_comma_separated(options.as_slice()))?; } else { - write!(f, " {}", display_separated(options.as_slice(), " "))?; + write!(f, " {}", display_separated(options.as_slice(), " "))? } } Ok(()) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 04401a48b..1fd93e6cf 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -60,13 +60,14 @@ pub use self::ddl::{ AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, AlterPolicyOperation, AlterTableAlgorithm, AlterTableLock, AlterTableOperation, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, - ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnPolicy, ColumnPolicyProperty, - ConstraintCharacteristics, CreateConnector, CreateDomain, CreateFunction, Deduplicate, - DeferrableInitial, DropBehavior, GeneratedAs, GeneratedExpressionMode, IdentityParameters, - IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, - IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, Partition, - ProcedureParam, ReferentialAction, ReplicaIdentity, TableConstraint, TagsColumnOption, - UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, + ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, + ColumnPolicyProperty, ConstraintCharacteristics, CreateConnector, CreateDomain, CreateFunction, + Deduplicate, DeferrableInitial, DropBehavior, GeneratedAs, GeneratedExpressionMode, + IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, + IdentityPropertyOrder, IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, + Partition, ProcedureParam, ReferentialAction, ReplicaIdentity, TableConstraint, + TagsColumnOption, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, + ViewColumnDef, }; pub use self::dml::{CreateIndex, CreateTable, Delete, IndexColumn, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index abdabedd0..d91053f46 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -982,12 +982,14 @@ impl Spanned for ViewColumnDef { name, data_type: _, // todo, DataType options, - options_comma_separated: _, } = self; union_spans( - core::iter::once(name.span) - .chain(options.iter().flat_map(|i| i.iter().map(|k| k.span()))), + core::iter::once(name.span).chain( + options + .iter() + .flat_map(|i| i.as_slice().iter().map(|k| k.span())), + ), ) } } @@ -1049,7 +1051,9 @@ impl Spanned for CreateTableOptions { match self { CreateTableOptions::None => Span::empty(), CreateTableOptions::With(vec) => union_spans(vec.iter().map(|i| i.span())), - CreateTableOptions::Options(vec) => union_spans(vec.iter().map(|i| i.span())), + CreateTableOptions::Options(vec) => { + union_spans(vec.as_slice().iter().map(|i| i.span())) + } CreateTableOptions::Plain(vec) => union_spans(vec.iter().map(|i| i.span())), CreateTableOptions::TableProperties(vec) => union_spans(vec.iter().map(|i| i.span())), } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e70e34891..84caaf143 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10580,11 +10580,10 @@ impl<'a> Parser<'a> { name, data_type, options, - options_comma_separated: !dialect_of!(self is SnowflakeDialect), }) } - fn parse_view_column_options(&mut self) -> Result>, ParserError> { + fn parse_view_column_options(&mut self) -> Result, ParserError> { let mut options = Vec::new(); loop { let option = self.parse_optional_column_option()?; @@ -10594,7 +10593,14 @@ impl<'a> Parser<'a> { break; } } - Ok(options.is_empty().not().then_some(options)) + Ok(options + .is_empty() + .not() + .then_some(if dialect_of!(self is SnowflakeDialect) { + ColumnOptions::SpaceSeparated(options) + } else { + ColumnOptions::CommaSeparated(options) + })) } /// Parses a parenthesized comma-separated list of unqualified, possibly quoted identifiers. diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 446649f2f..a3158a668 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -348,20 +348,20 @@ fn parse_create_view_with_options() { name: Ident::new("name"), data_type: None, options: None, - options_comma_separated: true, }, ViewColumnDef { name: Ident::new("age"), data_type: None, - options: Some(vec![ColumnOption::Options(vec![SqlOption::KeyValue { - key: Ident::new("description"), - value: Expr::Value( - Value::DoubleQuotedString("field age".to_string()).with_span( - Span::new(Location::new(1, 42), Location::new(1, 52)) - ) - ), - }])]), - options_comma_separated: true, + options: Some(ColumnOptions::CommaSeparated(vec![ColumnOption::Options( + vec![SqlOption::KeyValue { + key: Ident::new("description"), + value: Expr::Value( + Value::DoubleQuotedString("field age".to_string()).with_span( + Span::new(Location::new(1, 42), Location::new(1, 52)) + ) + ), + }] + )])), }, ], columns diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index d3dadef72..ed5d7ad2f 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -915,7 +915,6 @@ fn parse_create_view_with_fields_data_types() { vec![] )), options: None, - options_comma_separated: true, }, ViewColumnDef { name: "f".into(), @@ -928,7 +927,6 @@ fn parse_create_view_with_fields_data_types() { vec![] )), options: None, - options_comma_separated: true, }, ] ); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index d85512a3a..9f40eb903 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7960,64 +7960,52 @@ fn parse_create_view_with_options() { #[test] fn parse_create_view_with_columns() { let sql = "CREATE VIEW v (has, cols) AS SELECT 1, 2"; - fn assert_stmt_as_expected(stmt: Statement, options_comma_separated: bool) { - match stmt { - Statement::CreateView { - or_alter, - name, + // TODO: why does this fail for ClickHouseDialect? (#1449) + // match all_dialects().verified_stmt(sql) { + match all_dialects_where(|d| !d.is::()).verified_stmt(sql) { + Statement::CreateView { + or_alter, + name, + columns, + or_replace, + options, + query, + materialized, + cluster_by, + comment, + with_no_schema_binding: late_binding, + if_not_exists, + temporary, + to, + params, + } => { + assert_eq!(or_alter, false); + assert_eq!("v", name.to_string()); + assert_eq!( columns, - or_replace, - options, - query, - materialized, - cluster_by, - comment, - with_no_schema_binding: late_binding, - if_not_exists, - temporary, - to, - params, - } => { - assert_eq!(or_alter, false); - assert_eq!("v", name.to_string()); - assert_eq!( - columns, - vec![Ident::new("has"), Ident::new("cols"),] - .into_iter() - .map(|name| ViewColumnDef { - name, - data_type: None, - options: None, - options_comma_separated, - }) - .collect::>() - ); - assert_eq!(options, CreateTableOptions::None); - assert_eq!("SELECT 1, 2", query.to_string()); - assert!(!materialized); - assert!(!or_replace); - assert_eq!(cluster_by, vec![]); - assert!(comment.is_none()); - assert!(!late_binding); - assert!(!if_not_exists); - assert!(!temporary); - assert!(to.is_none()); - assert!(params.is_none()); - } - _ => unreachable!(), + vec![Ident::new("has"), Ident::new("cols"),] + .into_iter() + .map(|name| ViewColumnDef { + name, + data_type: None, + options: None, + }) + .collect::>() + ); + assert_eq!(options, CreateTableOptions::None); + assert_eq!("SELECT 1, 2", query.to_string()); + assert!(!materialized); + assert!(!or_replace); + assert_eq!(cluster_by, vec![]); + assert!(comment.is_none()); + assert!(!late_binding); + assert!(!if_not_exists); + assert!(!temporary); + assert!(to.is_none()); + assert!(params.is_none()); } + _ => unreachable!(), } - // TODO: why does this fail for ClickHouseDialect? (#1449) - // match all_dialects().verified_stmt(sql) { - assert_stmt_as_expected( - all_dialects_where(|d| !d.is::() && !d.is::()) - .verified_stmt(sql), - true, - ); - assert_stmt_as_expected( - all_dialects_where(|d| d.is::()).verified_stmt(sql), - false, - ); } #[test] diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 4b1bc5f62..ada37e99e 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3142,14 +3142,14 @@ fn parse_view_column_descriptions() { ViewColumnDef { name: Ident::new("a"), data_type: None, - options: Some(vec![ColumnOption::Comment("Comment".to_string())]), - options_comma_separated: false, + options: Some(ColumnOptions::SpaceSeparated(vec![ColumnOption::Comment( + "Comment".to_string() + )])), }, ViewColumnDef { name: Ident::new("b"), data_type: None, options: None, - options_comma_separated: false, } ] ); @@ -4087,13 +4087,7 @@ fn parse_connect_by_root_operator() { #[test] fn test_snowflake_create_view_with_tag() { - let create_view_with_tag = r#"CREATE VIEW X (COL WITH TAG (pii='email')) AS SELECT * FROM Y"#; - snowflake().verified_stmt(create_view_with_tag); -} - -#[test] -fn test_snowflake_create_view_with_tag_and_comment() { - let create_view_with_tag_and_comment = + let create_view_with_tag = r#"CREATE VIEW X (COL WITH TAG (pii='email') COMMENT 'foobar') AS SELECT * FROM Y"#; - snowflake().verified_stmt(create_view_with_tag_and_comment); + snowflake().verified_stmt(create_view_with_tag); } From 4c52327704aa7314824e9a9a64635bfac7bbe812 Mon Sep 17 00:00:00 2001 From: Elia Perantoni Date: Wed, 25 Jun 2025 15:15:47 +0200 Subject: [PATCH 4/4] feat: pr feedback --- src/ast/ddl.rs | 11 +++++++---- src/ast/spans.rs | 16 ++++++++-------- src/dialect/mod.rs | 4 ++++ src/dialect/snowflake.rs | 4 ++++ src/parser/mod.rs | 16 +++++++--------- tests/sqlparser_common.rs | 2 +- tests/sqlparser_snowflake.rs | 2 +- 7 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 0258b7590..d2863c3a1 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1453,10 +1453,13 @@ impl fmt::Display for ViewColumnDef { write!(f, " {}", data_type)?; } if let Some(options) = self.options.as_ref() { - if matches!(options, ColumnOptions::CommaSeparated(_)) { - write!(f, " {}", display_comma_separated(options.as_slice()))?; - } else { - write!(f, " {}", display_separated(options.as_slice(), " "))? + match options { + ColumnOptions::CommaSeparated(column_options) => { + write!(f, " {}", display_comma_separated(column_options.as_slice()))?; + } + ColumnOptions::SpaceSeparated(column_options) => { + write!(f, " {}", display_separated(column_options.as_slice(), " "))? + } } } Ok(()) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 07d414210..78ed772bd 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -use crate::ast::query::SelectItemQualifiedWildcardKind; +use crate::ast::{query::SelectItemQualifiedWildcardKind, ColumnOptions}; use core::iter; use crate::tokenizer::Span; @@ -991,13 +991,13 @@ impl Spanned for ViewColumnDef { options, } = self; - union_spans( - core::iter::once(name.span).chain( - options - .iter() - .flat_map(|i| i.as_slice().iter().map(|k| k.span())), - ), - ) + name.span.union_opt(&options.as_ref().map(|o| o.span())) + } +} + +impl Spanned for ColumnOptions { + fn span(&self) -> Span { + union_spans(self.as_slice().iter().map(|i| i.span())) } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index a4c899e6b..bc92948d1 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1028,6 +1028,10 @@ pub trait Dialect: Debug + Any { fn supports_set_names(&self) -> bool { false } + + fn supports_space_separated_column_options(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 66e04ac24..5ebb7e37c 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -356,6 +356,10 @@ impl Dialect for SnowflakeDialect { fn get_reserved_keywords_for_select_item_operator(&self) -> &[Keyword] { &RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR } + + fn supports_space_separated_column_options(&self) -> bool { + true + } } fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e7b767f8d..44ba0bb7c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -22,7 +22,6 @@ use alloc::{ }; use core::{ fmt::{self, Display}, - ops::Not, str::FromStr, }; use helpers::attached_token::AttachedToken; @@ -10603,14 +10602,13 @@ impl<'a> Parser<'a> { break; } } - Ok(options - .is_empty() - .not() - .then_some(if dialect_of!(self is SnowflakeDialect) { - ColumnOptions::SpaceSeparated(options) - } else { - ColumnOptions::CommaSeparated(options) - })) + if options.is_empty() { + Ok(None) + } else if self.dialect.supports_space_separated_column_options() { + Ok(Some(ColumnOptions::SpaceSeparated(options))) + } else { + Ok(Some(ColumnOptions::CommaSeparated(options))) + } } /// Parses a parenthesized comma-separated list of unqualified, possibly quoted identifiers. diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 089dbc900..3c20f82a6 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7964,7 +7964,7 @@ fn parse_create_view_with_columns() { let sql = "CREATE VIEW v (has, cols) AS SELECT 1, 2"; // TODO: why does this fail for ClickHouseDialect? (#1449) // match all_dialects().verified_stmt(sql) { - match all_dialects_where(|d| !d.is::()).verified_stmt(sql) { + match all_dialects_except(|d| d.is::()).verified_stmt(sql) { Statement::CreateView { or_alter, name, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 48edade41..8b3988d92 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4169,7 +4169,7 @@ fn test_snowflake_fetch_clause_syntax() { } #[test] -fn test_snowflake_create_view_with_tag() { +fn test_snowflake_create_view_with_multiple_column_options() { let create_view_with_tag = r#"CREATE VIEW X (COL WITH TAG (pii='email') COMMENT 'foobar') AS SELECT * FROM Y"#; snowflake().verified_stmt(create_view_with_tag);