From a281171106b1cd28744eb0ec3e642182bf5d49a3 Mon Sep 17 00:00:00 2001 From: finchxxia <13153363548@163.com> Date: Sat, 2 May 2026 12:52:09 +0800 Subject: [PATCH 001/141] Databricks: Add support for `UPDATE SET *` and `INSERT *` in MERGE statements (#2325) Co-authored-by: Ifeanyi Ubah --- src/ast/dml.rs | 56 +++++++++++++++++++++++++++--- src/ast/mod.rs | 6 ++-- src/ast/spans.rs | 28 ++++++++------- src/parser/merge.rs | 68 +++++++++++++++++++++++-------------- tests/sqlparser_bigquery.rs | 4 +-- tests/sqlparser_common.rs | 43 +++++++++++++++++++++-- 6 files changed, 156 insertions(+), 49 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 3e2f7ee09f..c2fe2ffd60 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -649,6 +649,14 @@ pub enum MergeInsertKind { /// ``` /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) Row, + /// The insert expression uses the `*` wildcard to insert all columns. + /// + /// Example: + /// ```sql + /// INSERT * + /// ``` + /// [Databricks](https://docs.databricks.com/en/sql/language-manual/delta-merge-into.html) + Wildcard, } impl Display for MergeInsertKind { @@ -660,6 +668,9 @@ impl Display for MergeInsertKind { MergeInsertKind::Row => { write!(f, "ROW") } + MergeInsertKind::Wildcard => { + write!(f, "*") + } } } } @@ -710,25 +721,62 @@ impl Display for MergeInsertExpr { } } +/// The kind of update used within a `MERGE` statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum MergeUpdateKind { + /// Standard update with explicit assignments. + /// + /// Example: + /// ```sql + /// UPDATE SET quantity = source.quantity, name = source.name + /// ``` + Set(Vec), + /// The `*` wildcard to update all columns from the source. + /// + /// Example: + /// ```sql + /// UPDATE SET * + /// ``` + /// [Databricks](https://docs.databricks.com/en/sql/language-manual/delta-merge-into.html) + Wildcard, +} + +impl Display for MergeUpdateKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MergeUpdateKind::Set(assignments) => { + write!(f, "SET {}", display_comma_separated(assignments)) + } + MergeUpdateKind::Wildcard => { + write!(f, "SET *") + } + } + } +} + /// The expression used to update rows within a `MERGE` statement. /// /// Examples /// ```sql /// UPDATE SET quantity = T.quantity + S.quantity +/// UPDATE SET * /// ``` /// /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) /// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/MERGE.html) +/// [Databricks](https://docs.databricks.com/en/sql/language-manual/delta-merge-into.html) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct MergeUpdateExpr { /// The `UPDATE` token that starts the sub-expression. pub update_token: AttachedToken, - /// The update assiment expressions - pub assignments: Vec, - /// `where_clause` for the update (Oralce specific) + /// The kind of update: explicit assignments or `*` shorthand. + pub kind: MergeUpdateKind, + /// `where_clause` for the update (Oracle specific) pub update_predicate: Option, /// `delete_clause` for the update "delete where" (Oracle specific) pub delete_predicate: Option, @@ -736,7 +784,7 @@ pub struct MergeUpdateExpr { impl Display for MergeUpdateExpr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "SET {}", display_comma_separated(&self.assignments))?; + write!(f, "{}", self.kind)?; if let Some(predicate) = self.update_predicate.as_ref() { write!(f, " WHERE {predicate}")?; } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 886bea26d5..3cb5f80418 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -85,9 +85,9 @@ pub use self::ddl::{ }; pub use self::dml::{ Delete, Insert, Merge, MergeAction, MergeClause, MergeClauseKind, MergeInsertExpr, - MergeInsertKind, MergeUpdateExpr, MultiTableInsertIntoClause, MultiTableInsertType, - MultiTableInsertValue, MultiTableInsertValues, MultiTableInsertWhenClause, OutputClause, - Update, + MergeInsertKind, MergeUpdateExpr, MergeUpdateKind, MultiTableInsertIntoClause, + MultiTableInsertType, MultiTableInsertValue, MultiTableInsertValues, + MultiTableInsertWhenClause, OutputClause, Update, }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 0dc834ba03..f0e3e252f5 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -39,15 +39,15 @@ use super::{ IfStatement, IlikeSelectItem, IndexColumn, Insert, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause, MatchRecognizePattern, Measure, Merge, MergeAction, MergeClause, MergeInsertExpr, - MergeInsertKind, MergeUpdateExpr, NamedParenthesizedList, NamedWindowDefinition, ObjectName, - ObjectNamePart, Offset, OnConflict, OnConflictAction, OnInsert, OpenStatement, OrderBy, - OrderByExpr, OrderByKind, OutputClause, Parens, Partition, PartitionBoundValue, - PivotValueSource, ProjectionSelect, Query, RaiseStatement, RaiseStatementValue, - ReferentialAction, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, - SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, - TableAliasColumnDef, TableConstraint, TableFactor, TableObject, TableOptionsClustered, - TableWithJoins, Update, UpdateTableFromKind, Use, Values, ViewColumnDef, WhileStatement, - WildcardAdditionalOptions, With, WithFill, + MergeInsertKind, MergeUpdateExpr, MergeUpdateKind, NamedParenthesizedList, + NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, + OnInsert, OpenStatement, OrderBy, OrderByExpr, OrderByKind, OutputClause, Parens, Partition, + PartitionBoundValue, PivotValueSource, ProjectionSelect, Query, RaiseStatement, + RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, + ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, + SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, + TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, Values, ViewColumnDef, + WhileStatement, WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -2534,7 +2534,7 @@ impl Spanned for MergeInsertExpr { self.kind_token.0.span, match self.kind { MergeInsertKind::Values(ref values) => values.span(), - MergeInsertKind::Row => Span::empty(), // ~ covered by `kind_token` + MergeInsertKind::Row | MergeInsertKind::Wildcard => Span::empty(), }, ] .into_iter() @@ -2546,9 +2546,13 @@ impl Spanned for MergeInsertExpr { impl Spanned for MergeUpdateExpr { fn span(&self) -> Span { + let kind_span = match &self.kind { + MergeUpdateKind::Set(assignments) => union_spans(assignments.iter().map(Spanned::span)), + MergeUpdateKind::Wildcard => Span::empty(), + }; union_spans( core::iter::once(self.update_token.0.span) - .chain(self.assignments.iter().map(Spanned::span)) + .chain(core::iter::once(kind_span)) .chain(self.update_predicate.iter().map(Spanned::span)) .chain(self.delete_predicate.iter().map(Spanned::span)), ) @@ -2930,7 +2934,7 @@ WHERE id = 1 ); if let MergeAction::Update(MergeUpdateExpr { update_token, - assignments: _, + kind: _, update_predicate: _, delete_predicate: _, }) = &clauses[1].action diff --git a/src/parser/merge.rs b/src/parser/merge.rs index 619be612bf..21e9d6087b 100644 --- a/src/parser/merge.rs +++ b/src/parser/merge.rs @@ -18,11 +18,12 @@ use alloc::{boxed::Box, format, vec, vec::Vec}; use crate::{ ast::{ Merge, MergeAction, MergeClause, MergeClauseKind, MergeInsertExpr, MergeInsertKind, - MergeUpdateExpr, ObjectName, OutputClause, SetExpr, + MergeUpdateExpr, MergeUpdateKind, ObjectName, OutputClause, SetExpr, }, dialect::{BigQueryDialect, GenericDialect, MySqlDialect}, keywords::Keyword, parser::IsOptional, + tokenizer::Token, tokenizer::TokenWithSpan, }; @@ -120,7 +121,11 @@ impl Parser<'_> { let update_token = self.get_current_token().clone(); self.expect_keyword_is(Keyword::SET)?; - let assignments = self.parse_comma_separated(Parser::parse_assignment)?; + let kind = if self.consume_token(&Token::Mul) { + MergeUpdateKind::Wildcard + } else { + MergeUpdateKind::Set(self.parse_comma_separated(Parser::parse_assignment)?) + }; let update_predicate = if self.parse_keyword(Keyword::WHERE) { Some(self.parse_expr()?) } else { @@ -134,7 +139,7 @@ impl Parser<'_> { }; MergeAction::Update(MergeUpdateExpr { update_token: update_token.into(), - assignments, + kind, update_predicate, delete_predicate, }) @@ -167,32 +172,43 @@ impl Parser<'_> { }; let insert_token = self.get_current_token().clone(); - let is_mysql = dialect_of!(self is MySqlDialect); - let columns = self.parse_merge_clause_insert_columns(is_mysql)?; - let (kind, kind_token) = if dialect_of!(self is BigQueryDialect | GenericDialect) - && self.parse_keyword(Keyword::ROW) - { - (MergeInsertKind::Row, self.get_current_token().clone()) - } else { - self.expect_keyword_is(Keyword::VALUES)?; - let values_token = self.get_current_token().clone(); - let values = self.parse_values(is_mysql, false)?; - (MergeInsertKind::Values(values), values_token) - }; - let insert_predicate = if self.parse_keyword(Keyword::WHERE) { - Some(self.parse_expr()?) + if self.consume_token(&Token::Mul) { + let star_token = self.get_current_token().clone(); + MergeAction::Insert(MergeInsertExpr { + insert_token: insert_token.into(), + columns: vec![], + kind_token: star_token.into(), + kind: MergeInsertKind::Wildcard, + insert_predicate: None, + }) } else { - None - }; + let is_mysql = dialect_of!(self is MySqlDialect); + let columns = self.parse_merge_clause_insert_columns(is_mysql)?; + let (kind, kind_token) = if dialect_of!(self is BigQueryDialect | GenericDialect) + && self.parse_keyword(Keyword::ROW) + { + (MergeInsertKind::Row, self.get_current_token().clone()) + } else { + self.expect_keyword_is(Keyword::VALUES)?; + let values_token = self.get_current_token().clone(); + let values = self.parse_values(is_mysql, false)?; + (MergeInsertKind::Values(values), values_token) + }; + let insert_predicate = if self.parse_keyword(Keyword::WHERE) { + Some(self.parse_expr()?) + } else { + None + }; - MergeAction::Insert(MergeInsertExpr { - insert_token: insert_token.into(), - columns, - kind_token: kind_token.into(), - kind, - insert_predicate, - }) + MergeAction::Insert(MergeInsertExpr { + insert_token: insert_token.into(), + columns, + kind_token: kind_token.into(), + kind, + insert_predicate, + }) + } } _ => { return parser_err!( diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 4bdb54f74f..afb6190822 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1830,7 +1830,7 @@ fn parse_merge() { }); let update_action = MergeAction::Update(MergeUpdateExpr { update_token: AttachedToken::empty(), - assignments: vec![ + kind: MergeUpdateKind::Set(vec![ Assignment { target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new("a")])), value: Expr::value(number("1")), @@ -1839,7 +1839,7 @@ fn parse_merge() { target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new("b")])), value: Expr::value(number("2")), }, - ], + ]), update_predicate: None, delete_predicate: None, }); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 221c88971a..739238c822 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10157,7 +10157,7 @@ fn parse_merge() { }), action: MergeAction::Update(MergeUpdateExpr { update_token: AttachedToken::empty(), - assignments: vec![ + kind: MergeUpdateKind::Set(vec![ Assignment { target: AssignmentTarget::ColumnName(ObjectName::from(vec![ Ident::new("dest"), @@ -10178,7 +10178,7 @@ fn parse_merge() { Ident::new("G"), ]), }, - ], + ]), update_predicate: None, delete_predicate: None, }), @@ -10239,6 +10239,45 @@ WHEN NOT MATCHED THEN \ INSERT (PLAYGROUND.FOO.ID, PLAYGROUND.FOO.NAME) \ VALUES (1, 'abc')"; all_dialects().verified_stmt(sql); + + // MERGE with wildcard (UPDATE SET * and INSERT *) + let sql = "MERGE INTO target USING source ON target.id = source.id WHEN MATCHED THEN UPDATE SET * WHEN NOT MATCHED THEN INSERT *"; + match verified_stmt(sql) { + Statement::Merge(merge) => { + assert_eq!(merge.clauses.len(), 2); + + match &merge.clauses[0].action { + MergeAction::Update(update_expr) => { + assert!(matches!(update_expr.kind, MergeUpdateKind::Wildcard)); + } + _ => panic!("Expected UPDATE action"), + } + + match &merge.clauses[1].action { + MergeAction::Insert(insert_expr) => { + assert!(matches!(insert_expr.kind, MergeInsertKind::Wildcard)); + assert!(insert_expr.columns.is_empty()); + } + _ => panic!("Expected INSERT action"), + } + } + _ => panic!("Expected MERGE statement"), + } + + verified_stmt("MERGE INTO target USING source ON target.id = source.id WHEN MATCHED AND source.active = 1 THEN UPDATE SET *"); + + verified_stmt("MERGE INTO target USING source ON target.id = source.id WHEN NOT MATCHED BY TARGET THEN INSERT *"); + + verified_stmt("MERGE INTO target USING source ON target.id = source.id WHEN MATCHED THEN UPDATE SET * WHEN NOT MATCHED THEN INSERT (a, b) VALUES (source.a, source.b)"); + + let sql = concat!( + "MERGE INTO t1 AS target ", + "USING (SELECT * FROM t2) AS source ", + "ON target.id = source.id ", + "WHEN MATCHED THEN UPDATE SET * ", + "WHEN NOT MATCHED THEN INSERT *" + ); + verified_stmt(sql); } #[test] From 824b5c1530a15ad6f16f0c164d3a1394b44c4276 Mon Sep 17 00:00:00 2001 From: Corey Fritz Date: Wed, 6 May 2026 04:49:59 -0600 Subject: [PATCH 002/141] Snowflake: Fix COPY INTO transformation parsing for cast expressions (#2328) --- src/dialect/snowflake.rs | 6 ++++++ tests/sqlparser_snowflake.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 1ac21d0073..fda5b7b976 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -1545,6 +1545,12 @@ fn parse_select_item_for_data_load( } } + // A trailing `::` means this is a cast expression (e.g. + // `$1:"col"::NUMBER(38,0)`), not a stage-load-select-item. + if matches!(parser.peek_token_ref().token, Token::DoubleColon) { + return parser.expected("stage load select item", parser.peek_token()); + } + // as if parser.parse_keyword(Keyword::AS) { item_as = Some(match parser.next_token().token { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 790bf15151..32a55d5002 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2447,6 +2447,35 @@ fn test_copy_into_with_transformations() { snowflake().parse_sql_statements(sql1).unwrap(); } +#[test] +fn test_copy_into_with_cast_transformation() { + let variants = [ + concat!( + "COPY INTO my_company.emp_basic (a) FROM ", + r#"(SELECT $1:"A"::NUMBER(38, 0) FROM @stg)"#, + ), + concat!( + "COPY INTO my_company.emp_basic (a) FROM ", + "(SELECT $1::NUMBER(38, 0) FROM @stg)", + ), + concat!( + "COPY INTO my_company.emp_basic (a) FROM ", + "(SELECT $1:SEQUENCE::NUMBER(38, 0) FROM @stg)", + ), + concat!( + "COPY INTO my_company.emp_basic (a, b) FROM ", + r#"(SELECT $1:"A"::VARIANT, $1:"B"::TEXT FROM @stg)"#, + ), + concat!( + "COPY INTO my_company.emp_basic (a, b) FROM ", + r#"(SELECT t.$1:plain AS plain, $1:"B"::TEXT FROM @stg AS t)"#, + ), + ]; + for sql in variants { + snowflake().verified_stmt(sql); + } +} + #[test] fn test_copy_into_file_format() { let sql = concat!( From 299f54df9ac042b3b55716603ad4ff728d7eab52 Mon Sep 17 00:00:00 2001 From: Corey Fritz Date: Wed, 6 May 2026 04:50:20 -0600 Subject: [PATCH 003/141] Snowflake: Accept COPY GRANTS after CREATE VIEW column list (#2327) --- src/parser/mod.rs | 8 +++++++- tests/sqlparser_snowflake.rs | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 668c520e5e..612cc9be47 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6533,10 +6533,16 @@ impl<'a> Parser<'a> { let name_before_not_exists = !if_not_exists_first && self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let if_not_exists = if_not_exists_first || name_before_not_exists; - let copy_grants = self.parse_keywords(&[Keyword::COPY, Keyword::GRANTS]); + let mut copy_grants = self.parse_keywords(&[Keyword::COPY, Keyword::GRANTS]); // Many dialects support `OR ALTER` right after `CREATE`, but we don't (yet). // ANSI SQL and Postgres support RECURSIVE here, but we don't support it either. let columns = self.parse_view_columns()?; + // Snowflake also documents `COPY GRANTS` *after* the column list; accept + // either position, but not both. + // + if !copy_grants { + copy_grants = self.parse_keywords(&[Keyword::COPY, Keyword::GRANTS]); + } let mut options = CreateTableOptions::None; let with_options = self.parse_options(Keyword::WITH)?; if !with_options.is_empty() { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 32a55d5002..75cf8ebbcb 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4793,6 +4793,28 @@ fn test_snowflake_create_view_copy_grants() { ); } +#[test] +fn test_snowflake_create_view_copy_grants_after_columns() { + let cases = [ + ( + "CREATE OR REPLACE VIEW v (a, b) COPY GRANTS AS SELECT a, b FROM t", + "CREATE OR REPLACE VIEW v COPY GRANTS (a, b) AS SELECT a, b FROM t", + ), + ( + "CREATE OR REPLACE SECURE VIEW v (a, b) COPY GRANTS AS SELECT a, b FROM t", + "CREATE OR REPLACE SECURE VIEW v COPY GRANTS (a, b) AS SELECT a, b FROM t", + ), + ( + "CREATE MATERIALIZED VIEW v (a) COPY GRANTS AS SELECT a FROM t", + "CREATE MATERIALIZED VIEW v COPY GRANTS (a) AS SELECT a FROM t", + ), + ]; + for (sql, parsed) in cases { + snowflake().one_statement_parses_to(sql, parsed); + } + snowflake().verified_stmt("CREATE OR REPLACE VIEW v (a) AS SELECT a FROM t"); +} + #[test] fn test_snowflake_identifier_function() { // Using IDENTIFIER to reference a column From 3f347e38c0b3290f90cb853f8b6d37c509bdc2f6 Mon Sep 17 00:00:00 2001 From: Mrmaxmeier <3913977+Mrmaxmeier@users.noreply.github.com> Date: Wed, 6 May 2026 12:57:11 +0200 Subject: [PATCH 004/141] Fix panic in EscapeQuotedString and parse_flush, clean up a few unwraps (#2330) --- src/ast/ddl.rs | 3 +-- src/ast/helpers/stmt_data_loading.rs | 16 ++++++-------- src/ast/mod.rs | 31 +++++++++++----------------- src/ast/query.rs | 15 +++++++------- src/ast/value.rs | 12 ++++++++++- src/parser/mod.rs | 2 +- 6 files changed, 38 insertions(+), 41 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 67aefb3928..067f4b8df3 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2113,8 +2113,7 @@ impl fmt::Display for ColumnOption { GeneratedAs::ExpStored => "", }; write!(f, "GENERATED {when} AS IDENTITY")?; - if sequence_options.is_some() { - let so = sequence_options.as_ref().unwrap(); + if let Some(so) = sequence_options { if !so.is_empty() { write!(f, " (")?; } diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs index dfc1f4b0bf..5e997bac70 100644 --- a/src/ast/helpers/stmt_data_loading.rs +++ b/src/ast/helpers/stmt_data_loading.rs @@ -86,18 +86,14 @@ pub struct StageLoadSelectItem { impl fmt::Display for StageParamsObject { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let url = &self.url.as_ref(); - let storage_integration = &self.storage_integration.as_ref(); - let endpoint = &self.endpoint.as_ref(); - - if url.is_some() { - write!(f, " URL='{}'", url.unwrap())?; + if let Some(ref url) = self.url { + write!(f, " URL='{url}'")?; } - if storage_integration.is_some() { - write!(f, " STORAGE_INTEGRATION={}", storage_integration.unwrap())?; + if let Some(ref storage_integration) = self.storage_integration { + write!(f, " STORAGE_INTEGRATION={storage_integration}")?; } - if endpoint.is_some() { - write!(f, " ENDPOINT='{}'", endpoint.unwrap())?; + if let Some(ref endpoint) = self.endpoint { + write!(f, " ENDPOINT='{endpoint}'")?; } if !self.credentials.options.is_empty() { write!(f, " CREDENTIALS=({})", self.credentials)?; diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 3cb5f80418..ad2b244b36 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1853,16 +1853,10 @@ impl fmt::Display for Expr { negated, } => { let not_ = if *negated { "NOT " } else { "" }; - if form.is_none() { - write!(f, "{expr} IS {not_}NORMALIZED") + if let Some(form) = form { + write!(f, "{} IS {}{} NORMALIZED", expr, not_, form) } else { - write!( - f, - "{} IS {}{} NORMALIZED", - expr, - not_, - form.as_ref().unwrap() - ) + write!(f, "{expr} IS {not_}NORMALIZED") } } Expr::SimilarTo { @@ -5741,8 +5735,8 @@ impl fmt::Display for Statement { write!(f, " SESSION")?; } write!(f, " STATUS")?; - if filter.is_some() { - write!(f, " {}", filter.as_ref().unwrap())?; + if let Some(filter) = filter { + write!(f, " {}", filter)?; } Ok(()) } @@ -5759,8 +5753,8 @@ impl fmt::Display for Statement { write!(f, " SESSION")?; } write!(f, " VARIABLES")?; - if filter.is_some() { - write!(f, " {}", filter.as_ref().unwrap())?; + if let Some(filter) = filter { + write!(f, " {}", filter)?; } Ok(()) } @@ -6172,8 +6166,8 @@ impl fmt::Display for Statement { if !copy_options.options.is_empty() { write!(f, " COPY_OPTIONS=({copy_options})")?; } - if comment.is_some() { - write!(f, " COMMENT='{}'", comment.as_ref().unwrap())?; + if let Some(comment) = comment { + write!(f, " COMMENT='{}'", comment)?; } Ok(()) } @@ -6260,12 +6254,11 @@ impl fmt::Display for Statement { } Statement::Pragma { name, value, is_eq } => { write!(f, "PRAGMA {name}")?; - if value.is_some() { - let val = value.as_ref().unwrap(); + if let Some(value) = value { if *is_eq { - write!(f, " = {val}")?; + write!(f, " = {value}")?; } else { - write!(f, "({val})")?; + write!(f, "({value})")?; } } Ok(()) diff --git a/src/ast/query.rs b/src/ast/query.rs index bbdd7540af..35f3288a8e 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -307,15 +307,14 @@ pub struct Table { impl fmt::Display for Table { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(ref schema_name) = self.schema_name { - write!( - f, - "TABLE {}.{}", - schema_name, - self.table_name.as_ref().unwrap(), - )?; + if let Some(ref table_name) = self.table_name { + if let Some(ref schema_name) = self.schema_name { + write!(f, "TABLE {}.{}", schema_name, table_name,)?; + } else { + write!(f, "TABLE {}", table_name)?; + } } else { - write!(f, "TABLE {}", self.table_name.as_ref().unwrap(),)?; + write!(f, "TABLE")?; } Ok(()) } diff --git a/src/ast/value.rs b/src/ast/value.rs index 5f069f36cc..ade7decf9a 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -577,7 +577,8 @@ impl fmt::Display for EscapeQuotedString<'_> { // The quote is not escaped. // Including idx in the range, so the quote at idx will be printed twice: // in this call to write_str() and in the next one. - f.write_str(&self.string[start_idx..=idx])?; + let end_idx = idx + ch.len_utf8(); + f.write_str(&self.string[start_idx..end_idx])?; start_idx = idx; } } @@ -707,3 +708,12 @@ impl fmt::Display for TrimWhereField { }) } } + +#[cfg(test)] +#[test] +fn test_escape_quoted_string_with_multibyte_quote_char() { + assert_eq!( + format!("{}", escape_quoted_string("a🦀b🦀c", '🦀')), + "a🦀🦀b🦀🦀c" + ); +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 612cc9be47..3d84891011 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1009,7 +1009,7 @@ impl<'a> Parser<'a> { FlushType::OptimizerCosts } else if self.parse_keywords(&[Keyword::RELAY, Keyword::LOGS]) { if self.parse_keywords(&[Keyword::FOR, Keyword::CHANNEL]) { - channel = Some(self.parse_object_name(false).unwrap().to_string()); + channel = Some(self.parse_object_name(false)?.to_string()); } FlushType::RelayLogs } else if self.parse_keywords(&[Keyword::SLOW, Keyword::LOGS]) { From f98c9d151170a86eacc57066c598dfbca3b304df Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Wed, 6 May 2026 13:01:27 +0200 Subject: [PATCH 005/141] [Teradata] Add `CREATE TABLE` options (#2329) --- src/ast/ddl.rs | 59 ++++++++++++++++++++++- src/ast/helpers/stmt_create_table.rs | 33 ++++++++++++- src/ast/mod.rs | 2 +- src/ast/spans.rs | 3 ++ src/dialect/mod.rs | 10 ++++ src/dialect/teradata.rs | 5 ++ src/keywords.rs | 1 + src/parser/mod.rs | 70 +++++++++++++++++++++++++++- tests/sqlparser_duckdb.rs | 3 ++ tests/sqlparser_mssql.rs | 6 +++ tests/sqlparser_postgres.rs | 3 ++ tests/sqlparser_snowflake.rs | 7 --- tests/sqlparser_teradata.rs | 62 +++++++++++++++++++++++- 13 files changed, 251 insertions(+), 13 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 067f4b8df3..62a8a71a64 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3059,6 +3059,20 @@ pub struct CreateTable { /// Redshift `BACKUP` option: `BACKUP { YES | NO }` /// pub backup: Option, + /// `MULTISET | SET` table-kind prefix. + /// `Some(true)` => `MULTISET`, `Some(false)` => `SET`. + /// + /// [Teradata](https://docs.teradata.com/r/Enterprise_IntelliFlex_VMware/SQL-Data-Definition-Language-Syntax-and-Examples/Table-Statements/CREATE-TABLE-and-CREATE-TABLE-AS/Syntax-Elements/MULTISET-or-SET) + pub multiset: Option, + /// `FALLBACK` clause. + /// `Some(true)` => `FALLBACK`, `Some(false)` => `NO FALLBACK` + /// + /// [Teradata](https://docs.teradata.com/r/Enterprise_IntelliFlex_VMware/SQL-Data-Definition-Language-Syntax-and-Examples/Table-Statements/CREATE-TABLE-and-CREATE-TABLE-AS/Syntax-Elements/FALLBACK-or-NO-FALLBACK) + pub fallback: Option, + /// `WITH DATA` clause on a `CREATE TABLE ... AS` statement. + /// + /// [Teradata](https://docs.teradata.com/r/Enterprise_IntelliFlex_VMware/SQL-Data-Definition-Language-Syntax-and-Examples/Table-Statements/CREATE-TABLE-and-CREATE-TABLE-AS/Syntax-Elements/AS_clause/WITH-Clause-Phrase) + pub with_data: Option, } impl fmt::Display for CreateTable { @@ -3072,7 +3086,7 @@ impl fmt::Display for CreateTable { // `CREATE TABLE t (a INT) AS SELECT a from t2` write!( f, - "CREATE {or_replace}{external}{global}{temporary}{transient}{volatile}{dynamic}{iceberg}{snapshot}TABLE {if_not_exists}{name}", + "CREATE {or_replace}{external}{global}{multiset}{temporary}{transient}{volatile}{dynamic}{iceberg}{snapshot}TABLE {if_not_exists}{name}", or_replace = if self.or_replace { "OR REPLACE " } else { "" }, external = if self.external { "EXTERNAL " } else { "" }, snapshot = if self.snapshot { "SNAPSHOT " } else { "" }, @@ -3086,14 +3100,20 @@ impl fmt::Display for CreateTable { }) .unwrap_or(""), if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" }, + multiset = self + .multiset + .map(|m| if m { "MULTISET " } else { "SET " }) + .unwrap_or(""), temporary = if self.temporary { "TEMPORARY " } else { "" }, transient = if self.transient { "TRANSIENT " } else { "" }, volatile = if self.volatile { "VOLATILE " } else { "" }, - // Only for Snowflake iceberg = if self.iceberg { "ICEBERG " } else { "" }, dynamic = if self.dynamic { "DYNAMIC " } else { "" }, name = self.name, )?; + if let Some(fallback) = self.fallback { + write!(f, ", {}", if fallback { "FALLBACK" } else { "NO FALLBACK" })?; + } if let Some(partition_of) = &self.partition_of { write!(f, " PARTITION OF {partition_of}")?; } @@ -3378,6 +3398,41 @@ impl fmt::Display for CreateTable { if let Some(query) = &self.query { write!(f, " AS {query}")?; } + if let Some(with_data) = &self.with_data { + write!(f, " {with_data}")?; + } + Ok(()) + } +} + +/// `WITH DATA` clause on `CREATE TABLE ... AS` statement. +/// +/// [Teradata](https://docs.teradata.com/r/Enterprise_IntelliFlex_VMware/SQL-Data-Definition-Language-Syntax-and-Examples/Table-Statements/CREATE-TABLE-and-CREATE-TABLE-AS/Syntax-Elements/AS_clause/WITH-Clause-Phrase) +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct WithData { + /// `true` for `WITH DATA`, `false` for `WITH NO DATA`. + pub data: bool, + /// `Some(true)` for `AND STATISTICS`, `Some(false)` for `AND NO STATISTICS`, + /// `None` if the `AND [NO] STATISTICS` sub-clause is omitted. + pub statistics: Option, +} + +impl fmt::Display for WithData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("WITH ")?; + if !self.data { + f.write_str("NO ")?; + } + f.write_str("DATA")?; + if let Some(stats) = self.statistics { + f.write_str(" AND ")?; + if !stats { + f.write_str("NO ")?; + } + f.write_str("STATISTICS")?; + } Ok(()) } } diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index ab2feb6930..fc81d3b861 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -29,7 +29,7 @@ use crate::ast::{ DistStyle, Expr, FileFormat, ForValues, HiveDistributionStyle, HiveFormat, Ident, InitializeKind, ObjectName, OnCommit, OneOrManyWithParens, Query, RefreshModeKind, RowAccessPolicy, Statement, StorageLifecyclePolicy, StorageSerializationPolicy, - TableConstraint, TableVersion, Tag, WrappedCollection, + TableConstraint, TableVersion, Tag, WithData, WrappedCollection, }; use crate::parser::ParserError; @@ -183,6 +183,12 @@ pub struct CreateTableBuilder { pub sortkey: Option>, /// Redshift `BACKUP` option. pub backup: Option, + /// `MULTISET | SET` table-kind prefix. + pub multiset: Option, + /// `FALLBACK` clause. + pub fallback: Option, + /// `WITH DATA` clause. + pub with_data: Option, } impl CreateTableBuilder { @@ -248,6 +254,9 @@ impl CreateTableBuilder { distkey: None, sortkey: None, backup: None, + multiset: None, + fallback: None, + with_data: None, } } /// Set `OR REPLACE` for the CREATE TABLE statement. @@ -556,6 +565,22 @@ impl CreateTableBuilder { self.backup = backup; self } + /// Set `MULTISET | SET` table-kind prefix. + /// Some(true) => `MULTISET`, Some(false) => `SET`. + pub fn multiset(mut self, multiset: Option) -> Self { + self.multiset = multiset; + self + } + /// Set `FALLBACK` / `NO FALLBACK` flag. + pub fn fallback(mut self, fallback: Option) -> Self { + self.fallback = fallback; + self + } + /// Set `WITH DATA` clause. + pub fn with_data(mut self, with_data: Option) -> Self { + self.with_data = with_data; + self + } /// Consume the builder and produce a `CreateTable`. pub fn build(self) -> CreateTable { CreateTable { @@ -618,6 +643,9 @@ impl CreateTableBuilder { distkey: self.distkey, sortkey: self.sortkey, backup: self.backup, + multiset: self.multiset, + fallback: self.fallback, + with_data: self.with_data, } } } @@ -699,6 +727,9 @@ impl From for CreateTableBuilder { distkey: table.distkey, sortkey: table.sortkey, backup: table.backup, + multiset: table.multiset, + fallback: table.fallback, + with_data: table.with_data, } } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ad2b244b36..758bc6a6be 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -81,7 +81,7 @@ pub use self::ddl::{ PartitionBoundValue, ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, TagsColumnOption, TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, - UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, + UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, WithData, }; pub use self::dml::{ Delete, Insert, Merge, MergeAction, MergeClause, MergeClauseKind, MergeInsertExpr, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index f0e3e252f5..343d99e736 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -604,6 +604,9 @@ impl Spanned for CreateTable { distkey: _, sortkey: _, backup: _, + multiset: _, + fallback: _, + with_data: _, } = self; union_spans( diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 6ab6cb15e4..84032847af 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1224,6 +1224,16 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect accepts a comma-separated list of table-level + /// options placed between the table name and the column-list parenthesis, e.g. + /// + /// ```sql + /// CREATE TABLE foo, NO FALLBACK, NO BEFORE JOURNAL (col INTEGER) + /// ``` + fn supports_leading_comma_before_table_options(&self) -> bool { + false + } + /// Returns true if the dialect supports PartiQL for querying semi-structured data /// fn supports_partiql(&self) -> bool { diff --git a/src/dialect/teradata.rs b/src/dialect/teradata.rs index e88a400754..c8470cb642 100644 --- a/src/dialect/teradata.rs +++ b/src/dialect/teradata.rs @@ -89,4 +89,9 @@ impl Dialect for TeradataDialect { fn supports_string_literal_concatenation(&self) -> bool { true } + + /// See + fn supports_leading_comma_before_table_options(&self) -> bool { + true + } } diff --git a/src/keywords.rs b/src/keywords.rs index 4fc8f72d1d..a0a65be689 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -414,6 +414,7 @@ define_keywords!( FACTS, FAIL, FAILOVER, + FALLBACK, FALSE, FAMILY, FETCH, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3d84891011..76616bc368 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5124,6 +5124,7 @@ impl<'a> Parser<'a> { pub fn parse_create(&mut self) -> Result { let or_replace = self.parse_keywords(&[Keyword::OR, Keyword::REPLACE]); let or_alter = self.parse_keywords(&[Keyword::OR, Keyword::ALTER]); + let multiset = self.maybe_parse_multiset(); let local = self.parse_one_of_keywords(&[Keyword::LOCAL]).is_some(); let global = self.parse_one_of_keywords(&[Keyword::GLOBAL]).is_some(); let transient = self.parse_one_of_keywords(&[Keyword::TRANSIENT]).is_some(); @@ -5137,13 +5138,14 @@ impl<'a> Parser<'a> { let temporary = self .parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY]) .is_some(); + let volatile = self.parse_keyword(Keyword::VOLATILE); let persistent = dialect_of!(self is DuckDbDialect) && self.parse_one_of_keywords(&[Keyword::PERSISTENT]).is_some(); let create_view_params = self.parse_create_view_params()?; if self.peek_keywords(&[Keyword::SNAPSHOT, Keyword::TABLE]) { self.parse_create_snapshot_table().map(Into::into) } else if self.parse_keyword(Keyword::TABLE) { - self.parse_create_table(or_replace, temporary, global, transient) + self.parse_create_table(or_replace, temporary, global, transient, volatile, multiset) .map(Into::into) } else if self.peek_keyword(Keyword::MATERIALIZED) || self.peek_keyword(Keyword::VIEW) @@ -8476,11 +8478,25 @@ impl<'a> Parser<'a> { temporary: bool, global: Option, transient: bool, + volatile: bool, + multiset: Option, ) -> Result { let allow_unquoted_hyphen = dialect_of!(self is BigQueryDialect); let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let table_name = self.parse_object_name(allow_unquoted_hyphen)?; + let fallback = if self.dialect.supports_leading_comma_before_table_options() + && self.consume_token(&Token::Comma) + { + let fallback = self.maybe_parse_fallback()?; + if fallback.is_none() { + self.prev_token(); // Put back comma. + } + fallback + } else { + None + }; + // PostgreSQL PARTITION OF for child partition tables // Note: This is a PostgreSQL-specific feature, but the dialect check was intentionally // removed to allow GenericDialect and other dialects to parse this syntax. This enables @@ -8632,6 +8648,13 @@ impl<'a> Parser<'a> { None }; + // `WITH DATA` clause only applies if there is a query body. + let with_data = if query.is_some() { + self.maybe_parse_with_data()? + } else { + None + }; + Ok(CreateTableBuilder::new(table_name) .temporary(temporary) .columns(columns) @@ -8639,6 +8662,9 @@ impl<'a> Parser<'a> { .or_replace(or_replace) .if_not_exists(if_not_exists) .transient(transient) + .volatile(volatile) + .multiset(multiset) + .fallback(fallback) .hive_distribution(hive_distribution) .hive_formats(hive_formats) .global(global) @@ -8658,6 +8684,7 @@ impl<'a> Parser<'a> { .for_values(for_values) .table_options(create_table_config.table_options) .primary_key(primary_key) + .with_data(with_data) .strict(strict) .backup(backup) .diststyle(diststyle) @@ -8666,6 +8693,47 @@ impl<'a> Parser<'a> { .build()) } + /// Parse `MULTISET` table-kind prefix on `CREATE TABLE`. + fn maybe_parse_multiset(&mut self) -> Option { + match self.parse_one_of_keywords(&[Keyword::SET, Keyword::MULTISET]) { + Some(Keyword::MULTISET) => Some(true), + Some(Keyword::SET) => Some(false), + _ => None, + } + } + + /// Parse `FALLBACK` option on a `CREATE TABLE` statement, + fn maybe_parse_fallback(&mut self) -> Result, ParserError> { + if self.parse_keywords(&[Keyword::NO, Keyword::FALLBACK]) { + Ok(Some(false)) + } else if self.parse_keyword(Keyword::FALLBACK) { + Ok(Some(true)) + } else { + Ok(None) + } + } + + /// Parse [`WithData`] clause on `CREATE TABLE ... AS` statement. + fn maybe_parse_with_data(&mut self) -> Result, ParserError> { + let data = if self.parse_keywords(&[Keyword::WITH, Keyword::DATA]) { + true + } else if self.parse_keywords(&[Keyword::WITH, Keyword::NO, Keyword::DATA]) { + false + } else { + return Ok(None); + }; + + let statistics = if self.parse_keywords(&[Keyword::AND, Keyword::STATISTICS]) { + Some(true) + } else if self.parse_keywords(&[Keyword::AND, Keyword::NO, Keyword::STATISTICS]) { + Some(false) + } else { + None + }; + + Ok(Some(WithData { data, statistics })) + } + fn maybe_parse_create_table_like( &mut self, allow_unquoted_hyphen: bool, diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index df62685808..548ad27cf3 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -794,6 +794,9 @@ fn test_duckdb_union_datatype() { distkey: Default::default(), sortkey: Default::default(), backup: Default::default(), + multiset: Default::default(), + fallback: Default::default(), + with_data: Default::default(), }), stmt ); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 1e053da78c..d784c74ae5 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2013,6 +2013,9 @@ fn parse_create_table_with_valid_options() { distkey: None, sortkey: None, backup: None, + multiset: None, + fallback: None, + with_data: None, }) ); } @@ -2187,6 +2190,9 @@ fn parse_create_table_with_identity_column() { distkey: None, sortkey: None, backup: None, + multiset: None, + fallback: None, + with_data: None, }), ); } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 86315b1ef9..87d17d1b46 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6716,6 +6716,9 @@ fn parse_trigger_related_functions() { distkey: None, sortkey: None, backup: None, + multiset: None, + fallback: None, + with_data: None, } ); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 75cf8ebbcb..e75dcbf786 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -471,13 +471,6 @@ fn test_snowflake_create_invalid_temporal_table() { )) ); - assert_eq!( - snowflake().parse_sql_statements("CREATE TEMP VOLATILE TABLE my_table (a INT)"), - Err(ParserError::ParserError( - "Expected: an object type after CREATE, found: VOLATILE".to_string() - )) - ); - assert_eq!( snowflake().parse_sql_statements("CREATE TEMP TRANSIENT TABLE my_table (a INT)"), Err(ParserError::ParserError( diff --git a/tests/sqlparser_teradata.rs b/tests/sqlparser_teradata.rs index b8e6a53ce3..a4e66af0d9 100644 --- a/tests/sqlparser_teradata.rs +++ b/tests/sqlparser_teradata.rs @@ -17,7 +17,8 @@ //! Test SQL syntax, specific to [sqlparser::dialect::TeradataDialect]. -use sqlparser::dialect::{Dialect, TeradataDialect}; +use sqlparser::dialect::{Dialect, GenericDialect, TeradataDialect}; +use sqlparser::test_utils::all_dialects_where; use test_utils::TestedDialects; mod test_utils; @@ -26,6 +27,10 @@ fn teradata() -> TestedDialects { TestedDialects::new(vec![Box::new(TeradataDialect)]) } +fn teradata_and_generic() -> TestedDialects { + TestedDialects::new(vec![Box::new(TeradataDialect), Box::new(GenericDialect {})]) +} + #[test] fn dialect_methods() { let d: &dyn Dialect = &TeradataDialect; @@ -45,6 +50,7 @@ fn dialect_methods() { assert!(d.supports_top_before_distinct()); assert!(d.supports_window_function_null_treatment_arg()); assert!(d.supports_string_literal_concatenation()); + assert!(d.supports_leading_comma_before_table_options()); } #[test] @@ -61,3 +67,57 @@ fn parse_identifier() { r#"NULL AS "quoted id""# )); } + +#[test] +fn parse_create_table_multiset() { + teradata_and_generic().verified_stmt("CREATE MULTISET TABLE foo (id INT)"); + teradata_and_generic().verified_stmt("CREATE SET TABLE foo (id INT)"); +} + +#[test] +fn parse_create_table_volatile() { + teradata_and_generic().verified_stmt("CREATE VOLATILE TABLE foo (id INT)"); + teradata_and_generic().verified_stmt("CREATE MULTISET VOLATILE TABLE foo (id INT)"); +} + +#[test] +fn parse_create_table_fallback() { + teradata().verified_stmt("CREATE TABLE foo, FALLBACK (id INT)"); + teradata().verified_stmt("CREATE TABLE foo, NO FALLBACK (id INT)"); + teradata().verified_stmt("CREATE MULTISET TABLE foo, NO FALLBACK (id INT)"); +} + +#[test] +fn parse_create_table_as_with_data() { + teradata_and_generic().verified_stmt("CREATE TABLE foo AS (SELECT 1 AS a) WITH DATA"); + teradata_and_generic().verified_stmt("CREATE TABLE foo AS (SELECT 1 AS a) WITH NO DATA"); + teradata_and_generic() + .verified_stmt("CREATE TABLE foo AS (SELECT 1 AS a) WITH DATA AND STATISTICS"); + teradata_and_generic() + .verified_stmt("CREATE TABLE foo AS (SELECT 1 AS a) WITH DATA AND NO STATISTICS"); + teradata_and_generic() + .verified_stmt("CREATE TABLE foo AS (SELECT 1 AS a) WITH NO DATA AND STATISTICS"); + teradata_and_generic() + .verified_stmt("CREATE TABLE foo AS (SELECT 1 AS a) WITH NO DATA AND NO STATISTICS"); +} + +#[test] +fn parse_create_table_options() { + teradata().verified_stmt(concat!( + "CREATE MULTISET VOLATILE TABLE foo, NO FALLBACK ", + "(id INT, name VARCHAR(100)) ", + "ON COMMIT PRESERVE ROWS" + )); +} + +#[test] +fn parse_leading_comma_before_table_options() { + let dialect = all_dialects_where(|d| d.supports_leading_comma_before_table_options()); + dialect.verified_stmt("CREATE TABLE foo, FALLBACK (id INT)"); + + let unsupported_dialects = + all_dialects_where(|d| !d.supports_leading_comma_before_table_options()); + assert!(unsupported_dialects + .parse_sql_statements("CREATE TABLE foo, FALLBACK (id INT)") + .is_err()); +} From 79f06bd258ae9e6b91980821af49d066d435e9f2 Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Wed, 6 May 2026 13:03:44 +0200 Subject: [PATCH 006/141] Add support for PostgreSQL's ORDER BY ... USING clause (#2246) --- src/ast/mod.rs | 2 +- src/ast/query.rs | 44 ++++++++++--- src/dialect/mod.rs | 8 +++ src/dialect/postgresql.rs | 4 ++ src/parser/mod.rs | 51 +++++++++++--- tests/sqlparser_bigquery.rs | 4 +- tests/sqlparser_clickhouse.rs | 6 +- tests/sqlparser_common.rs | 121 +++++++++++++++++++++++++--------- tests/sqlparser_hive.rs | 7 +- tests/sqlparser_mysql.rs | 8 +-- tests/sqlparser_postgres.rs | 4 +- 11 files changed, 197 insertions(+), 62 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 758bc6a6be..c0826f2008 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -99,7 +99,7 @@ pub use self::query::{ JsonTableNestedColumn, LateralView, LimitClause, LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, OrderByOptions, - PipeOperator, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, + OrderBySort, PipeOperator, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, SelectFlavor, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SelectModifiers, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, diff --git a/src/ast/query.rs b/src/ast/query.rs index 35f3288a8e..1de0e0e9db 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2927,7 +2927,7 @@ impl fmt::Display for OrderBy { pub struct OrderByExpr { /// The expression to order by. pub expr: Expr, - /// Ordering options such as `ASC`/`DESC` and `NULLS` behavior. + /// Ordering options such as `ASC`/`DESC`/`USING ` and `NULLS` behavior. pub options: OrderByOptions, /// Optional `WITH FILL` clause (ClickHouse extension) which specifies how to fill gaps. pub with_fill: Option, @@ -2945,7 +2945,8 @@ impl From for OrderByExpr { impl fmt::Display for OrderByExpr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", self.expr, self.options)?; + write!(f, "{}", self.expr)?; + write!(f, "{}", self.options)?; if let Some(ref with_fill) = self.with_fill { write!(f, " {with_fill}")? } @@ -3020,22 +3021,47 @@ impl fmt::Display for InterpolateExpr { } } -#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +/// The sort order for an `ORDER BY` expression. +/// +/// See PostgreSQL `USING` operator: +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -/// Options for an `ORDER BY` expression (ASC/DESC and NULLS FIRST/LAST). +pub enum OrderBySort { + /// `ASC` + Asc, + /// `DESC` + Desc, + /// PostgreSQL `USING ` ordering. + /// + /// See + Using(ObjectName), +} + +#[derive(Default, Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// Options for an `ORDER BY` expression. pub struct OrderByOptions { - /// Optional `ASC` (`Some(true)`) or `DESC` (`Some(false)`). - pub asc: Option, + /// Optional sort order: `ASC`, `DESC`, or `USING `. + pub sort: Option, /// Optional `NULLS FIRST` (`Some(true)`) or `NULLS LAST` (`Some(false)`). pub nulls_first: Option, } impl fmt::Display for OrderByOptions { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self.asc { - Some(true) => write!(f, " ASC")?, - Some(false) => write!(f, " DESC")?, + match &self.sort { + Some(OrderBySort::Asc) => write!(f, " ASC")?, + Some(OrderBySort::Desc) => write!(f, " DESC")?, + Some(OrderBySort::Using(op)) => { + if op.0.len() > 1 { + write!(f, " USING OPERATOR({op})")?; + } else { + write!(f, " USING {op}")?; + } + } None => (), } match self.nulls_first { diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 84032847af..9b2ede40d2 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1414,6 +1414,14 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports PostgreSQL-style ordering operators: + /// `ORDER BY expr USING `. + /// + /// For example: `SELECT * FROM t ORDER BY a USING <`. + fn supports_order_by_using_operator(&self) -> bool { + false + } + /// Returns true if the dialect supports `SET NAMES [COLLATE ]`. /// /// - [MySQL](https://dev.mysql.com/doc/refman/8.4/en/set-names.html) diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index fda676eb26..c40d6d6746 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -278,6 +278,10 @@ impl Dialect for PostgreSqlDialect { true } + fn supports_order_by_using_operator(&self) -> bool { + true + } + fn supports_set_names(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 76616bc368..3d4b309581 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1378,7 +1378,7 @@ impl<'a> Parser<'a> { } let alias = self.parse_optional_alias_inner(None, validator)?; let order_by = OrderByOptions { - asc: self.parse_asc_desc(), + sort: self.parse_optional_order_by_sort(), nulls_first: None, }; Ok(ExprWithAliasAndOrderBy { @@ -18852,6 +18852,15 @@ impl<'a> Parser<'a> { } } + /// Parse ASC or DESC and map to [OrderBySort]. + fn parse_optional_order_by_sort(&mut self) -> Option { + match self.parse_asc_desc() { + Some(true) => Some(OrderBySort::Asc), + Some(false) => Some(OrderBySort::Desc), + None => None, + } + } + /// Parse an [OrderByExpr] expression. pub fn parse_order_by_expr(&mut self) -> Result { self.parse_order_by_expr_inner(false) @@ -18888,7 +18897,18 @@ impl<'a> Parser<'a> { None }; - let options = self.parse_order_by_options()?; + let options = if !with_operator_class + && self.dialect.supports_order_by_using_operator() + && self.parse_keyword(Keyword::USING) + { + let op = self.parse_order_by_using_operator()?; + OrderByOptions { + sort: Some(OrderBySort::Using(op)), + nulls_first: self.parse_null_ordering_modifier(), + } + } else { + self.parse_order_by_options()? + }; let with_fill = if self.dialect.supports_with_fill() && self.parse_keywords(&[Keyword::WITH, Keyword::FILL]) @@ -18908,18 +18928,33 @@ impl<'a> Parser<'a> { )) } - fn parse_order_by_options(&mut self) -> Result { - let asc = self.parse_asc_desc(); + fn parse_order_by_using_operator(&mut self) -> Result { + if self.parse_keyword(Keyword::OPERATOR) { + self.expect_token(&Token::LParen)?; + let operator_name = self.parse_operator_name()?; + self.expect_token(&Token::RParen)?; + return Ok(operator_name); + } + + let token = self.next_token(); + Ok(ObjectName::from(vec![Ident::new(token.token.to_string())])) + } - let nulls_first = if self.parse_keywords(&[Keyword::NULLS, Keyword::FIRST]) { + fn parse_null_ordering_modifier(&mut self) -> Option { + if self.parse_keywords(&[Keyword::NULLS, Keyword::FIRST]) { Some(true) } else if self.parse_keywords(&[Keyword::NULLS, Keyword::LAST]) { Some(false) } else { None - }; + } + } + + fn parse_order_by_options(&mut self) -> Result { + let sort = self.parse_optional_order_by_sort(); + let nulls_first = self.parse_null_ordering_modifier(); - Ok(OrderByOptions { asc, nulls_first }) + Ok(OrderByOptions { sort, nulls_first }) } // Parse a WITH FILL clause (ClickHouse dialect) @@ -21170,7 +21205,7 @@ mod tests { column: OrderByExpr { expr: Expr::Identifier(name.into()), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index afb6190822..212607e0cc 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -2731,7 +2731,7 @@ fn test_export_data() { kind: OrderByKind::Expressions(vec![OrderByExpr { expr: Expr::Identifier(Ident::new("field1")), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -2837,7 +2837,7 @@ fn test_export_data() { kind: OrderByKind::Expressions(vec![OrderByExpr { expr: Expr::Identifier(Ident::new("field1")), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index f16a1f8e44..716a3919fc 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -390,7 +390,7 @@ fn parse_alter_table_add_projection() { kind: OrderByKind::Expressions(vec![OrderByExpr { expr: Identifier(Ident::new("b")), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -1218,7 +1218,7 @@ fn parse_select_order_by_with_fill_interpolate() { OrderByExpr { expr: Expr::Identifier(Ident::new("fname")), options: OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: Some(true), }, with_fill: Some(WithFill { @@ -1230,7 +1230,7 @@ fn parse_select_order_by_with_fill_interpolate() { OrderByExpr { expr: Expr::Identifier(Ident::new("lname")), options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: Some(false), }, with_fill: Some(WithFill { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 739238c822..326fbf678e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2596,7 +2596,7 @@ fn parse_select_order_by() { OrderByExpr { expr: Expr::Identifier(Ident::new("lname")), options: OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: None, }, with_fill: None, @@ -2604,7 +2604,7 @@ fn parse_select_order_by() { OrderByExpr { expr: Expr::Identifier(Ident::new("fname")), options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: None, }, with_fill: None, @@ -2612,7 +2612,7 @@ fn parse_select_order_by() { OrderByExpr { expr: Expr::Identifier(Ident::new("id")), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -2637,7 +2637,7 @@ fn parse_select_order_by_limit() { OrderByExpr { expr: Expr::Identifier(Ident::new("lname")), options: OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: None, }, with_fill: None, @@ -2645,7 +2645,7 @@ fn parse_select_order_by_limit() { OrderByExpr { expr: Expr::Identifier(Ident::new("fname")), options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: None, }, with_fill: None, @@ -2675,63 +2675,63 @@ fn parse_select_order_by_all() { ( "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL", OrderByKind::All(OrderByOptions { - asc: None, + sort: None, nulls_first: None, }), ), ( "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL NULLS FIRST", OrderByKind::All(OrderByOptions { - asc: None, + sort: None, nulls_first: Some(true), }), ), ( "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL NULLS LAST", OrderByKind::All(OrderByOptions { - asc: None, + sort: None, nulls_first: Some(false), }), ), ( "SELECT id, fname, lname FROM customer ORDER BY ALL ASC", OrderByKind::All(OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: None, }), ), ( "SELECT id, fname, lname FROM customer ORDER BY ALL ASC NULLS FIRST", OrderByKind::All(OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: Some(true), }), ), ( "SELECT id, fname, lname FROM customer ORDER BY ALL ASC NULLS LAST", OrderByKind::All(OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: Some(false), }), ), ( "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL DESC", OrderByKind::All(OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: None, }), ), ( "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL DESC NULLS FIRST", OrderByKind::All(OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: Some(true), }), ), ( "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL DESC NULLS LAST", OrderByKind::All(OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: Some(false), }), ), @@ -2758,7 +2758,7 @@ fn parse_select_order_by_not_support_all() { OrderByKind::Expressions(vec![OrderByExpr { expr: Expr::Identifier(Ident::new("ALL")), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -2769,7 +2769,7 @@ fn parse_select_order_by_not_support_all() { OrderByKind::Expressions(vec![OrderByExpr { expr: Expr::Identifier(Ident::new("ALL")), options: OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: Some(true), }, with_fill: None, @@ -2780,7 +2780,7 @@ fn parse_select_order_by_not_support_all() { OrderByKind::Expressions(vec![OrderByExpr { expr: Expr::Identifier(Ident::new("ALL")), options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: Some(false), }, with_fill: None, @@ -2803,7 +2803,7 @@ fn parse_select_order_by_nulls_order() { OrderByExpr { expr: Expr::Identifier(Ident::new("lname")), options: OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: Some(true), }, with_fill: None, @@ -2811,7 +2811,7 @@ fn parse_select_order_by_nulls_order() { OrderByExpr { expr: Expr::Identifier(Ident::new("fname")), options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: Some(false), }, with_fill: None, @@ -2827,6 +2827,67 @@ fn parse_select_order_by_nulls_order() { assert_eq!(Some(expected_limit_clause), select.limit_clause); } +#[test] +fn parse_aggregate_order_by_using_operator() { + let sql = "SELECT aggfns(DISTINCT a, a, c ORDER BY c USING ~<~, a) FROM t"; + let dialects = all_dialects_where(|d| d.supports_order_by_using_operator()); + let select = dialects.verified_only_select(sql); + let SelectItem::UnnamedExpr(Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { clauses, .. }), + .. + })) = &select.projection[0] + else { + unreachable!("expected aggregate function in projection"); + }; + + let Some(FunctionArgumentClause::OrderBy(order_by_exprs)) = clauses + .iter() + .find(|clause| matches!(clause, FunctionArgumentClause::OrderBy(_))) + else { + unreachable!("expected ORDER BY clause in aggregate function argument list"); + }; + + assert_eq!( + order_by_exprs[0].options.sort, + Some(OrderBySort::Using(ObjectName::from(vec!["~<~".into()]))) + ); + assert_eq!(order_by_exprs[1].options.sort, None); +} + +#[test] +fn parse_order_by_using_operator_syntax() { + let dialects = all_dialects_where(|d| d.supports_order_by_using_operator()); + dialects.one_statement_parses_to( + "SELECT a FROM t ORDER BY a USING OPERATOR(<)", + "SELECT a FROM t ORDER BY a USING <", + ); + + let query = dialects + .verified_query("SELECT a FROM t ORDER BY a USING OPERATOR(pg_catalog.<) NULLS LAST"); + let order_by = query.order_by.expect("expected ORDER BY clause"); + let OrderByKind::Expressions(exprs) = order_by.kind else { + unreachable!("expected ORDER BY expressions"); + }; + + assert_eq!( + exprs[0].options.sort, + Some(OrderBySort::Using(ObjectName::from(vec![ + Ident::new("pg_catalog"), + Ident::new("<"), + ]))) + ); + assert_eq!(exprs[0].options.nulls_first, Some(false)); +} + +#[test] +fn parse_order_by_using_operator_invalid_cases() { + let dialects = all_dialects_where(|d| d.supports_order_by_using_operator()); + let err = dialects + .parse_sql_statements("SELECT a FROM t ORDER BY a USING OPERATOR();") + .unwrap_err(); + assert!(matches!(err, ParserError::ParserError(_))); +} + #[test] fn parse_select_group_by() { let sql = "SELECT id, fname, lname FROM customer GROUP BY lname, fname"; @@ -3033,7 +3094,7 @@ fn parse_select_qualify() { order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("o")), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -3482,7 +3543,7 @@ fn parse_listagg() { span: Span::empty(), }), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -3494,7 +3555,7 @@ fn parse_listagg() { span: Span::empty(), }), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -5753,7 +5814,7 @@ fn parse_window_functions() { order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("dt")), options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: None, }, with_fill: None, @@ -5979,7 +6040,7 @@ fn test_parse_named_window() { span: Span::empty(), }), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -9494,7 +9555,7 @@ fn parse_create_index() { expr: Expr::Identifier(Ident::new("name")), with_fill: None, options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, }, @@ -9505,7 +9566,7 @@ fn parse_create_index() { expr: Expr::Identifier(Ident::new("age")), with_fill: None, options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: None, }, }, @@ -9540,7 +9601,7 @@ fn test_create_index_with_using_function() { expr: Expr::Identifier(Ident::new("name")), with_fill: None, options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, }, @@ -9551,7 +9612,7 @@ fn test_create_index_with_using_function() { expr: Expr::Identifier(Ident::new("age")), with_fill: None, options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: None, }, }, @@ -9596,7 +9657,7 @@ fn test_create_index_with_with_clause() { column: OrderByExpr { expr: Expr::Identifier(Ident::new("title")), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -13276,7 +13337,7 @@ fn test_match_recognize() { order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("price_date")), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 1b09485185..c0a15d5b9a 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -23,7 +23,8 @@ use sqlparser::ast::{ ClusteredBy, CommentDef, CreateFunction, CreateFunctionBody, CreateFunctionUsing, CreateTable, Expr, Function, FunctionArgumentList, FunctionArguments, Ident, ObjectName, OrderByExpr, - OrderByOptions, SelectItem, Set, Statement, TableFactor, UnaryOperator, Use, Value, + OrderByOptions, OrderBySort, SelectItem, Set, Statement, TableFactor, UnaryOperator, Use, + Value, }; use sqlparser::dialect::{AnsiDialect, GenericDialect, HiveDialect}; use sqlparser::parser::ParserError; @@ -171,7 +172,7 @@ fn create_table_with_clustered_by() { OrderByExpr { expr: Expr::Identifier(Ident::new("a")), options: OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: None, }, with_fill: None, @@ -179,7 +180,7 @@ fn create_table_with_clustered_by() { OrderByExpr { expr: Expr::Identifier(Ident::new("b")), options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: None, }, with_fill: None, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 5c4f4a8c34..a511c0dc8d 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -691,7 +691,7 @@ fn table_constraint_unique_primary_ctor( column: OrderByExpr { expr: Expr::Identifier(ident), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -2813,7 +2813,7 @@ fn parse_update_with_order_by() { span: Span::empty(), }), options: OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: None, }, with_fill: None, @@ -2840,7 +2840,7 @@ fn parse_update_with_order_by_and_limit() { span: Span::empty(), }), options: OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: None, }, with_fill: None, @@ -2866,7 +2866,7 @@ fn parse_delete_with_order_by() { span: Span::empty(), }), options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: None, }, with_fill: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 87d17d1b46..369e7fc5d7 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2943,7 +2943,7 @@ fn parse_create_indices_with_operator_classes() { within_group: vec![], }), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -3007,7 +3007,7 @@ fn parse_create_indices_with_operator_classes() { span: Span::empty() }), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, From 7c78d138ae4b69359ad38b0996eda7de5e1a356b Mon Sep 17 00:00:00 2001 From: Corey Fritz Date: Wed, 6 May 2026 08:39:38 -0600 Subject: [PATCH 007/141] BigQuery: Parse WITH CONNECTION on CREATE EXTERNAL TABLE (#2326) --- src/ast/ddl.rs | 6 ++++++ src/ast/helpers/stmt_create_table.rs | 11 +++++++++++ src/ast/spans.rs | 1 + src/parser/mod.rs | 7 +++++++ tests/sqlparser_bigquery.rs | 25 +++++++++++++++++++++++++ tests/sqlparser_duckdb.rs | 1 + tests/sqlparser_mssql.rs | 2 ++ tests/sqlparser_postgres.rs | 1 + 8 files changed, 54 insertions(+) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 62a8a71a64..0a93f5b7ec 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3020,6 +3020,9 @@ pub struct CreateTable { /// Snowflake "EXTERNAL_VOLUME" clause for Iceberg tables /// pub external_volume: Option, + /// `WITH CONNECTION` clause. + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_external_table_statement) + pub with_connection: Option, /// Snowflake "BASE_LOCATION" clause for Iceberg tables /// pub base_location: Option, @@ -3269,6 +3272,9 @@ impl fmt::Display for CreateTable { if let Some(cluster_by) = self.cluster_by.as_ref() { write!(f, " CLUSTER BY {cluster_by}")?; } + if let Some(with_connection) = &self.with_connection { + write!(f, " WITH CONNECTION {with_connection}")?; + } if let options @ CreateTableOptions::Options(_) = &self.table_options { write!(f, " {options}")?; } diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index fc81d3b861..9ec9ab28ce 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -157,6 +157,9 @@ pub struct CreateTableBuilder { pub base_location: Option, /// Optional external volume identifier. pub external_volume: Option, + /// `WITH CONNECTION` clause. + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_external_table_statement) + pub with_connection: Option, /// Optional catalog name. pub catalog: Option, /// Optional catalog synchronization option. @@ -241,6 +244,7 @@ impl CreateTableBuilder { with_tags: None, base_location: None, external_volume: None, + with_connection: None, catalog: None, catalog_sync: None, storage_serialization_policy: None, @@ -497,6 +501,11 @@ impl CreateTableBuilder { self.external_volume = external_volume; self } + /// Set the `WITH CONNECTION` clause. + pub fn with_connection(mut self, with_connection: Option) -> Self { + self.with_connection = with_connection; + self + } /// Set the catalog name for the table. pub fn catalog(mut self, catalog: Option) -> Self { self.catalog = catalog; @@ -630,6 +639,7 @@ impl CreateTableBuilder { with_tags: self.with_tags, base_location: self.base_location, external_volume: self.external_volume, + with_connection: self.with_connection, catalog: self.catalog, catalog_sync: self.catalog_sync, storage_serialization_policy: self.storage_serialization_policy, @@ -714,6 +724,7 @@ impl From for CreateTableBuilder { with_tags: table.with_tags, base_location: table.base_location, external_volume: table.external_volume, + with_connection: table.with_connection, catalog: table.catalog, catalog_sync: table.catalog_sync, storage_serialization_policy: table.storage_serialization_policy, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 343d99e736..f6ba895478 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -589,6 +589,7 @@ impl Spanned for CreateTable { with_storage_lifecycle_policy: _, // todo, Snowflake specific with_tags: _, // todo, Snowflake specific external_volume: _, // todo, Snowflake specific + with_connection: _, // todo, BigQuery external table connection base_location: _, // todo, Snowflake specific catalog: _, // todo, Snowflake specific catalog_sync: _, // todo, Snowflake specific diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3d4b309581..07497b04f6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6418,6 +6418,12 @@ impl<'a> Parser<'a> { None }; let location = hive_formats.as_ref().and_then(|hf| hf.location.clone()); + + let with_connection = if self.parse_keywords(&[Keyword::WITH, Keyword::CONNECTION]) { + Some(self.parse_object_name(false)?) + } else { + None + }; let table_properties = self.parse_options(Keyword::TBLPROPERTIES)?; let table_options = if !table_properties.is_empty() { CreateTableOptions::TableProperties(table_properties) @@ -6432,6 +6438,7 @@ impl<'a> Parser<'a> { .hive_distribution(hive_distribution) .hive_formats(hive_formats) .table_options(table_options) + .with_connection(with_connection) .or_replace(or_replace) .if_not_exists(if_not_exists) .external(true) diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 212607e0cc..f6d4483c24 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -2203,6 +2203,31 @@ fn parse_big_query_declare() { ); } +#[test] +fn parse_bigquery_create_external_table_with_connection() { + bigquery().one_statement_parses_to( + concat!( + "CREATE OR REPLACE EXTERNAL TABLE `proj.ds.tbl` ", + "WITH CONNECTION `projects/proj/locations/us/connections/c` ", + r#"OPTIONS(format = "ICEBERG", uris = ["gs://b/m.json"])"#, + ), + concat!( + "CREATE OR REPLACE EXTERNAL TABLE `proj`.`ds`.`tbl` () ", + "WITH CONNECTION `projects/proj/locations/us/connections/c` ", + r#"OPTIONS(format = "ICEBERG", uris = ["gs://b/m.json"])"#, + ), + ); + bigquery().one_statement_parses_to( + "CREATE EXTERNAL TABLE t WITH CONNECTION c", + "CREATE EXTERNAL TABLE t () WITH CONNECTION c", + ); + bigquery().verified_stmt(concat!( + "CREATE EXTERNAL TABLE t (a INT64, b STRING) ", + r#"WITH CONNECTION c OPTIONS(uris = ["gs://x"])"#, + )); + bigquery().verified_stmt(r#"CREATE EXTERNAL TABLE t (a INT64) OPTIONS(uris = ["gs://x"])"#); +} + fn bigquery() -> TestedDialects { TestedDialects::new(vec![Box::new(BigQueryDialect {})]) } diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 548ad27cf3..86c7658131 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -780,6 +780,7 @@ fn test_duckdb_union_datatype() { with_tags: Default::default(), base_location: Default::default(), external_volume: Default::default(), + with_connection: Default::default(), catalog: Default::default(), catalog_sync: Default::default(), storage_serialization_policy: Default::default(), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index d784c74ae5..6e866746d7 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1999,6 +1999,7 @@ fn parse_create_table_with_valid_options() { with_tags: None, base_location: None, external_volume: None, + with_connection: None, catalog: None, catalog_sync: None, storage_serialization_policy: None, @@ -2176,6 +2177,7 @@ fn parse_create_table_with_identity_column() { with_tags: None, base_location: None, external_volume: None, + with_connection: None, catalog: None, catalog_sync: None, storage_serialization_policy: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 369e7fc5d7..11e76dbc7c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6702,6 +6702,7 @@ fn parse_trigger_related_functions() { with_tags: None, base_location: None, external_volume: None, + with_connection: None, catalog: None, catalog_sync: None, storage_serialization_policy: None, From ab8e6adade1568ee7e40a60f1548ba6a070f1e21 Mon Sep 17 00:00:00 2001 From: Aleksandar Maksimovic Date: Fri, 15 May 2026 00:21:57 -0700 Subject: [PATCH 008/141] Add ASYNC keyword support for CREATE INDEX (#2302) Co-authored-by: Ifeanyi Ubah --- src/ast/ddl.rs | 7 ++++++- src/ast/spans.rs | 1 + src/keywords.rs | 1 + src/parser/mod.rs | 2 ++ tests/sqlparser_common.rs | 10 ++++++++++ tests/sqlparser_postgres.rs | 18 ++++++++++++++++++ 6 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 0a93f5b7ec..da348e3a0d 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2817,6 +2817,10 @@ pub struct CreateIndex { pub unique: bool, /// whether the index is created concurrently pub concurrently: bool, + /// whether the index is created asynchronously ([DSQL]). + /// + /// [DSQL]: https://docs.aws.amazon.com/aurora-dsql/latest/userguide/working-with-create-index-async.html + pub r#async: bool, /// IF NOT EXISTS clause pub if_not_exists: bool, /// INCLUDE clause: @@ -2842,13 +2846,14 @@ impl fmt::Display for CreateIndex { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "CREATE {unique}INDEX {concurrently}{if_not_exists}", + "CREATE {unique}INDEX {concurrently}{async_}{if_not_exists}", unique = if self.unique { "UNIQUE " } else { "" }, concurrently = if self.concurrently { "CONCURRENTLY " } else { "" }, + async_ = if self.r#async { "ASYNC " } else { "" }, if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index f6ba895478..81d75b7cac 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -694,6 +694,7 @@ impl Spanned for CreateIndex { columns, unique: _, // bool concurrently: _, // bool + r#async: _, // bool if_not_exists: _, // bool include, nulls_distinct: _, // bool diff --git a/src/keywords.rs b/src/keywords.rs index a0a65be689..8c84601760 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -131,6 +131,7 @@ define_keywords!( ASOF, ASSERT, ASYMMETRIC, + ASYNC, AT, ATOMIC, ATTACH, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 07497b04f6..9655a580ae 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8043,6 +8043,7 @@ impl<'a> Parser<'a> { /// Parse a `CREATE INDEX` statement. pub fn parse_create_index(&mut self, unique: bool) -> Result { let concurrently = self.parse_keyword(Keyword::CONCURRENTLY); + let r#async = self.parse_keyword(Keyword::ASYNC); let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let mut using = None; @@ -8122,6 +8123,7 @@ impl<'a> Parser<'a> { columns, unique, concurrently, + r#async, if_not_exists, include, nulls_distinct, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 326fbf678e..b36998bfea 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9626,6 +9626,7 @@ fn test_create_index_with_using_function() { columns, unique, concurrently, + r#async, if_not_exists, include, nulls_distinct: None, @@ -9640,6 +9641,7 @@ fn test_create_index_with_using_function() { assert_eq!(indexed_columns, columns); assert!(unique); assert!(!concurrently); + assert!(!r#async); assert!(if_not_exists); assert!(include.is_empty()); assert!(with.is_empty()); @@ -9681,6 +9683,7 @@ fn test_create_index_with_with_clause() { columns, unique, concurrently, + r#async, if_not_exists, include, nulls_distinct: None, @@ -9694,6 +9697,7 @@ fn test_create_index_with_with_clause() { pretty_assertions::assert_eq!(indexed_columns, columns); assert!(unique); assert!(!concurrently); + assert!(!r#async); assert!(!if_not_exists); assert!(include.is_empty()); pretty_assertions::assert_eq!(with_parameters, with); @@ -9704,6 +9708,12 @@ fn test_create_index_with_with_clause() { } } +#[test] +fn parse_create_index_async() { + verified_stmt("CREATE INDEX ASYNC my_index ON my_table(col1)"); + verified_stmt("CREATE UNIQUE INDEX ASYNC my_index ON my_table(col1)"); +} + #[test] fn parse_drop_index() { let sql = "DROP INDEX idx_a"; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 11e76dbc7c..274988be0e 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2806,6 +2806,7 @@ fn parse_create_index() { columns, unique, concurrently, + r#async, if_not_exists, nulls_distinct: None, include, @@ -2819,6 +2820,7 @@ fn parse_create_index() { assert_eq!(None, using); assert!(!unique); assert!(!concurrently); + assert!(!r#async); assert!(if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); @@ -2841,6 +2843,7 @@ fn parse_create_anonymous_index() { columns, unique, concurrently, + r#async, if_not_exists, include, nulls_distinct: None, @@ -2854,6 +2857,7 @@ fn parse_create_anonymous_index() { assert_eq!(None, using); assert!(!unique); assert!(!concurrently); + assert!(!r#async); assert!(!if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); @@ -2959,6 +2963,7 @@ fn parse_create_indices_with_operator_classes() { columns, unique: false, concurrently: false, + r#async: false, if_not_exists: false, include, nulls_distinct: None, @@ -2987,6 +2992,7 @@ fn parse_create_indices_with_operator_classes() { columns, unique: false, concurrently: false, + r#async: false, if_not_exists: false, include, nulls_distinct: None, @@ -3070,6 +3076,7 @@ fn parse_create_bloom() { columns, unique: false, concurrently: false, + r#async: false, if_not_exists: false, include, nulls_distinct: None, @@ -3126,6 +3133,7 @@ fn parse_create_brin() { columns, unique: false, concurrently: false, + r#async: false, if_not_exists: false, include, nulls_distinct: None, @@ -3193,6 +3201,7 @@ fn parse_create_index_concurrently() { columns, unique, concurrently, + r#async, if_not_exists, include, nulls_distinct: None, @@ -3206,6 +3215,7 @@ fn parse_create_index_concurrently() { assert_eq!(None, using); assert!(!unique); assert!(concurrently); + assert!(!r#async); assert!(if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); @@ -3228,6 +3238,7 @@ fn parse_create_index_with_predicate() { columns, unique, concurrently, + r#async, if_not_exists, include, nulls_distinct: None, @@ -3241,6 +3252,7 @@ fn parse_create_index_with_predicate() { assert_eq!(None, using); assert!(!unique); assert!(!concurrently); + assert!(!r#async); assert!(if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); @@ -3263,6 +3275,7 @@ fn parse_create_index_with_include() { columns, unique, concurrently, + r#async, if_not_exists, include, nulls_distinct: None, @@ -3276,6 +3289,7 @@ fn parse_create_index_with_include() { assert_eq!(None, using); assert!(!unique); assert!(!concurrently); + assert!(!r#async); assert!(if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); assert_eq_vec(&["col3", "col4"], &include); @@ -3298,6 +3312,7 @@ fn parse_create_index_with_nulls_distinct() { columns, unique, concurrently, + r#async, if_not_exists, include, nulls_distinct: Some(nulls_distinct), @@ -3311,6 +3326,7 @@ fn parse_create_index_with_nulls_distinct() { assert_eq!(None, using); assert!(!unique); assert!(!concurrently); + assert!(!r#async); assert!(if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); @@ -3331,6 +3347,7 @@ fn parse_create_index_with_nulls_distinct() { columns, unique, concurrently, + r#async, if_not_exists, include, nulls_distinct: Some(nulls_distinct), @@ -3344,6 +3361,7 @@ fn parse_create_index_with_nulls_distinct() { assert_eq!(None, using); assert!(!unique); assert!(!concurrently); + assert!(!r#async); assert!(if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); From 05af09cd1ddc3e56419dc48b5d5b0bfdce7d89fd Mon Sep 17 00:00:00 2001 From: Ning Sun Date: Mon, 18 May 2026 05:58:25 -0700 Subject: [PATCH 009/141] PostgreSQL: add support for `ABORT TRANSACTION` (#2332) --- src/parser/mod.rs | 15 +++++++++++++++ tests/sqlparser_common.rs | 16 ++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9655a580ae..763b876dec 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -675,6 +675,7 @@ impl<'a> Parser<'a> { self.parse_throw().map(Into::into) } Keyword::ROLLBACK => self.parse_rollback(), + Keyword::ABORT => self.parse_abort(), Keyword::ASSERT => self.parse_assert(), // `PREPARE`, `EXECUTE` and `DEALLOCATE` are Postgres-specific // syntaxes. They are used for Postgres prepared statement. @@ -19393,6 +19394,20 @@ impl<'a> Parser<'a> { Ok(Statement::Rollback { chain, savepoint }) } + /// Parse an 'ABORT' statement + /// + /// ```sql + /// ABORT [ TRANSACTION | WORK ] [ AND [ NO ] CHAIN ] + /// ``` + pub fn parse_abort(&mut self) -> Result { + let chain = self.parse_commit_rollback_chain()?; + + Ok(Statement::Rollback { + chain, + savepoint: None, + }) + } + /// Parse an optional `AND [NO] CHAIN` clause for `COMMIT` and `ROLLBACK` statements pub fn parse_commit_rollback_chain(&mut self) -> Result { let _ = self.parse_one_of_keywords(&[Keyword::TRANSACTION, Keyword::WORK, Keyword::TRAN]); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b36998bfea..bcf3c1d55f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9536,6 +9536,22 @@ fn parse_rollback() { ); } +#[test] +fn parse_abort() { + one_statement_parses_to("ABORT", "ROLLBACK"); + one_statement_parses_to("ABORT TRANSACTION", "ROLLBACK"); + one_statement_parses_to("ABORT WORK", "ROLLBACK"); + one_statement_parses_to("ABORT AND CHAIN", "ROLLBACK AND CHAIN"); + one_statement_parses_to("ABORT AND NO CHAIN", "ROLLBACK"); + one_statement_parses_to("ABORT TRANSACTION AND CHAIN", "ROLLBACK AND CHAIN"); + one_statement_parses_to("ABORT WORK AND NO CHAIN", "ROLLBACK"); + + assert_eq!( + parse_sql_statements("ABORT TO test1").unwrap_err(), + ParserError::ParserError("Expected: end of statement, found: TO".to_string()), + ); +} + #[test] #[should_panic(expected = "Parse results with GenericDialect are different from PostgreSqlDialect")] fn ensure_multiple_dialects_are_tested() { From 05428be15914cbf3fc1296838f5eaa1e883425d3 Mon Sep 17 00:00:00 2001 From: Guillaume Gelin Date: Wed, 20 May 2026 12:28:54 +0200 Subject: [PATCH 010/141] Snowflake: Add support for PUT (#2341) --- src/ast/mod.rs | 25 ++++++++++++++ src/ast/spans.rs | 2 +- src/dialect/snowflake.rs | 19 +++++++++++ src/keywords.rs | 1 + tests/sqlparser_snowflake.rs | 65 ++++++++++++++++++++++++++++++++++++ 5 files changed, 111 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c0826f2008..46826d7be0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4852,6 +4852,20 @@ pub enum Statement { /// Snowflake `LIST` /// See: List(FileStagingCommand), + /// Snowflake `PUT` + /// ```sql + /// PUT 'file://' [