From 3ac7a2ec8aadee52310b51aea993464ae41a69b9 Mon Sep 17 00:00:00 2001 From: finchxxia <13153363548@163.com> Date: Mon, 27 Apr 2026 15:21:45 +0800 Subject: [PATCH 1/4] Databricks: Add support for `UPDATE SET *` and `INSERT *` in MERGE statements Add parsing support for the Databricks star shorthand syntax in MERGE statements, allowing `UPDATE SET *` and `INSERT *` to reference all columns from the source. - Add `MergeUpdateKind` enum (`Set(Vec)` | `Star`) to replace the previous `assignments` field on `MergeUpdateExpr`, enforcing valid states at the type level - Add `MergeInsertKind::Star` variant for `INSERT *` shorthand - Add `supports_merge_star_syntax()` dialect method for Databricks and Generic --- src/ast/dml.rs | 56 ++++++++++++++++++++++++++-- src/ast/mod.rs | 3 +- src/ast/spans.rs | 13 +++++-- src/dialect/databricks.rs | 5 +++ src/dialect/generic.rs | 4 ++ src/dialect/mod.rs | 7 ++++ src/parser/merge.rs | 69 ++++++++++++++++++++++------------- tests/sqlparser_bigquery.rs | 4 +- tests/sqlparser_common.rs | 4 +- tests/sqlparser_databricks.rs | 58 +++++++++++++++++++++++++++++ 10 files changed, 185 insertions(+), 38 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 3e2f7ee09..416c2dd10 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 `*` shorthand to insert all columns. + /// + /// Example: + /// ```sql + /// INSERT * + /// ``` + /// [Databricks](https://docs.databricks.com/en/sql/language-manual/delta-merge-into.html) + Star, } impl Display for MergeInsertKind { @@ -660,6 +668,9 @@ impl Display for MergeInsertKind { MergeInsertKind::Row => { write!(f, "ROW") } + MergeInsertKind::Star => { + 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 `*` shorthand to update all columns from the source. + /// + /// Example: + /// ```sql + /// UPDATE SET * + /// ``` + /// [Databricks](https://docs.databricks.com/en/sql/language-manual/delta-merge-into.html) + Star, +} + +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::Star => { + 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 886bea26d..33d9d85b2 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -85,7 +85,8 @@ pub use self::ddl::{ }; pub use self::dml::{ Delete, Insert, Merge, MergeAction, MergeClause, MergeClauseKind, MergeInsertExpr, - MergeInsertKind, MergeUpdateExpr, MultiTableInsertIntoClause, MultiTableInsertType, + MergeInsertKind, MergeUpdateExpr, MergeUpdateKind, MultiTableInsertIntoClause, + MultiTableInsertType, MultiTableInsertValue, MultiTableInsertValues, MultiTableInsertWhenClause, OutputClause, Update, }; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index dc8be4aec..e7bc65da1 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -39,7 +39,8 @@ 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, + MergeInsertKind, MergeUpdateExpr, MergeUpdateKind, NamedParenthesizedList, + NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, OnInsert, OpenStatement, OrderBy, OrderByExpr, OrderByKind, OutputClause, Parens, Partition, PartitionBoundValue, PivotValueSource, ProjectionSelect, Query, RaiseStatement, RaiseStatementValue, @@ -2531,7 +2532,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::Star => Span::empty(), }, ] .into_iter() @@ -2543,9 +2544,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::Star => 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)), ) @@ -2927,7 +2932,7 @@ WHERE id = 1 ); if let MergeAction::Update(MergeUpdateExpr { update_token, - assignments: _, + kind: _, update_predicate: _, delete_predicate: _, }) = &clauses[1].action diff --git a/src/dialect/databricks.rs b/src/dialect/databricks.rs index 679d335d7..30438ef04 100644 --- a/src/dialect/databricks.rs +++ b/src/dialect/databricks.rs @@ -108,4 +108,9 @@ impl Dialect for DatabricksDialect { fn supports_select_item_multi_column_alias(&self) -> bool { true } + + /// See + fn supports_merge_star_syntax(&self) -> bool { + true + } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 674311a92..6d4708576 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -304,4 +304,8 @@ impl Dialect for GenericDialect { fn supports_xml_expressions(&self) -> bool { true } + + fn supports_merge_star_syntax(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 93ca5cf6a..e9c20ca14 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1164,6 +1164,13 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports `UPDATE SET *` and `INSERT *` + /// shorthand syntax in `MERGE` statements. + /// + fn supports_merge_star_syntax(&self) -> bool { + false + } + /// Returns true if the dialect supports the `LISTEN`, `UNLISTEN` and `NOTIFY` statements fn supports_listen_notify(&self) -> bool { false diff --git a/src/parser/merge.rs b/src/parser/merge.rs index 619be612b..1fc0f3e9b 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,13 @@ 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.dialect.supports_merge_star_syntax() + && self.consume_token(&Token::Mul) + { + MergeUpdateKind::Star + } 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 +141,7 @@ impl Parser<'_> { }; MergeAction::Update(MergeUpdateExpr { update_token: update_token.into(), - assignments, + kind, update_predicate, delete_predicate, }) @@ -167,32 +174,44 @@ 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) + if self.dialect.supports_merge_star_syntax() && self.consume_token(&Token::Mul) { - (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()?) + 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::Star, + 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 4bdb54f74..afb619082 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 221c88971..fac6c177c 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_databricks.rs b/tests/sqlparser_databricks.rs index 0ed0b4118..d143c4b51 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -738,3 +738,61 @@ fn parse_cte_without_as() { .parse_sql_statements("WITH cte (SELECT 1) SELECT * FROM cte") .is_err()); } + +#[test] +fn test_merge_update_set_star_and_insert_star() { + let sql = "MERGE INTO target USING source ON target.id = source.id WHEN MATCHED THEN UPDATE SET * WHEN NOT MATCHED THEN INSERT *"; + databricks_and_generic().verified_stmt(sql); + + match databricks().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::Star)); + } + _ => panic!("Expected UPDATE action"), + } + + match &merge.clauses[1].action { + MergeAction::Insert(insert_expr) => { + assert!(matches!(insert_expr.kind, MergeInsertKind::Star)); + assert!(insert_expr.columns.is_empty()); + } + _ => panic!("Expected INSERT action"), + } + } + _ => panic!("Expected MERGE statement"), + } +} + +#[test] +fn test_merge_update_set_star_with_predicate() { + let sql = "MERGE INTO target USING source ON target.id = source.id WHEN MATCHED AND source.active = true THEN UPDATE SET *"; + databricks_and_generic().verified_stmt(sql); +} + +#[test] +fn test_merge_insert_star_with_not_matched_by_target() { + let sql = "MERGE INTO target USING source ON target.id = source.id WHEN NOT MATCHED BY TARGET THEN INSERT *"; + databricks_and_generic().verified_stmt(sql); +} + +#[test] +fn test_merge_mixed_star_and_explicit() { + let sql = "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)"; + databricks_and_generic().verified_stmt(sql); +} + +#[test] +fn test_merge_star_with_subquery_source() { + 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 *" + ); + databricks_and_generic().verified_stmt(sql); +} From 084698a840c12d86cb968fd4b89569adede28bab Mon Sep 17 00:00:00 2001 From: finchxxia <13153363548@163.com> Date: Mon, 27 Apr 2026 15:30:00 +0800 Subject: [PATCH 2/4] chore: fix codestyle --- src/ast/mod.rs | 5 ++--- src/ast/spans.rs | 17 ++++++++--------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 33d9d85b2..3cb5f8041 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -86,9 +86,8 @@ pub use self::ddl::{ pub use self::dml::{ Delete, Insert, Merge, MergeAction, MergeClause, MergeClauseKind, MergeInsertExpr, MergeInsertKind, MergeUpdateExpr, MergeUpdateKind, MultiTableInsertIntoClause, - MultiTableInsertType, - MultiTableInsertValue, MultiTableInsertValues, MultiTableInsertWhenClause, OutputClause, - Update, + 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 e7bc65da1..8635f37a0 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -40,15 +40,14 @@ use super::{ JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause, MatchRecognizePattern, Measure, Merge, MergeAction, MergeClause, MergeInsertExpr, 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, + 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. From c8f3487b600d0fdeacc3558708f6220759a3c576 Mon Sep 17 00:00:00 2001 From: finchxxia <13153363548@163.com> Date: Thu, 30 Apr 2026 12:46:46 +0800 Subject: [PATCH 3/4] Update src/ast/dml.rs Co-authored-by: Ifeanyi Ubah --- src/ast/dml.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 416c2dd10..4a08de160 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -656,7 +656,7 @@ pub enum MergeInsertKind { /// INSERT * /// ``` /// [Databricks](https://docs.databricks.com/en/sql/language-manual/delta-merge-into.html) - Star, + Wildcard, } impl Display for MergeInsertKind { From 819cf4d8b54ad2322fbf7ac4f9e5e6b62e4d4dd3 Mon Sep 17 00:00:00 2001 From: finchxxia <13153363548@163.com> Date: Fri, 1 May 2026 21:17:41 +0800 Subject: [PATCH 4/4] update merge test --- src/ast/dml.rs | 10 +++--- src/ast/spans.rs | 4 +-- src/dialect/databricks.rs | 5 --- src/dialect/generic.rs | 4 --- src/dialect/mod.rs | 7 ----- src/parser/merge.rs | 11 +++---- tests/sqlparser_common.rs | 39 +++++++++++++++++++++++ tests/sqlparser_databricks.rs | 58 ----------------------------------- 8 files changed, 50 insertions(+), 88 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 4a08de160..c2fe2ffd6 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -649,7 +649,7 @@ pub enum MergeInsertKind { /// ``` /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) Row, - /// The insert expression uses the `*` shorthand to insert all columns. + /// The insert expression uses the `*` wildcard to insert all columns. /// /// Example: /// ```sql @@ -668,7 +668,7 @@ impl Display for MergeInsertKind { MergeInsertKind::Row => { write!(f, "ROW") } - MergeInsertKind::Star => { + MergeInsertKind::Wildcard => { write!(f, "*") } } @@ -733,14 +733,14 @@ pub enum MergeUpdateKind { /// UPDATE SET quantity = source.quantity, name = source.name /// ``` Set(Vec), - /// The `*` shorthand to update all columns from the source. + /// 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) - Star, + Wildcard, } impl Display for MergeUpdateKind { @@ -749,7 +749,7 @@ impl Display for MergeUpdateKind { MergeUpdateKind::Set(assignments) => { write!(f, "SET {}", display_comma_separated(assignments)) } - MergeUpdateKind::Star => { + MergeUpdateKind::Wildcard => { write!(f, "SET *") } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 8635f37a0..a711f1fd1 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2531,7 +2531,7 @@ impl Spanned for MergeInsertExpr { self.kind_token.0.span, match self.kind { MergeInsertKind::Values(ref values) => values.span(), - MergeInsertKind::Row | MergeInsertKind::Star => Span::empty(), + MergeInsertKind::Row | MergeInsertKind::Wildcard => Span::empty(), }, ] .into_iter() @@ -2545,7 +2545,7 @@ 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::Star => Span::empty(), + MergeUpdateKind::Wildcard => Span::empty(), }; union_spans( core::iter::once(self.update_token.0.span) diff --git a/src/dialect/databricks.rs b/src/dialect/databricks.rs index 30438ef04..679d335d7 100644 --- a/src/dialect/databricks.rs +++ b/src/dialect/databricks.rs @@ -108,9 +108,4 @@ impl Dialect for DatabricksDialect { fn supports_select_item_multi_column_alias(&self) -> bool { true } - - /// See - fn supports_merge_star_syntax(&self) -> bool { - true - } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 6d4708576..674311a92 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -304,8 +304,4 @@ impl Dialect for GenericDialect { fn supports_xml_expressions(&self) -> bool { true } - - fn supports_merge_star_syntax(&self) -> bool { - true - } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index e9c20ca14..93ca5cf6a 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1164,13 +1164,6 @@ pub trait Dialect: Debug + Any { false } - /// Returns true if the dialect supports `UPDATE SET *` and `INSERT *` - /// shorthand syntax in `MERGE` statements. - /// - fn supports_merge_star_syntax(&self) -> bool { - false - } - /// Returns true if the dialect supports the `LISTEN`, `UNLISTEN` and `NOTIFY` statements fn supports_listen_notify(&self) -> bool { false diff --git a/src/parser/merge.rs b/src/parser/merge.rs index 1fc0f3e9b..21e9d6087 100644 --- a/src/parser/merge.rs +++ b/src/parser/merge.rs @@ -121,10 +121,8 @@ impl Parser<'_> { let update_token = self.get_current_token().clone(); self.expect_keyword_is(Keyword::SET)?; - let kind = if self.dialect.supports_merge_star_syntax() - && self.consume_token(&Token::Mul) - { - MergeUpdateKind::Star + let kind = if self.consume_token(&Token::Mul) { + MergeUpdateKind::Wildcard } else { MergeUpdateKind::Set(self.parse_comma_separated(Parser::parse_assignment)?) }; @@ -175,14 +173,13 @@ impl Parser<'_> { let insert_token = self.get_current_token().clone(); - if self.dialect.supports_merge_star_syntax() && self.consume_token(&Token::Mul) - { + 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::Star, + kind: MergeInsertKind::Wildcard, insert_predicate: None, }) } else { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index fac6c177c..739238c82 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -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] diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index d143c4b51..0ed0b4118 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -738,61 +738,3 @@ fn parse_cte_without_as() { .parse_sql_statements("WITH cte (SELECT 1) SELECT * FROM cte") .is_err()); } - -#[test] -fn test_merge_update_set_star_and_insert_star() { - let sql = "MERGE INTO target USING source ON target.id = source.id WHEN MATCHED THEN UPDATE SET * WHEN NOT MATCHED THEN INSERT *"; - databricks_and_generic().verified_stmt(sql); - - match databricks().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::Star)); - } - _ => panic!("Expected UPDATE action"), - } - - match &merge.clauses[1].action { - MergeAction::Insert(insert_expr) => { - assert!(matches!(insert_expr.kind, MergeInsertKind::Star)); - assert!(insert_expr.columns.is_empty()); - } - _ => panic!("Expected INSERT action"), - } - } - _ => panic!("Expected MERGE statement"), - } -} - -#[test] -fn test_merge_update_set_star_with_predicate() { - let sql = "MERGE INTO target USING source ON target.id = source.id WHEN MATCHED AND source.active = true THEN UPDATE SET *"; - databricks_and_generic().verified_stmt(sql); -} - -#[test] -fn test_merge_insert_star_with_not_matched_by_target() { - let sql = "MERGE INTO target USING source ON target.id = source.id WHEN NOT MATCHED BY TARGET THEN INSERT *"; - databricks_and_generic().verified_stmt(sql); -} - -#[test] -fn test_merge_mixed_star_and_explicit() { - let sql = "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)"; - databricks_and_generic().verified_stmt(sql); -} - -#[test] -fn test_merge_star_with_subquery_source() { - 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 *" - ); - databricks_and_generic().verified_stmt(sql); -}