Snowflake: support multiple column options in CREATE VIEW (#1891)
Some checks failed
license / Release Audit Tool (RAT) (push) Has been cancelled
Rust / codestyle (push) Has been cancelled
Rust / lint (push) Has been cancelled
Rust / benchmark-lint (push) Has been cancelled
Rust / compile (push) Has been cancelled
Rust / docs (push) Has been cancelled
Rust / compile-no-std (push) Has been cancelled
Rust / test (beta) (push) Has been cancelled
Rust / test (nightly) (push) Has been cancelled
Rust / test (stable) (push) Has been cancelled

This commit is contained in:
Elia Perantoni 2025-06-25 16:10:01 +02:00 committed by GitHub
parent b2ab0061c1
commit 1bbc05cdff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 98 additions and 40 deletions

View file

@ -1426,7 +1426,24 @@ impl fmt::Display for ColumnDef {
pub struct ViewColumnDef { pub struct ViewColumnDef {
pub name: Ident, pub name: Ident,
pub data_type: Option<DataType>, pub data_type: Option<DataType>,
pub options: Option<Vec<ColumnOption>>, pub options: Option<ColumnOptions>,
}
#[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<ColumnOption>),
SpaceSeparated(Vec<ColumnOption>),
}
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 { impl fmt::Display for ViewColumnDef {
@ -1436,7 +1453,14 @@ impl fmt::Display for ViewColumnDef {
write!(f, " {}", data_type)?; write!(f, " {}", data_type)?;
} }
if let Some(options) = self.options.as_ref() { 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(()) Ok(())
} }

View file

@ -61,13 +61,14 @@ pub use self::ddl::{
AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, AlterPolicyOperation, AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, AlterPolicyOperation,
AlterTableAlgorithm, AlterTableLock, AlterTableOperation, AlterType, AlterTypeAddValue, AlterTableAlgorithm, AlterTableLock, AlterTableOperation, AlterType, AlterTypeAddValue,
AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue,
ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnPolicy, ColumnPolicyProperty, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy,
ConstraintCharacteristics, CreateConnector, CreateDomain, CreateFunction, Deduplicate, ColumnPolicyProperty, ConstraintCharacteristics, CreateConnector, CreateDomain, CreateFunction,
DeferrableInitial, DropBehavior, GeneratedAs, GeneratedExpressionMode, IdentityParameters, Deduplicate, DeferrableInitial, DropBehavior, GeneratedAs, GeneratedExpressionMode,
IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind,
IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, Partition, IdentityPropertyOrder, IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner,
ProcedureParam, ReferentialAction, ReplicaIdentity, TableConstraint, TagsColumnOption, Partition, ProcedureParam, ReferentialAction, ReplicaIdentity, TableConstraint,
UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, TagsColumnOption, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation,
ViewColumnDef,
}; };
pub use self::dml::{CreateIndex, CreateTable, Delete, IndexColumn, Insert}; pub use self::dml::{CreateIndex, CreateTable, Delete, IndexColumn, Insert};
pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::operator::{BinaryOperator, UnaryOperator};

View file

@ -15,7 +15,7 @@
// specific language governing permissions and limitations // specific language governing permissions and limitations
// under the License. // under the License.
use crate::ast::query::SelectItemQualifiedWildcardKind; use crate::ast::{query::SelectItemQualifiedWildcardKind, ColumnOptions};
use core::iter; use core::iter;
use crate::tokenizer::Span; use crate::tokenizer::Span;
@ -991,10 +991,13 @@ impl Spanned for ViewColumnDef {
options, options,
} = self; } = self;
union_spans( name.span.union_opt(&options.as_ref().map(|o| o.span()))
core::iter::once(name.span) }
.chain(options.iter().flat_map(|i| i.iter().map(|k| k.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 { match self {
CreateTableOptions::None => Span::empty(), CreateTableOptions::None => Span::empty(),
CreateTableOptions::With(vec) => union_spans(vec.iter().map(|i| i.span())), 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::Plain(vec) => union_spans(vec.iter().map(|i| i.span())),
CreateTableOptions::TableProperties(vec) => union_spans(vec.iter().map(|i| i.span())), CreateTableOptions::TableProperties(vec) => union_spans(vec.iter().map(|i| i.span())),
} }

View file

@ -1028,6 +1028,10 @@ pub trait Dialect: Debug + Any {
fn supports_set_names(&self) -> bool { fn supports_set_names(&self) -> bool {
false false
} }
fn supports_space_separated_column_options(&self) -> bool {
false
}
} }
/// This represents the operators for which precedence must be defined /// This represents the operators for which precedence must be defined

View file

@ -356,6 +356,10 @@ impl Dialect for SnowflakeDialect {
fn get_reserved_keywords_for_select_item_operator(&self) -> &[Keyword] { fn get_reserved_keywords_for_select_item_operator(&self) -> &[Keyword] {
&RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR &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<Statement, ParserError> { fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result<Statement, ParserError> {

View file

@ -10579,17 +10579,7 @@ impl<'a> Parser<'a> {
/// Parses a column definition within a view. /// Parses a column definition within a view.
fn parse_view_column(&mut self) -> Result<ViewColumnDef, ParserError> { fn parse_view_column(&mut self) -> Result<ViewColumnDef, ParserError> {
let name = self.parse_identifier()?; let name = self.parse_identifier()?;
let options = if (dialect_of!(self is BigQueryDialect | GenericDialect) let options = self.parse_view_column_options()?;
&& 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 data_type = if dialect_of!(self is ClickHouseDialect) { let data_type = if dialect_of!(self is ClickHouseDialect) {
Some(self.parse_data_type()?) Some(self.parse_data_type()?)
} else { } else {
@ -10602,6 +10592,25 @@ impl<'a> Parser<'a> {
}) })
} }
fn parse_view_column_options(&mut self) -> Result<Option<ColumnOptions>, 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. /// Parses a parenthesized comma-separated list of unqualified, possibly quoted identifiers.
/// For example: `(col1, "col 2", ...)` /// For example: `(col1, "col 2", ...)`
pub fn parse_parenthesized_column_list( pub fn parse_parenthesized_column_list(

View file

@ -355,14 +355,16 @@ fn parse_create_view_with_options() {
ViewColumnDef { ViewColumnDef {
name: Ident::new("age"), name: Ident::new("age"),
data_type: None, data_type: None,
options: Some(vec![ColumnOption::Options(vec![SqlOption::KeyValue { options: Some(ColumnOptions::CommaSeparated(vec![ColumnOption::Options(
key: Ident::new("description"), vec![SqlOption::KeyValue {
value: Expr::Value( key: Ident::new("description"),
Value::DoubleQuotedString("field age".to_string()).with_span( value: Expr::Value(
Span::new(Location::new(1, 42), Location::new(1, 52)) Value::DoubleQuotedString("field age".to_string()).with_span(
) Span::new(Location::new(1, 42), Location::new(1, 52))
), )
}])]), ),
}]
)])),
}, },
], ],
columns columns

View file

@ -914,7 +914,7 @@ fn parse_create_view_with_fields_data_types() {
}]), }]),
vec![] vec![]
)), )),
options: None options: None,
}, },
ViewColumnDef { ViewColumnDef {
name: "f".into(), name: "f".into(),
@ -926,7 +926,7 @@ fn parse_create_view_with_fields_data_types() {
}]), }]),
vec![] vec![]
)), )),
options: None options: None,
}, },
] ]
); );

View file

@ -7990,7 +7990,7 @@ fn parse_create_view_with_columns() {
.map(|name| ViewColumnDef { .map(|name| ViewColumnDef {
name, name,
data_type: None, data_type: None,
options: None options: None,
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
); );

View file

@ -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') 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", "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); .verified_stmt(sql);
} }
} }
@ -3133,7 +3133,7 @@ fn view_comment_option_should_be_after_column_list() {
fn parse_view_column_descriptions() { fn parse_view_column_descriptions() {
let sql = "CREATE OR REPLACE VIEW v (a COMMENT 'Comment', b) AS SELECT a, b FROM table1"; 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, .. } => { Statement::CreateView { name, columns, .. } => {
assert_eq!(name.to_string(), "v"); assert_eq!(name.to_string(), "v");
assert_eq!( assert_eq!(
@ -3142,7 +3142,9 @@ fn parse_view_column_descriptions() {
ViewColumnDef { ViewColumnDef {
name: Ident::new("a"), name: Ident::new("a"),
data_type: None, data_type: None,
options: Some(vec![ColumnOption::Comment("Comment".to_string())]), options: Some(ColumnOptions::SpaceSeparated(vec![ColumnOption::Comment(
"Comment".to_string()
)])),
}, },
ViewColumnDef { ViewColumnDef {
name: Ident::new("b"), name: Ident::new("b"),
@ -4165,3 +4167,10 @@ fn test_snowflake_fetch_clause_syntax() {
canonical, 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);
}