From f406386e514a7ab22f10dcf121f224bac96b6e23 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Wed, 19 Nov 2025 13:42:37 +0100 Subject: [PATCH] Oracle: Support for MERGE predicates --- src/ast/mod.rs | 37 +++- src/ast/spans.rs | 50 ++++- src/dialect/generic.rs | 16 ++ src/dialect/mod.rs | 111 ++++++++++- src/parser/merge.rs | 369 ++++++++++++++++++++++++++++++++++++ src/parser/mod.rs | 163 +--------------- tests/sqlparser_bigquery.rs | 17 +- tests/sqlparser_common.rs | 77 ++++++++ 8 files changed, 672 insertions(+), 168 deletions(-) create mode 100644 src/parser/merge.rs diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2c452a699..d3dc8857f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8618,6 +8618,7 @@ impl Display for MergeInsertKind { /// /// [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) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -8636,6 +8637,10 @@ pub struct MergeInsertExpr { pub kind_token: AttachedToken, /// The insert type used by the statement. pub kind: MergeInsertKind, + /// An optional condition to restrict the insertion (Oracle specific) + /// + /// Enabled via [`Dialect::supports_merge_insert_predicate`](crate::dialect::Dialect::supports_merge_insert_predicate). + pub insert_predicate: Option, } impl Display for MergeInsertExpr { @@ -8643,7 +8648,11 @@ impl Display for MergeInsertExpr { if !self.columns.is_empty() { write!(f, "({}) ", display_comma_separated(self.columns.as_slice()))?; } - write!(f, "{}", self.kind) + write!(f, "{}", self.kind)?; + if let Some(predicate) = self.insert_predicate.as_ref() { + write!(f, " WHERE {}", predicate)?; + } + Ok(()) } } @@ -8656,6 +8665,7 @@ impl Display for MergeInsertExpr { /// /// [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) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -8676,7 +8686,16 @@ pub enum MergeAction { Update { /// The `UPDATE` token that starts the sub-expression. update_token: AttachedToken, + /// The update assiment expressions assignments: Vec, + /// `where_clause` for the update (Oralce specific) + /// + /// Enabled via [`Dialect::supports_merge_update_predicate`](crate::dialect::Dialect::supports_merge_update_predicate). + update_predicate: Option, + /// `delete_clause` for the update "delete where" (Oracle specific) + /// + /// Enabled via [`Dialect::supports_merge_update_delete_predicate`](crate::dialect::Dialect::supports_merge_update_delete_predicate). + delete_predicate: Option, }, /// A plain `DELETE` clause Delete { @@ -8691,8 +8710,20 @@ impl Display for MergeAction { MergeAction::Insert(insert) => { write!(f, "INSERT {insert}") } - MergeAction::Update { assignments, .. } => { - write!(f, "UPDATE SET {}", display_comma_separated(assignments)) + MergeAction::Update { + update_token: _, + assignments, + update_predicate, + delete_predicate, + } => { + write!(f, "UPDATE SET {}", display_comma_separated(assignments))?; + if let Some(predicate) = update_predicate.as_ref() { + write!(f, " WHERE {predicate}")?; + } + if let Some(predicate) = delete_predicate.as_ref() { + write!(f, " DELETE WHERE {predicate}")?; + } + Ok(()) } MergeAction::Delete { .. } => { write!(f, "DELETE") diff --git a/src/ast/spans.rs b/src/ast/spans.rs index d54290b68..9f4d27f26 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2407,8 +2407,13 @@ impl Spanned for MergeAction { MergeAction::Update { update_token, assignments, + update_predicate, + delete_predicate, } => union_spans( - core::iter::once(update_token.0.span).chain(assignments.iter().map(Spanned::span)), + core::iter::once(update_token.0.span) + .chain(assignments.iter().map(Spanned::span)) + .chain(update_predicate.iter().map(Spanned::span)) + .chain(delete_predicate.iter().map(Spanned::span)), ), MergeAction::Delete { delete_token } => delete_token.0.span, } @@ -2427,6 +2432,7 @@ impl Spanned for MergeInsertExpr { }, ] .into_iter() + .chain(self.insert_predicate.iter().map(Spanned::span)) .chain(self.columns.iter().map(|i| i.span)), ) } @@ -2800,6 +2806,8 @@ WHERE id = 1 if let MergeAction::Update { update_token, assignments: _, + update_predicate: _, + delete_predicate: _, } = &clauses[1].action { assert_eq!( @@ -2920,4 +2928,44 @@ WHERE id = 1 panic!("not a MERGE statement"); }; } + + #[test] + fn test_merge_statement_spans_with_update_predicates() { + let sql = r#" + MERGE INTO a USING b ON a.id = b.id + WHEN MATCHED THEN + UPDATE set a.x = a.x + b.x + WHERE b.x != 2 + DELETE WHERE a.x <> 3"#; + + let r = Parser::parse_sql(&crate::dialect::GenericDialect, sql).unwrap(); + assert_eq!(1, r.len()); + + // ~ assert the span of the whole statement + let stmt_span = r[0].span(); + assert_eq!( + stmt_span, + Span::new(Location::new(2, 8), Location::new(6, 36)) + ); + } + + #[test] + fn test_merge_statement_spans_with_insert_predicate() { + let sql = r#" + MERGE INTO a USING b ON a.id = b.id + WHEN NOT MATCHED THEN + INSERT VALUES (b.x, b.y) WHERE b.x != 2 +-- qed +"#; + + let r = Parser::parse_sql(&crate::dialect::GenericDialect, sql).unwrap(); + assert_eq!(1, r.len()); + + // ~ assert the span of the whole statement + let stmt_span = r[0].span(); + assert_eq!( + stmt_span, + Span::new(Location::new(2, 8), Location::new(4, 52)) + ); + } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index dffc5b527..b606ad9e2 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -195,4 +195,20 @@ impl Dialect for GenericDialect { fn supports_interval_options(&self) -> bool { true } + + fn supports_merge_insert_qualified_columns(&self) -> bool { + true + } + + fn supports_merge_insert_predicate(&self) -> bool { + true + } + + fn supports_merge_update_predicate(&self) -> bool { + true + } + + fn supports_merge_update_delete_predicate(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index ef4e1cdde..8c532c021 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -601,13 +601,122 @@ pub trait Dialect: Debug + Any { false } - /// Return true if the dialect supports specifying multiple options + /// Returns true if the dialect supports specifying multiple options /// in a `CREATE TABLE` statement for the structure of the new table. For example: /// `CREATE TABLE t (a INT, b INT) AS SELECT 1 AS b, 2 AS a` fn supports_create_table_multi_schema_info_sources(&self) -> bool { false } + /// Returns `true` if the dialect supports qualified column names + /// as part of a MERGE's INSERT's column list. Example: + /// + /// ```sql + /// MERGE INTO FOO + /// USING FOO_IMP + /// ON (FOO.ID = FOO_IMP.ID) + /// WHEN NOT MATCHED THEN + /// -- no qualifier + /// INSERT (ID, NAME) + /// VALUES (FOO_IMP.ID, UPPER(FOO_IMP.NAME)) + /// ``` + /// vs. + /// ```sql + /// MERGE INTO FOO + /// USING FOO_IMP + /// ON (FOO.ID = FOO_IMP.ID) + /// WHEN NOT MATCHED THEN + /// -- here: qualified + /// INSERT (FOO.ID, FOO.NAME) + /// VALUES (FOO_IMP.ID, UPPER(FOO_IMP.NAME)) + /// ``` + /// or + /// ```sql + /// MERGE INTO FOO X + /// USING FOO_IMP + /// ON (X.ID = FOO_IMP.ID) + /// WHEN NOT MATCHED THEN + /// -- here: qualified using the alias + /// INSERT (X.ID, X.NAME) + /// VALUES (FOO_IMP.ID, UPPER(FOO_IMP.NAME)) + /// ``` + /// + /// Note: in the latter case, the qualifier must match the target table + /// name or its alias if one is present. The parser will enforce this. + /// + /// The default implementation always returns `false` not allowing the + /// qualifiers. + fn supports_merge_insert_qualified_columns(&self) -> bool { + false + } + + /// Returns `true` if the dialect supports specify an INSERT predicate in + /// MERGE statements. Example: + /// + /// ```sql + /// MERGE INTO FOO + /// USING FOO_IMP + /// ON (FOO.ID = FOO_IMP.ID) + /// WHEN NOT MATCHED THEN + /// INSERT (ID, NAME) + /// VALUES (FOO_IMP.ID, UPPER(FOO_IMP.NAME)) + /// -- insert predicate + /// WHERE NOT FOO_IMP.NAME like '%.IGNORE' + /// ``` + /// + /// The default implementation always returns `false` indicating no + /// support for the additional predicate. + /// + /// See also [Dialect::supports_merge_update_predicate] and + /// [Dialect::supports_merge_update_delete_predicate]. + fn supports_merge_insert_predicate(&self) -> bool { + false + } + + /// Indicates the supports of UPDATE predicates in MERGE + /// statements. Example: + /// + /// ```sql + /// MERGE INTO FOO + /// USING FOO_IMPORT + /// ON (FOO.ID = FOO_IMPORT.ID) + /// WHEN MATCHED THEN + /// UPDATE SET FOO.NAME = FOO_IMPORT.NAME + /// -- update predicate + /// WHERE FOO.NAME <> 'pete' + /// ``` + /// + /// The default implementation always returns false indicating no support + /// for the additional predicate. + /// + /// See also [Dialect::supports_merge_insert_predicate] and + /// [Dialect::supports_merge_update_delete_predicate]. + fn supports_merge_update_predicate(&self) -> bool { + false + } + + /// Indicates the supports of UPDATE ... DELETEs and associated predicates + /// in MERGE statements. Example: + /// + /// ```sql + /// MERGE INTO FOO + /// USING FOO_IMPORT + /// ON (FOO.ID = FOO_IMPORT.ID) + /// WHEN MATCHED THEN + /// UPDATE SET FOO.NAME = FOO_IMPORT.NAME + /// -- update delete with predicate + /// DELETE WHERE UPPER(FOO.NAME) == FOO.NAME + /// ``` + /// + /// The default implementation always returns false indicating no support + /// for the `UPDATE ... DELETE` and its associated predicate. + /// + /// See also [Dialect::supports_merge_insert_predicate] and + /// [Dialect::supports_merge_update_predicate]. + fn supports_merge_update_delete_predicate(&self) -> bool { + false + } + /// Dialect-specific infix parser override /// /// This method is called to parse the next infix expression. diff --git a/src/parser/merge.rs b/src/parser/merge.rs new file mode 100644 index 000000000..992ed3917 --- /dev/null +++ b/src/parser/merge.rs @@ -0,0 +1,369 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! SQL Parser for MERGE + +#[cfg(not(feature = "std"))] +use alloc::{boxed::Box, format, string::ToString, vec, vec::Vec}; + +use crate::{ + ast::{ + Ident, MergeAction, MergeClause, MergeClauseKind, MergeInsertExpr, MergeInsertKind, + ObjectName, ObjectNamePart, SetExpr, Statement, TableFactor, + }, + dialect::{BigQueryDialect, GenericDialect, MySqlDialect}, + keywords::Keyword, + parser::IsOptional, + tokenizer::TokenWithSpan, +}; + +use super::{Parser, ParserError}; + +impl Parser<'_> { + /// Parse a MERGE statement, returning a `Box`ed SetExpr + /// + /// This is used to reduce the size of the stack frames in debug builds + pub(super) fn parse_merge_setexpr_boxed( + &mut self, + merge_token: TokenWithSpan, + ) -> Result, ParserError> { + Ok(Box::new(SetExpr::Merge(self.parse_merge(merge_token)?))) + } + + pub fn parse_merge(&mut self, merge_token: TokenWithSpan) -> Result { + let into = self.parse_keyword(Keyword::INTO); + + let table = self.parse_table_factor()?; + + self.expect_keyword_is(Keyword::USING)?; + let source = self.parse_table_factor()?; + self.expect_keyword_is(Keyword::ON)?; + let on = self.parse_expr()?; + let clauses = self.parse_merge_clauses(&table)?; + let output = match self.parse_one_of_keywords(&[Keyword::OUTPUT, Keyword::RETURNING]) { + Some(keyword) => Some(self.parse_output(keyword, self.get_current_token().clone())?), + None => None, + }; + + Ok(Statement::Merge { + merge_token: merge_token.into(), + into, + table, + source, + on: Box::new(on), + clauses, + output, + }) + } + + fn parse_merge_clauses( + &mut self, + target_table: &TableFactor, + ) -> Result, ParserError> { + let mut clauses = vec![]; + loop { + if !(self.parse_keyword(Keyword::WHEN)) { + break; + } + let when_token = self.get_current_token().clone(); + + let mut clause_kind = MergeClauseKind::Matched; + if self.parse_keyword(Keyword::NOT) { + clause_kind = MergeClauseKind::NotMatched; + } + self.expect_keyword_is(Keyword::MATCHED)?; + + if matches!(clause_kind, MergeClauseKind::NotMatched) + && self.parse_keywords(&[Keyword::BY, Keyword::SOURCE]) + { + clause_kind = MergeClauseKind::NotMatchedBySource; + } else if matches!(clause_kind, MergeClauseKind::NotMatched) + && self.parse_keywords(&[Keyword::BY, Keyword::TARGET]) + { + clause_kind = MergeClauseKind::NotMatchedByTarget; + } + + let predicate = if self.parse_keyword(Keyword::AND) { + Some(self.parse_expr()?) + } else { + None + }; + + self.expect_keyword_is(Keyword::THEN)?; + + let merge_clause = match self.parse_one_of_keywords(&[ + Keyword::UPDATE, + Keyword::INSERT, + Keyword::DELETE, + ]) { + Some(Keyword::UPDATE) => { + if matches!( + clause_kind, + MergeClauseKind::NotMatched | MergeClauseKind::NotMatchedByTarget + ) { + return parser_err!( + format_args!("UPDATE is not allowed in a {clause_kind} merge clause"), + self.get_current_token().span.start + ); + } + + let update_token = self.get_current_token().clone(); + self.expect_keyword_is(Keyword::SET)?; + let assignments = self.parse_comma_separated(Parser::parse_assignment)?; + let update_predicate = if self.dialect.supports_merge_update_predicate() + && self.parse_keyword(Keyword::WHERE) + { + Some(self.parse_expr()?) + } else { + None + }; + let delete_predicate = if self.dialect.supports_merge_update_delete_predicate() + && self.parse_keyword(Keyword::DELETE) + { + let _ = self.expect_keyword(Keyword::WHERE)?; + Some(self.parse_expr()?) + } else { + None + }; + MergeAction::Update { + update_token: update_token.into(), + assignments, + update_predicate, + delete_predicate, + } + } + Some(Keyword::DELETE) => { + if matches!( + clause_kind, + MergeClauseKind::NotMatched | MergeClauseKind::NotMatchedByTarget + ) { + return parser_err!( + format_args!("DELETE is not allowed in a {clause_kind} merge clause"), + self.get_current_token().span.start + ); + }; + + let delete_token = self.get_current_token().clone(); + MergeAction::Delete { + delete_token: delete_token.into(), + } + } + Some(Keyword::INSERT) => { + if !matches!( + clause_kind, + MergeClauseKind::NotMatched | MergeClauseKind::NotMatchedByTarget + ) { + return parser_err!( + format_args!("INSERT is not allowed in a {clause_kind} merge clause"), + self.get_current_token().span.start + ); + }; + + let insert_token = self.get_current_token().clone(); + let is_mysql = dialect_of!(self is MySqlDialect); + + let columns = self.parse_merge_clause_insert_columns( + target_table, + &clause_kind, + 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.dialect.supports_merge_insert_predicate() + && 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, + }) + } + _ => { + return parser_err!( + "expected UPDATE, DELETE or INSERT in merge clause", + self.peek_token_ref().span.start + ); + } + }; + clauses.push(MergeClause { + when_token: when_token.into(), + clause_kind, + predicate, + action: merge_clause, + }); + } + Ok(clauses) + } + + fn parse_merge_clause_insert_columns( + &mut self, + target_table: &TableFactor, + clause_kind: &MergeClauseKind, + allow_empty: bool, + ) -> Result, ParserError> { + if self.dialect.supports_merge_insert_qualified_columns() { + let cols = + self.parse_parenthesized_qualified_column_list(IsOptional::Optional, allow_empty)?; + if let TableFactor::Table { name, alias, .. } = target_table { + if let Some(alias) = alias { + if alias.columns.is_empty() { + // ~ only the alias is supported at this point + unqualify_columns(cols, None, Some(&alias.name)).map_err(|e| { + ParserError::ParserError(format!( + "Invalid column for INSERT in a {clause_kind} merge clause: {e}" + )) + }) + } else { + Err(ParserError::ParserError(format!( + "Invalid target ALIAS for INSERT in a {clause_kind} merge clause; must be an identifier" + ))) + } + } else { + // ~ allow the full qualifier, but also just the table name + if name.0.len() == 1 { + unqualify_columns(cols, Some(name), None).map_err(|e| { + ParserError::ParserError(format!( + "Invalid column for INSERT in a {clause_kind} merge clause: {e}" + )) + }) + } else if let Some(table_name) = + name.0.last().and_then(ObjectNamePart::as_ident) + { + unqualify_columns(cols, Some(name), Some(table_name)).map_err(|e| { + ParserError::ParserError(format!( + "Invalid column for INSERT in a {clause_kind} merge clause: {e}" + )) + }) + } else { + Err(ParserError::ParserError(format!( + "Invalid target table NAME for INSERT in a {clause_kind} merge clause; must be an identifier" + ))) + } + } + } else { + Err(ParserError::ParserError(format!( + "Invalid target for INSERT in a {clause_kind} merge clause; must be a TABLE identifier" + ))) + } + } else { + self.parse_parenthesized_column_list(IsOptional::Optional, allow_empty) + } + } +} + +/// Helper to unqualify a list of columns with either a qualified prefix or a +/// qualifier identifier +/// +/// Oracle allows `INSERT ([qualifier.]column_name, ...)` in MERGE statements +/// with `qualifier` referring to the alias of the target table (if one is +/// present) or, if no alias is present, to the target table name itself - +/// either qualified or unqualified. +fn unqualify_columns( + columns: Vec, + allowed_qualifier_1: Option<&ObjectName>, + allowed_qualifier_2: Option<&Ident>, +) -> Result, &'static str> { + // ~ helper to turn a column name (part) into a plain `ident` + // possibly bailing with error + fn to_ident(name: ObjectNamePart) -> Result { + match name { + ObjectNamePart::Identifier(ident) => Ok(ident), + ObjectNamePart::Function(_) => Err("not an identifier"), + } + } + + // ~ helper to return the last part of `name` if it is + // preceded by `prefix` + fn unqualify_column( + mut name: ObjectName, + prefix: &ObjectName, + ) -> Result { + let mut name_iter = name.0.iter(); + let mut prefix_iter = prefix.0.iter(); + loop { + match (name_iter.next(), prefix_iter.next()) { + (Some(_), None) => { + if name_iter.next().is_none() { + return Ok(name.0.pop().expect("missing name part")); + } else { + return Err(name); + } + } + (Some(c), Some(q)) if c == q => { + // ~ continue matching next part + } + _ => { + return Err(name); + } + } + } + } + + let mut unqualified = Vec::::with_capacity(columns.len()); + for mut name in columns { + if name.0.is_empty() { + return Err("empty column name"); + } + + if name.0.len() == 1 { + unqualified.push(to_ident(name.0.pop().expect("missing name part"))?); + continue; + } + + // ~ try matching by the primary prefix + if let Some(allowed_qualifier) = allowed_qualifier_1 { + match unqualify_column(name, allowed_qualifier) { + Ok(ident) => { + unqualified.push(to_ident(ident)?); + continue; + } + Err(n) => { + // ~ continue trying with the alternate prefix below + name = n; + } + } + } + + // ~ try matching by the alternate prefix + if let Some(allowed_qualifier) = allowed_qualifier_2 { + if name.0.len() == 2 + && name + .0 + .first() + .and_then(ObjectNamePart::as_ident) + .map(|i| i == allowed_qualifier) + .unwrap_or(false) + { + unqualified.push(to_ident(name.0.pop().expect("missing name part"))?); + continue; + } + } + + return Err("not matching target table"); + } + Ok(unqualified) +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1fa7f796a..1f24d0cb0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -45,8 +45,6 @@ use crate::keywords::{Keyword, ALL_KEYWORDS}; use crate::tokenizer::*; use sqlparser::parser::ParserState::ColumnDefinition; -mod alter; - #[derive(Debug, Clone, PartialEq, Eq)] pub enum ParserError { TokenizerError(String), @@ -61,6 +59,9 @@ macro_rules! parser_err { }; } +mod alter; +mod merge; + #[cfg(feature = "std")] /// Implementation [`RecursionCounter`] if std is available mod recursion { @@ -11618,7 +11619,7 @@ impl<'a> Parser<'a> { token => { return Err(ParserError::ParserError(format!( "Unexpected token in identifier: {token}" - )))? + )))?; } } } @@ -12122,16 +12123,6 @@ impl<'a> Parser<'a> { Ok(Box::new(SetExpr::Delete(self.parse_delete(delete_token)?))) } - /// Parse a MERGE statement, returning a `Box`ed SetExpr - /// - /// This is used to reduce the size of the stack frames in debug builds - fn parse_merge_setexpr_boxed( - &mut self, - merge_token: TokenWithSpan, - ) -> Result, ParserError> { - Ok(Box::new(SetExpr::Merge(self.parse_merge(merge_token)?))) - } - pub fn parse_delete(&mut self, delete_token: TokenWithSpan) -> Result { let (tables, with_from_keyword) = if !self.parse_keyword(Keyword::FROM) { // `FROM` keyword is optional in BigQuery SQL. @@ -17197,126 +17188,6 @@ impl<'a> Parser<'a> { }) } - pub fn parse_merge_clauses(&mut self) -> Result, ParserError> { - let mut clauses = vec![]; - loop { - if !(self.parse_keyword(Keyword::WHEN)) { - break; - } - let when_token = self.get_current_token().clone(); - - let mut clause_kind = MergeClauseKind::Matched; - if self.parse_keyword(Keyword::NOT) { - clause_kind = MergeClauseKind::NotMatched; - } - self.expect_keyword_is(Keyword::MATCHED)?; - - if matches!(clause_kind, MergeClauseKind::NotMatched) - && self.parse_keywords(&[Keyword::BY, Keyword::SOURCE]) - { - clause_kind = MergeClauseKind::NotMatchedBySource; - } else if matches!(clause_kind, MergeClauseKind::NotMatched) - && self.parse_keywords(&[Keyword::BY, Keyword::TARGET]) - { - clause_kind = MergeClauseKind::NotMatchedByTarget; - } - - let predicate = if self.parse_keyword(Keyword::AND) { - Some(self.parse_expr()?) - } else { - None - }; - - self.expect_keyword_is(Keyword::THEN)?; - - let merge_clause = match self.parse_one_of_keywords(&[ - Keyword::UPDATE, - Keyword::INSERT, - Keyword::DELETE, - ]) { - Some(Keyword::UPDATE) => { - if matches!( - clause_kind, - MergeClauseKind::NotMatched | MergeClauseKind::NotMatchedByTarget - ) { - return parser_err!( - format_args!("UPDATE is not allowed in a {clause_kind} merge clause"), - self.get_current_token().span.start - ); - } - - let update_token = self.get_current_token().clone(); - self.expect_keyword_is(Keyword::SET)?; - MergeAction::Update { - update_token: update_token.into(), - assignments: self.parse_comma_separated(Parser::parse_assignment)?, - } - } - Some(Keyword::DELETE) => { - if matches!( - clause_kind, - MergeClauseKind::NotMatched | MergeClauseKind::NotMatchedByTarget - ) { - return parser_err!( - format_args!("DELETE is not allowed in a {clause_kind} merge clause"), - self.get_current_token().span.start - ); - }; - - let delete_token = self.get_current_token().clone(); - MergeAction::Delete { - delete_token: delete_token.into(), - } - } - Some(Keyword::INSERT) => { - if !matches!( - clause_kind, - MergeClauseKind::NotMatched | MergeClauseKind::NotMatchedByTarget - ) { - return parser_err!( - format_args!("INSERT is not allowed in a {clause_kind} merge clause"), - self.get_current_token().span.start - ); - }; - - let insert_token = self.get_current_token().clone(); - let is_mysql = dialect_of!(self is MySqlDialect); - - let columns = self.parse_parenthesized_column_list(Optional, 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) - }; - MergeAction::Insert(MergeInsertExpr { - insert_token: insert_token.into(), - columns, - kind_token: kind_token.into(), - kind, - }) - } - _ => { - return parser_err!( - "expected UPDATE, DELETE or INSERT in merge clause", - self.peek_token_ref().span.start - ); - } - }; - clauses.push(MergeClause { - when_token: when_token.into(), - clause_kind, - predicate, - action: merge_clause, - }); - } - Ok(clauses) - } - fn parse_output( &mut self, start_keyword: Keyword, @@ -17360,32 +17231,6 @@ impl<'a> Parser<'a> { }) } - pub fn parse_merge(&mut self, merge_token: TokenWithSpan) -> Result { - let into = self.parse_keyword(Keyword::INTO); - - let table = self.parse_table_factor()?; - - self.expect_keyword_is(Keyword::USING)?; - let source = self.parse_table_factor()?; - self.expect_keyword_is(Keyword::ON)?; - let on = self.parse_expr()?; - let clauses = self.parse_merge_clauses()?; - let output = match self.parse_one_of_keywords(&[Keyword::OUTPUT, Keyword::RETURNING]) { - Some(keyword) => Some(self.parse_output(keyword, self.get_current_token().clone())?), - None => None, - }; - - Ok(Statement::Merge { - merge_token: merge_token.into(), - into, - table, - source, - on: Box::new(on), - clauses, - output, - }) - } - fn parse_pragma_value(&mut self) -> Result { match self.parse_value()?.value { v @ Value::SingleQuotedString(_) => Ok(v), diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 15bf59cd3..709216670 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1813,6 +1813,7 @@ fn parse_merge() { explicit_row: false, rows: vec![vec![Expr::value(number("1")), Expr::value(number("2"))]], }), + insert_predicate: None, }); let update_action = MergeAction::Update { update_token: AttachedToken::empty(), @@ -1826,6 +1827,8 @@ fn parse_merge() { value: Expr::value(number("2")), }, ], + update_predicate: None, + delete_predicate: None, }; match bigquery_and_generic().verified_stmt(sql) { @@ -1926,6 +1929,7 @@ fn parse_merge() { columns: vec![Ident::new("product"), Ident::new("quantity"),], kind_token: AttachedToken::empty(), kind: MergeInsertKind::Row, + insert_predicate: None, }) }, MergeClause { @@ -1937,6 +1941,7 @@ fn parse_merge() { columns: vec![Ident::new("product"), Ident::new("quantity"),], kind_token: AttachedToken::empty(), kind: MergeInsertKind::Row, + insert_predicate: None, }) }, MergeClause { @@ -1947,7 +1952,8 @@ fn parse_merge() { insert_token: AttachedToken::empty(), columns: vec![], kind_token: AttachedToken::empty(), - kind: MergeInsertKind::Row + kind: MergeInsertKind::Row, + insert_predicate: None, }) }, MergeClause { @@ -1958,7 +1964,8 @@ fn parse_merge() { insert_token: AttachedToken::empty(), columns: vec![], kind_token: AttachedToken::empty(), - kind: MergeInsertKind::Row + kind: MergeInsertKind::Row, + insert_predicate: None, }) }, MergeClause { @@ -1990,7 +1997,8 @@ fn parse_merge() { Expr::value(number("1")), Expr::Identifier(Ident::new("DEFAULT")), ]] - }) + }), + insert_predicate: None, }) }, MergeClause { @@ -2008,7 +2016,8 @@ fn parse_merge() { Expr::value(number("1")), Expr::Identifier(Ident::new("DEFAULT")), ]] - }) + }), + insert_predicate: None, }) }, ], diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ba1e64488..7ad39bcb7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1644,6 +1644,10 @@ fn ms_and_generic() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {}), Box::new(GenericDialect {})]) } +fn only_generic() -> TestedDialects { + TestedDialects::new(vec![Box::new(GenericDialect {})]) +} + #[test] fn parse_json_ops_without_colon() { use self::BinaryOperator::*; @@ -9946,6 +9950,7 @@ fn parse_merge() { ]), ]] }), + insert_predicate: None, }), }, MergeClause { @@ -9985,6 +9990,8 @@ fn parse_merge() { ]), }, ], + update_predicate: None, + delete_predicate: None, }, }, MergeClause { @@ -10077,6 +10084,76 @@ fn test_merge_with_delimiter() { } } +#[test] +fn test_merge_with_predicates() { + let sql = "\ +MERGE INTO FOO \ +USING FOO_IMPORT \ +ON (FOO.ID = FOO_IMPORT.ID) \ +WHEN MATCHED THEN \ +UPDATE SET FOO.NAME = FOO_IMPORT.NAME \ +WHERE 1 = 1 \ +DELETE WHERE FOO.NAME LIKE '%.DELETE' \ +WHEN NOT MATCHED THEN \ +INSERT (ID, NAME) \ +VALUES (FOO_IMPORT.ID, UPPER(FOO_IMPORT.NAME)) \ +WHERE NOT FOO_IMPORT.NAME LIKE '%.DO_NOT_INSERT'"; + only_generic().verified_stmt(sql); +} + +#[test] +fn test_merge_with_insert_qualified_columns() { + let sql = "\ +MERGE INTO FOO USING FOO_IMPORT ON (FOO.ID = FOO_IMPORT.ID) \ +WHEN NOT MATCHED THEN \ +INSERT (FOO.ID, FOO.NAME) \ +VALUES (1, 2)"; + + let expected = "\ +MERGE INTO FOO USING FOO_IMPORT ON (FOO.ID = FOO_IMPORT.ID) \ +WHEN NOT MATCHED THEN \ +INSERT (ID, NAME) \ +VALUES (1, 2)"; + + only_generic().one_statement_parses_to(sql, expected); +} + +#[test] +fn test_merge_with_insert_qualified_columns_via_alias() { + let sql = "\ +MERGE INTO FOO F USING FOO_IMPORT ON (F.ID = FOO_IMPORT.ID) \ +WHEN NOT MATCHED THEN \ +INSERT (F.ID, F.NAME) \ +VALUES (1, 2)"; + + // note: this serialized form will break execution on an Oracle database + // as it doesn't allow the "AS" keyword; Issue #1784 + let expected = "\ +MERGE INTO FOO AS F USING FOO_IMPORT ON (F.ID = FOO_IMPORT.ID) \ +WHEN NOT MATCHED THEN \ +INSERT (ID, NAME) \ +VALUES (1, 2)"; + + only_generic().one_statement_parses_to(sql, expected); +} + +#[test] +fn test_merge_with_insert_qualified_columns_with_schema() { + let sql = "\ +MERGE INTO PLAYGROUND.FOO USING FOO_IMPORT ON (PLAYGROUND.FOO.ID = FOO_IMPORT.ID) \ +WHEN NOT MATCHED THEN \ +INSERT (PLAYGROUND.FOO.ID, PLAYGROUND.FOO.NAME) \ +VALUES (1, 2)"; + + let expected = "\ +MERGE INTO PLAYGROUND.FOO USING FOO_IMPORT ON (PLAYGROUND.FOO.ID = FOO_IMPORT.ID) \ +WHEN NOT MATCHED THEN \ +INSERT (ID, NAME) \ +VALUES (1, 2)"; + + only_generic().one_statement_parses_to(sql, expected); +} + #[test] fn test_merge_invalid_statements() { let dialects = all_dialects();