From fbdb772f9757731ac2eb4c3cb0aa07c100ae1b6c Mon Sep 17 00:00:00 2001 From: osipovartem Date: Sun, 13 Sep 2026 00:10:18 +0300 Subject: [PATCH] Support Snowflake MERGE ALL BY NAME --- src/ast/dml.rs | 45 +++++++++++++++++++++++++++++++++--- src/ast/mod.rs | 6 ++--- src/ast/spans.rs | 37 +++++++++++++++-------------- src/parser/merge.rs | 44 ++++++++++++++++++++++++----------- tests/sqlparser_bigquery.rs | 4 ++-- tests/sqlparser_common.rs | 4 ++-- tests/sqlparser_snowflake.rs | 23 ++++++++++++++++++ 7 files changed, 120 insertions(+), 43 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 3e2f7ee09f..ff0add671f 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -649,6 +649,13 @@ pub enum MergeInsertKind { /// ``` /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) Row, + /// Snowflake's name-based shorthand for inserting every target column. + /// + /// Example: + /// ```sql + /// INSERT ALL BY NAME + /// ``` + AllByName, } impl Display for MergeInsertKind { @@ -660,6 +667,9 @@ impl Display for MergeInsertKind { MergeInsertKind::Row => { write!(f, "ROW") } + MergeInsertKind::AllByName => { + write!(f, "ALL BY NAME") + } } } } @@ -710,6 +720,35 @@ 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. + Set(Vec), + /// Snowflake's name-based shorthand for updating every target column. + /// + /// Example: + /// ```sql + /// UPDATE ALL BY NAME + /// ``` + AllByName, +} + +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::AllByName => { + write!(f, "ALL BY NAME") + } + } + } +} + /// The expression used to update rows within a `MERGE` statement. /// /// Examples @@ -726,8 +765,8 @@ impl Display for MergeInsertExpr { pub struct MergeUpdateExpr { /// The `UPDATE` token that starts the sub-expression. pub update_token: AttachedToken, - /// The update assiment expressions - pub assignments: Vec, + /// The update kind. + pub kind: MergeUpdateKind, /// `where_clause` for the update (Oralce specific) pub update_predicate: Option, /// `delete_clause` for the update "delete where" (Oracle specific) @@ -736,7 +775,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 41f7861786..d7f83fbba6 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 b725897db2..8afa2f65a8 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. @@ -2540,7 +2540,8 @@ 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::AllByName => Span::empty(), + // `ROW` and `ALL BY NAME` are covered by `kind_token`. }, ] .into_iter() @@ -2552,9 +2553,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::AllByName => Span::empty(), + }; union_spans( - core::iter::once(self.update_token.0.span) - .chain(self.assignments.iter().map(Spanned::span)) + [self.update_token.0.span, kind_span] + .into_iter() .chain(self.update_predicate.iter().map(Spanned::span)) .chain(self.delete_predicate.iter().map(Spanned::span)), ) @@ -2934,13 +2939,7 @@ WHERE id = 1 clauses[1].when_token.0.span, Span::new(Location::new(12, 17), Location::new(12, 21)) ); - if let MergeAction::Update(MergeUpdateExpr { - update_token, - assignments: _, - update_predicate: _, - delete_predicate: _, - }) = &clauses[1].action - { + if let MergeAction::Update(MergeUpdateExpr { update_token, .. }) = &clauses[1].action { assert_eq!( update_token.0.span, Span::new(Location::new(13, 13), Location::new(13, 19)) diff --git a/src/parser/merge.rs b/src/parser/merge.rs index 619be612bf..f63def8588 100644 --- a/src/parser/merge.rs +++ b/src/parser/merge.rs @@ -18,9 +18,9 @@ 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}, + dialect::{BigQueryDialect, GenericDialect, MySqlDialect, SnowflakeDialect}, keywords::Keyword, parser::IsOptional, tokenizer::TokenWithSpan, @@ -119,8 +119,14 @@ 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 dialect_of!(self is SnowflakeDialect) + && self.parse_keywords(&[Keyword::ALL, Keyword::BY, Keyword::NAME]) + { + MergeUpdateKind::AllByName + } else { + self.expect_keyword_is(Keyword::SET)?; + 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 +140,7 @@ impl Parser<'_> { }; MergeAction::Update(MergeUpdateExpr { update_token: update_token.into(), - assignments, + kind, update_predicate, delete_predicate, }) @@ -168,17 +174,27 @@ 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) + let (columns, kind, kind_token) = if dialect_of!(self is SnowflakeDialect) + && self.parse_keywords(&[Keyword::ALL, Keyword::BY, Keyword::NAME]) { - (MergeInsertKind::Row, self.get_current_token().clone()) + ( + vec![], + MergeInsertKind::AllByName, + 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 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) + }; + (columns, kind, kind_token) }; let insert_predicate = if self.parse_keyword(Keyword::WHERE) { Some(self.parse_expr()?) 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 40152062d9..9fd1234a14 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, }), diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 69ac21d144..4160fb0ae3 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -57,6 +57,29 @@ fn parse_order_by_all() { ); } +#[test] +fn parse_merge_all_by_name() { + let sql = "MERGE INTO target AS t USING source AS s ON t.id = s.id WHEN MATCHED THEN UPDATE ALL BY NAME WHEN NOT MATCHED THEN INSERT ALL BY NAME"; + let Statement::Merge(merge) = snowflake().verified_stmt(sql) else { + unreachable!(); + }; + + assert!(matches!( + merge.clauses[0].action, + MergeAction::Update(MergeUpdateExpr { + kind: MergeUpdateKind::AllByName, + .. + }) + )); + assert!(matches!( + merge.clauses[1].action, + MergeAction::Insert(MergeInsertExpr { + kind: MergeInsertKind::AllByName, + .. + }) + )); +} + #[test] fn test_snowflake_create_table_timestamp_ntz_precision_ctas_values() { let sql = "CREATE TABLE t (x TIMESTAMP_NTZ(3)) AS SELECT * FROM VALUES ('2025-04-09T21:11:23')";