diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index f81c6fc20..d2863c3a1 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1426,7 +1426,24 @@ impl fmt::Display for ColumnDef { pub struct ViewColumnDef { pub name: Ident, pub data_type: Option, - pub options: Option>, + 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 { @@ -1436,7 +1453,14 @@ 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()))?; + 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/mod.rs b/src/ast/mod.rs index ef0c4dc92..bfed91fb6 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -61,13 +61,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 ca321cc2e..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,10 +991,13 @@ impl Spanned for ViewColumnDef { options, } = self; - union_spans( - core::iter::once(name.span) - .chain(options.iter().flat_map(|i| i.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())) } } @@ -1055,7 +1058,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/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 ca658d7da..44ba0bb7c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10579,17 +10579,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 { @@ -10602,6 +10592,25 @@ impl<'a> Parser<'a> { }) } + 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; + } + } + 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. /// For example: `(col1, "col 2", ...)` pub fn parse_parenthesized_column_list( diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 0de0b12ba..2bcdb4e59 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -355,14 +355,16 @@ fn parse_create_view_with_options() { 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: 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 93b4c4f59..ed5d7ad2f 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -914,7 +914,7 @@ fn parse_create_view_with_fields_data_types() { }]), vec![] )), - options: None + options: None, }, ViewColumnDef { name: "f".into(), @@ -926,7 +926,7 @@ fn parse_create_view_with_fields_data_types() { }]), vec![] )), - options: None + options: None, }, ] ); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index d45da69ae..3c20f82a6 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7990,7 +7990,7 @@ fn parse_create_view_with_columns() { .map(|name| ViewColumnDef { name, data_type: None, - options: None + options: None, }) .collect::>() ); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 7dc00f9a8..8b3988d92 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3124,7 +3124,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); } } @@ -3133,7 +3133,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!( @@ -3142,7 +3142,9 @@ fn parse_view_column_descriptions() { ViewColumnDef { name: Ident::new("a"), data_type: None, - options: Some(vec![ColumnOption::Comment("Comment".to_string())]), + options: Some(ColumnOptions::SpaceSeparated(vec![ColumnOption::Comment( + "Comment".to_string() + )])), }, ViewColumnDef { name: Ident::new("b"), @@ -4165,3 +4167,10 @@ fn test_snowflake_fetch_clause_syntax() { canonical, ); } + +#[test] +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); +}