From 202801f323fd411f31ac5758c0bb3b3724e20001 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Sun, 23 Nov 2025 09:11:43 +0100 Subject: [PATCH] `Merge` as top-level struct --- src/ast/dml.rs | 346 +++++++++++++++++++++++++++++++++++- src/ast/mod.rs | 326 +-------------------------------- src/ast/spans.rs | 79 ++++---- src/parser/merge.rs | 39 +++- src/parser/mod.rs | 27 --- tests/sqlparser_bigquery.rs | 8 +- tests/sqlparser_common.rs | 12 +- 7 files changed, 431 insertions(+), 406 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index d6009ce8..cf689f70 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -24,13 +24,16 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use crate::display_utils::{indented_list, Indent, SpaceOrNewline}; +use crate::{ + ast::display_separated, + display_utils::{indented_list, Indent, SpaceOrNewline}, +}; use super::{ display_comma_separated, helpers::attached_token::AttachedToken, query::InputFormatClause, Assignment, Expr, FromTable, Ident, InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, - OrderByExpr, Query, SelectItem, Setting, SqliteOnConflict, TableObject, TableWithJoins, - UpdateTableFromKind, + OrderByExpr, Query, SelectInto, SelectItem, Setting, SqliteOnConflict, TableFactor, + TableObject, TableWithJoins, UpdateTableFromKind, Values, }; /// INSERT statement. @@ -310,3 +313,340 @@ impl Display for Update { Ok(()) } } + +/// 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 struct Merge { + /// The `MERGE` token that starts the statement. + pub merge_token: AttachedToken, + /// optional INTO keyword + pub into: bool, + /// Specifies the table to merge + pub table: TableFactor, + /// Specifies the table or subquery to join with the target table + pub source: TableFactor, + /// Specifies the expression on which to join the target table and source + pub on: Box, + /// Specifies the actions to perform when values match or do not match. + pub clauses: Vec, + // Specifies the output to save changes in MSSQL + pub output: Option, +} + +impl Display for Merge { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "MERGE{int} {table} USING {source} ", + int = if self.into { " INTO" } else { "" }, + table = self.table, + source = self.source, + )?; + write!(f, "ON {on} ", on = self.on)?; + write!(f, "{}", display_separated(&self.clauses, " "))?; + if let Some(ref output) = self.output { + write!(f, " {output}")?; + } + Ok(()) + } +} + +/// A `WHEN` clause within a `MERGE` Statement +/// +/// Example: +/// ```sql +/// WHEN NOT MATCHED BY SOURCE AND product LIKE '%washer%' THEN DELETE +/// ``` +/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) +/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#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 struct MergeClause { + /// The `WHEN` token that starts the sub-expression. + pub when_token: AttachedToken, + pub clause_kind: MergeClauseKind, + pub predicate: Option, + pub action: MergeAction, +} + +impl Display for MergeClause { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let MergeClause { + when_token: _, + clause_kind, + predicate, + action, + } = self; + + write!(f, "WHEN {clause_kind}")?; + if let Some(pred) = predicate { + write!(f, " AND {pred}")?; + } + write!(f, " THEN {action}") + } +} + +/// Variant of `WHEN` clause used within a `MERGE` Statement. +/// +/// Example: +/// ```sql +/// MERGE INTO T USING U ON FALSE WHEN MATCHED THEN DELETE +/// ``` +/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) +/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#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 MergeClauseKind { + /// `WHEN MATCHED` + Matched, + /// `WHEN NOT MATCHED` + NotMatched, + /// `WHEN MATCHED BY TARGET` + /// + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) + NotMatchedByTarget, + /// `WHEN MATCHED BY SOURCE` + /// + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) + NotMatchedBySource, +} + +impl Display for MergeClauseKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + MergeClauseKind::Matched => write!(f, "MATCHED"), + MergeClauseKind::NotMatched => write!(f, "NOT MATCHED"), + MergeClauseKind::NotMatchedByTarget => write!(f, "NOT MATCHED BY TARGET"), + MergeClauseKind::NotMatchedBySource => write!(f, "NOT MATCHED BY SOURCE"), + } + } +} + +/// Underlying statement of a `WHEN` clause within a `MERGE` Statement +/// +/// Example +/// ```sql +/// INSERT (product, quantity) VALUES(product, quantity) +/// ``` +/// +/// [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))] +pub enum MergeAction { + /// An `INSERT` clause + /// + /// Example: + /// ```sql + /// INSERT (product, quantity) VALUES(product, quantity) + /// ``` + Insert(MergeInsertExpr), + /// An `UPDATE` clause + /// + /// Example: + /// ```sql + /// UPDATE SET quantity = T.quantity + S.quantity + /// ``` + Update(MergeUpdateExpr), + /// A plain `DELETE` clause + Delete { + /// The `DELETE` token that starts the sub-expression. + delete_token: AttachedToken, + }, +} + +impl Display for MergeAction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + MergeAction::Insert(insert) => { + write!(f, "INSERT {insert}") + } + MergeAction::Update(update) => { + write!(f, "UPDATE {update}") + } + MergeAction::Delete { .. } => { + write!(f, "DELETE") + } + } + } +} + +/// The type of expression used to insert rows within a `MERGE` statement. +/// +/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) +/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#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 MergeInsertKind { + /// The insert expression is defined from an explicit `VALUES` clause + /// + /// Example: + /// ```sql + /// INSERT VALUES(product, quantity) + /// ``` + Values(Values), + /// The insert expression is defined using only the `ROW` keyword. + /// + /// Example: + /// ```sql + /// INSERT ROW + /// ``` + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) + Row, +} + +impl Display for MergeInsertKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + MergeInsertKind::Values(values) => { + write!(f, "{values}") + } + MergeInsertKind::Row => { + write!(f, "ROW") + } + } + } +} + +/// The expression used to insert rows within a `MERGE` statement. +/// +/// Examples +/// ```sql +/// INSERT (product, quantity) VALUES(product, quantity) +/// INSERT ROW +/// ``` +/// +/// [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))] +pub struct MergeInsertExpr { + /// The `INSERT` token that starts the sub-expression. + pub insert_token: AttachedToken, + /// Columns (if any) specified by the insert. + /// + /// Example: + /// ```sql + /// INSERT (product, quantity) VALUES(product, quantity) + /// INSERT (product, quantity) ROW + /// ``` + pub columns: Vec, + /// The token, `[VALUES | ROW]` starting `kind`. + 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 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if !self.columns.is_empty() { + write!(f, "({}) ", display_comma_separated(self.columns.as_slice()))?; + } + write!(f, "{}", self.kind)?; + if let Some(predicate) = self.insert_predicate.as_ref() { + write!(f, " WHERE {}", predicate)?; + } + Ok(()) + } +} + +/// The expression used to update rows within a `MERGE` statement. +/// +/// Examples +/// ```sql +/// UPDATE SET quantity = T.quantity + S.quantity +/// ``` +/// +/// [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))] +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) + /// + /// Enabled via [`Dialect::supports_merge_update_predicate`](crate::dialect::Dialect::supports_merge_update_predicate). + pub 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). + pub delete_predicate: Option, +} + +impl Display for MergeUpdateExpr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SET {}", display_comma_separated(&self.assignments))?; + if let Some(predicate) = self.update_predicate.as_ref() { + write!(f, " WHERE {predicate}")?; + } + if let Some(predicate) = self.delete_predicate.as_ref() { + write!(f, " DELETE WHERE {predicate}")?; + } + Ok(()) + } +} + +/// A `OUTPUT` Clause in the end of a `MERGE` Statement +/// +/// Example: +/// OUTPUT $action, deleted.* INTO dbo.temp_products; +/// [mssql](https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum OutputClause { + Output { + output_token: AttachedToken, + select_items: Vec, + into_table: Option, + }, + Returning { + returning_token: AttachedToken, + select_items: Vec, + }, +} + +impl fmt::Display for OutputClause { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + OutputClause::Output { + output_token: _, + select_items, + into_table, + } => { + f.write_str("OUTPUT ")?; + display_comma_separated(select_items).fmt(f)?; + if let Some(into_table) = into_table { + f.write_str(" ")?; + into_table.fmt(f)?; + } + Ok(()) + } + OutputClause::Returning { + returning_token: _, + select_items, + } => { + f.write_str("RETURNING ")?; + display_comma_separated(select_items).fmt(f) + } + } + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5c3dca32..d5c479ec 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -77,7 +77,10 @@ pub use self::ddl::{ UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, }; -pub use self::dml::{Delete, Insert, Update}; +pub use self::dml::{ + Delete, Insert, Merge, MergeAction, MergeClause, MergeClauseKind, MergeInsertExpr, + MergeInsertKind, MergeUpdateExpr, OutputClause, Update, +}; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode, @@ -4082,22 +4085,7 @@ pub enum Statement { /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) /// [MSSQL](https://learn.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql?view=sql-server-ver16) - Merge { - /// The `MERGE` token that starts the statement. - merge_token: AttachedToken, - /// optional INTO keyword - into: bool, - /// Specifies the table to merge - table: TableFactor, - /// Specifies the table or subquery to join with the target table - source: TableFactor, - /// Specifies the expression on which to join the target table and source - on: Box, - /// Specifies the actions to perform when values match or do not match. - clauses: Vec, - // Specifies the output to save changes in MSSQL - output: Option, - }, + Merge(Merge), /// ```sql /// CACHE [ FLAG ] TABLE [ OPTIONS('K1' = 'V1', 'K2' = V2) ] [ AS ] [ ] /// ``` @@ -5514,27 +5502,7 @@ impl fmt::Display for Statement { Statement::ReleaseSavepoint { name } => { write!(f, "RELEASE SAVEPOINT {name}") } - Statement::Merge { - merge_token: _, - into, - table, - source, - on, - clauses, - output, - } => { - write!( - f, - "MERGE{int} {table} USING {source} ", - int = if *into { " INTO" } else { "" } - )?; - write!(f, "ON {on} ")?; - write!(f, "{}", display_separated(clauses, " "))?; - if let Some(output) = output { - write!(f, " {output}")?; - } - Ok(()) - } + Statement::Merge(merge) => merge.fmt(f), Statement::Cache { table_name, table_flag, @@ -8559,288 +8527,6 @@ impl fmt::Display for CopyLegacyCsvOption { } } -/// Variant of `WHEN` clause used within a `MERGE` Statement. -/// -/// Example: -/// ```sql -/// MERGE INTO T USING U ON FALSE WHEN MATCHED THEN DELETE -/// ``` -/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) -/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#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 MergeClauseKind { - /// `WHEN MATCHED` - Matched, - /// `WHEN NOT MATCHED` - NotMatched, - /// `WHEN MATCHED BY TARGET` - /// - /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) - NotMatchedByTarget, - /// `WHEN MATCHED BY SOURCE` - /// - /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) - NotMatchedBySource, -} - -impl Display for MergeClauseKind { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - MergeClauseKind::Matched => write!(f, "MATCHED"), - MergeClauseKind::NotMatched => write!(f, "NOT MATCHED"), - MergeClauseKind::NotMatchedByTarget => write!(f, "NOT MATCHED BY TARGET"), - MergeClauseKind::NotMatchedBySource => write!(f, "NOT MATCHED BY SOURCE"), - } - } -} - -/// The type of expression used to insert rows within a `MERGE` statement. -/// -/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) -/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#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 MergeInsertKind { - /// The insert expression is defined from an explicit `VALUES` clause - /// - /// Example: - /// ```sql - /// INSERT VALUES(product, quantity) - /// ``` - Values(Values), - /// The insert expression is defined using only the `ROW` keyword. - /// - /// Example: - /// ```sql - /// INSERT ROW - /// ``` - /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) - Row, -} - -impl Display for MergeInsertKind { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - MergeInsertKind::Values(values) => { - write!(f, "{values}") - } - MergeInsertKind::Row => { - write!(f, "ROW") - } - } - } -} - -/// The expression used to insert rows within a `MERGE` statement. -/// -/// Examples -/// ```sql -/// INSERT (product, quantity) VALUES(product, quantity) -/// INSERT ROW -/// ``` -/// -/// [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))] -pub struct MergeInsertExpr { - /// The `INSERT` token that starts the sub-expression. - pub insert_token: AttachedToken, - /// Columns (if any) specified by the insert. - /// - /// Example: - /// ```sql - /// INSERT (product, quantity) VALUES(product, quantity) - /// INSERT (product, quantity) ROW - /// ``` - pub columns: Vec, - /// The token, `[VALUES | ROW]` starting `kind`. - 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 { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if !self.columns.is_empty() { - write!(f, "({}) ", display_comma_separated(self.columns.as_slice()))?; - } - write!(f, "{}", self.kind)?; - if let Some(predicate) = self.insert_predicate.as_ref() { - write!(f, " WHERE {}", predicate)?; - } - Ok(()) - } -} - -/// Underlying statement of a when clause within a `MERGE` Statement -/// -/// Example -/// ```sql -/// INSERT (product, quantity) VALUES(product, quantity) -/// ``` -/// -/// [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))] -pub enum MergeAction { - /// An `INSERT` clause - /// - /// Example: - /// ```sql - /// INSERT (product, quantity) VALUES(product, quantity) - /// ``` - Insert(MergeInsertExpr), - /// An `UPDATE` clause - /// - /// Example: - /// ```sql - /// UPDATE SET quantity = T.quantity + S.quantity - /// ``` - 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 { - /// The `DELETE` token that starts the sub-expression. - delete_token: AttachedToken, - }, -} - -impl Display for MergeAction { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - MergeAction::Insert(insert) => { - write!(f, "INSERT {insert}") - } - 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") - } - } - } -} - -/// A when clause within a `MERGE` Statement -/// -/// Example: -/// ```sql -/// WHEN NOT MATCHED BY SOURCE AND product LIKE '%washer%' THEN DELETE -/// ``` -/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) -/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#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 struct MergeClause { - /// The `WHEN` token that starts the sub-expression. - pub when_token: AttachedToken, - pub clause_kind: MergeClauseKind, - pub predicate: Option, - pub action: MergeAction, -} - -impl Display for MergeClause { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let MergeClause { - when_token: _, - clause_kind, - predicate, - action, - } = self; - - write!(f, "WHEN {clause_kind}")?; - if let Some(pred) = predicate { - write!(f, " AND {pred}")?; - } - write!(f, " THEN {action}") - } -} - -/// A Output Clause in the end of a 'MERGE' Statement -/// -/// Example: -/// OUTPUT $action, deleted.* INTO dbo.temp_products; -/// [mssql](https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql) -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum OutputClause { - Output { - output_token: AttachedToken, - select_items: Vec, - into_table: Option, - }, - Returning { - returning_token: AttachedToken, - select_items: Vec, - }, -} - -impl fmt::Display for OutputClause { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - OutputClause::Output { - output_token: _, - select_items, - into_table, - } => { - f.write_str("OUTPUT ")?; - display_comma_separated(select_items).fmt(f)?; - if let Some(into_table) = into_table { - f.write_str(" ")?; - into_table.fmt(f)?; - } - Ok(()) - } - OutputClause::Returning { - returning_token: _, - select_items, - } => { - f.write_str("RETURNING ")?; - display_comma_separated(select_items).fmt(f) - } - } - } -} - #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 4851ce8a..34c67c6b 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -38,15 +38,15 @@ use super::{ FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, IndexColumn, Insert, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause, - MatchRecognizePattern, Measure, MergeAction, MergeClause, MergeInsertExpr, MergeInsertKind, - NamedParenthesizedList, NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, - OnConflictAction, OnInsert, OpenStatement, OrderBy, OrderByExpr, OrderByKind, OutputClause, - Partition, 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, Value, Values, ViewColumnDef, WhileStatement, - WildcardAdditionalOptions, With, WithFill, + MatchRecognizePattern, Measure, Merge, MergeAction, MergeClause, MergeInsertExpr, + MergeInsertKind, MergeUpdateExpr, NamedParenthesizedList, NamedWindowDefinition, ObjectName, + ObjectNamePart, Offset, OnConflict, OnConflictAction, OnInsert, OpenStatement, OrderBy, + OrderByExpr, OrderByKind, OutputClause, Partition, 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, Value, Values, + ViewColumnDef, WhileStatement, WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -451,20 +451,7 @@ impl Spanned for Statement { Statement::Explain { .. } => Span::empty(), Statement::Savepoint { .. } => Span::empty(), Statement::ReleaseSavepoint { .. } => Span::empty(), - Statement::Merge { - merge_token, - into: _, - table: _, - source: _, - on, - clauses, - output, - } => union_spans( - [merge_token.0.span, on.span()] - .into_iter() - .chain(clauses.iter().map(Spanned::span)) - .chain(output.iter().map(Spanned::span)), - ), + Statement::Merge(merge) => merge.span(), Statement::Cache { .. } => Span::empty(), Statement::UNCache { .. } => Span::empty(), Statement::CreateSequence { .. } => Span::empty(), @@ -925,6 +912,17 @@ impl Spanned for Update { } } +impl Spanned for Merge { + fn span(&self) -> Span { + union_spans( + [self.merge_token.0.span, self.on.span()] + .into_iter() + .chain(self.clauses.iter().map(Spanned::span)) + .chain(self.output.iter().map(Spanned::span)), + ) + } +} + impl Spanned for FromTable { fn span(&self) -> Span { match self { @@ -2419,17 +2417,7 @@ impl Spanned for MergeAction { fn span(&self) -> Span { match self { MergeAction::Insert(expr) => expr.span(), - MergeAction::Update { - update_token, - assignments, - update_predicate, - delete_predicate, - } => union_spans( - 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::Update(expr) => expr.span(), MergeAction::Delete { delete_token } => delete_token.0.span, } } @@ -2453,6 +2441,17 @@ impl Spanned for MergeInsertExpr { } } +impl Spanned for MergeUpdateExpr { + fn span(&self) -> Span { + union_spans( + core::iter::once(self.update_token.0.span) + .chain(self.assignments.iter().map(Spanned::span)) + .chain(self.update_predicate.iter().map(Spanned::span)) + .chain(self.delete_predicate.iter().map(Spanned::span)), + ) + } +} + impl Spanned for OutputClause { fn span(&self) -> Span { match self { @@ -2772,7 +2771,7 @@ WHERE id = 1 assert_eq!(stmt_span.end, (16, 67).into()); // ~ individual tokens within the statement - let Statement::Merge { + let Statement::Merge(Merge { merge_token, into: _, table: _, @@ -2780,7 +2779,7 @@ WHERE id = 1 on: _, clauses, output, - } = &r[0] + }) = &r[0] else { panic!("not a MERGE statement"); }; @@ -2818,12 +2817,12 @@ WHERE id = 1 clauses[1].when_token.0.span, Span::new(Location::new(12, 17), Location::new(12, 21)) ); - if let MergeAction::Update { + if let MergeAction::Update(MergeUpdateExpr { update_token, assignments: _, update_predicate: _, delete_predicate: _, - } = &clauses[1].action + }) = &clauses[1].action { assert_eq!( update_token.0.span, @@ -2896,7 +2895,7 @@ WHERE id = 1 ); // ~ individual tokens within the statement - if let Statement::Merge { output, .. } = &r[0] { + if let Statement::Merge(Merge { output, .. }) = &r[0] { if let Some(OutputClause::Returning { returning_token, .. }) = output @@ -2930,7 +2929,7 @@ WHERE id = 1 ); // ~ individual tokens within the statement - if let Statement::Merge { output, .. } = &r[0] { + if let Statement::Merge(Merge { output, .. }) = &r[0] { if let Some(OutputClause::Output { output_token, .. }) = output { assert_eq!( output_token.0.span, diff --git a/src/parser/merge.rs b/src/parser/merge.rs index 992ed391..23522268 100644 --- a/src/parser/merge.rs +++ b/src/parser/merge.rs @@ -17,8 +17,8 @@ use alloc::{boxed::Box, format, string::ToString, vec, vec::Vec}; use crate::{ ast::{ - Ident, MergeAction, MergeClause, MergeClauseKind, MergeInsertExpr, MergeInsertKind, - ObjectName, ObjectNamePart, SetExpr, Statement, TableFactor, + Ident, Merge, MergeAction, MergeClause, MergeClauseKind, MergeInsertExpr, MergeInsertKind, + MergeUpdateExpr, ObjectName, ObjectNamePart, OutputClause, SetExpr, Statement, TableFactor, }, dialect::{BigQueryDialect, GenericDialect, MySqlDialect}, keywords::Keyword, @@ -54,7 +54,7 @@ impl Parser<'_> { None => None, }; - Ok(Statement::Merge { + Ok(Statement::Merge(Merge { merge_token: merge_token.into(), into, table, @@ -62,7 +62,7 @@ impl Parser<'_> { on: Box::new(on), clauses, output, - }) + })) } fn parse_merge_clauses( @@ -134,12 +134,12 @@ impl Parser<'_> { } else { None }; - MergeAction::Update { + MergeAction::Update(MergeUpdateExpr { update_token: update_token.into(), assignments, update_predicate, delete_predicate, - } + }) } Some(Keyword::DELETE) => { if matches!( @@ -273,6 +273,33 @@ impl Parser<'_> { self.parse_parenthesized_column_list(IsOptional::Optional, allow_empty) } } + + fn parse_output( + &mut self, + start_keyword: Keyword, + start_token: TokenWithSpan, + ) -> Result { + let select_items = self.parse_projection()?; + let into_table = if start_keyword == Keyword::OUTPUT && self.peek_keyword(Keyword::INTO) { + self.expect_keyword_is(Keyword::INTO)?; + Some(self.parse_select_into()?) + } else { + None + }; + + Ok(if start_keyword == Keyword::OUTPUT { + OutputClause::Output { + output_token: start_token.into(), + select_items, + into_table, + } + } else { + OutputClause::Returning { + returning_token: start_token.into(), + select_items, + } + }) + } } /// Helper to unqualify a list of columns with either a qualified prefix or a diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 508205c4..55b7a0c0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17304,33 +17304,6 @@ impl<'a> Parser<'a> { }) } - fn parse_output( - &mut self, - start_keyword: Keyword, - start_token: TokenWithSpan, - ) -> Result { - let select_items = self.parse_projection()?; - let into_table = if start_keyword == Keyword::OUTPUT && self.peek_keyword(Keyword::INTO) { - self.expect_keyword_is(Keyword::INTO)?; - Some(self.parse_select_into()?) - } else { - None - }; - - Ok(if start_keyword == Keyword::OUTPUT { - OutputClause::Output { - output_token: start_token.into(), - select_items, - into_table, - } - } else { - OutputClause::Returning { - returning_token: start_token.into(), - select_items, - } - }) - } - fn parse_select_into(&mut self) -> Result { let temporary = self .parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY]) diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 7e82be3c..f82e011c 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1815,7 +1815,7 @@ fn parse_merge() { }), insert_predicate: None, }); - let update_action = MergeAction::Update { + let update_action = MergeAction::Update(MergeUpdateExpr { update_token: AttachedToken::empty(), assignments: vec![ Assignment { @@ -1829,17 +1829,17 @@ fn parse_merge() { ], update_predicate: None, delete_predicate: None, - }; + }); match bigquery_and_generic().verified_stmt(sql) { - Statement::Merge { + Statement::Merge(Merge { into, table, source, on, clauses, .. - } => { + }) => { assert!(!into); assert_eq!( TableFactor::Table { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 9206ccf5..031f324a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9797,22 +9797,22 @@ fn parse_merge() { let sql_no_into = "MERGE s.bar AS dest USING (SELECT * FROM s.foo) AS stg ON dest.D = stg.D AND dest.E = stg.E WHEN NOT MATCHED THEN INSERT (A, B, C) VALUES (stg.A, stg.B, stg.C) WHEN MATCHED AND dest.A = 'a' THEN UPDATE SET dest.F = stg.F, dest.G = stg.G WHEN MATCHED THEN DELETE"; match (verified_stmt(sql), verified_stmt(sql_no_into)) { ( - Statement::Merge { + Statement::Merge(Merge { into, table, source, on, clauses, .. - }, - Statement::Merge { + }), + Statement::Merge(Merge { into: no_into, table: table_no_into, source: source_no_into, on: on_no_into, clauses: clauses_no_into, .. - }, + }), ) => { assert!(into); assert!(!no_into); @@ -9961,7 +9961,7 @@ fn parse_merge() { (Value::SingleQuotedString("a".to_string())).with_empty_span() )), }), - action: MergeAction::Update { + action: MergeAction::Update(MergeUpdateExpr { update_token: AttachedToken::empty(), assignments: vec![ Assignment { @@ -9987,7 +9987,7 @@ fn parse_merge() { ], update_predicate: None, delete_predicate: None, - }, + }), }, MergeClause { when_token: AttachedToken::empty(),