MSSQL: Add support for functionality MERGE output clause (#1790)

This commit is contained in:
DilovanCelik 2025-04-05 18:37:28 +00:00 committed by GitHub
parent 3ed4ad9c66
commit 610096cad8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 109 additions and 16 deletions

View file

@ -3817,6 +3817,7 @@ pub enum Statement {
/// ``` /// ```
/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge)
/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) /// [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 { Merge {
/// optional INTO keyword /// optional INTO keyword
into: bool, into: bool,
@ -3828,6 +3829,8 @@ pub enum Statement {
on: Box<Expr>, on: Box<Expr>,
/// Specifies the actions to perform when values match or do not match. /// Specifies the actions to perform when values match or do not match.
clauses: Vec<MergeClause>, clauses: Vec<MergeClause>,
// Specifies the output to save changes in MSSQL
output: Option<OutputClause>,
}, },
/// ```sql /// ```sql
/// CACHE [ FLAG ] TABLE <table_name> [ OPTIONS('K1' = 'V1', 'K2' = V2) ] [ AS ] [ <query> ] /// CACHE [ FLAG ] TABLE <table_name> [ OPTIONS('K1' = 'V1', 'K2' = V2) ] [ AS ] [ <query> ]
@ -5407,6 +5410,7 @@ impl fmt::Display for Statement {
source, source,
on, on,
clauses, clauses,
output,
} => { } => {
write!( write!(
f, f,
@ -5414,7 +5418,11 @@ impl fmt::Display for Statement {
int = if *into { " INTO" } else { "" } int = if *into { " INTO" } else { "" }
)?; )?;
write!(f, "ON {on} ")?; write!(f, "ON {on} ")?;
write!(f, "{}", display_separated(clauses, " ")) write!(f, "{}", display_separated(clauses, " "))?;
if let Some(output) = output {
write!(f, " {output}")?;
}
Ok(())
} }
Statement::Cache { Statement::Cache {
table_name, table_name,
@ -7945,6 +7953,35 @@ impl Display for MergeClause {
} }
} }
/// 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 struct OutputClause {
pub select_items: Vec<SelectItem>,
pub into_table: SelectInto,
}
impl fmt::Display for OutputClause {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let OutputClause {
select_items,
into_table,
} = self;
write!(
f,
"OUTPUT {} {}",
display_comma_separated(select_items),
into_table
)
}
}
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]

View file

@ -632,6 +632,7 @@ define_keywords!(
ORGANIZATION, ORGANIZATION,
OUT, OUT,
OUTER, OUTER,
OUTPUT,
OUTPUTFORMAT, OUTPUTFORMAT,
OVER, OVER,
OVERFLOW, OVERFLOW,

View file

@ -10910,18 +10910,7 @@ impl<'a> Parser<'a> {
}; };
let into = if self.parse_keyword(Keyword::INTO) { let into = if self.parse_keyword(Keyword::INTO) {
let temporary = self Some(self.parse_select_into()?)
.parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY])
.is_some();
let unlogged = self.parse_keyword(Keyword::UNLOGGED);
let table = self.parse_keyword(Keyword::TABLE);
let name = self.parse_object_name(false)?;
Some(SelectInto {
temporary,
unlogged,
table,
name,
})
} else { } else {
None None
}; };
@ -14513,10 +14502,9 @@ impl<'a> Parser<'a> {
pub fn parse_merge_clauses(&mut self) -> Result<Vec<MergeClause>, ParserError> { pub fn parse_merge_clauses(&mut self) -> Result<Vec<MergeClause>, ParserError> {
let mut clauses = vec![]; let mut clauses = vec![];
loop { loop {
if self.peek_token() == Token::EOF || self.peek_token() == Token::SemiColon { if !(self.parse_keyword(Keyword::WHEN)) {
break; break;
} }
self.expect_keyword_is(Keyword::WHEN)?;
let mut clause_kind = MergeClauseKind::Matched; let mut clause_kind = MergeClauseKind::Matched;
if self.parse_keyword(Keyword::NOT) { if self.parse_keyword(Keyword::NOT) {
@ -14610,6 +14598,34 @@ impl<'a> Parser<'a> {
Ok(clauses) Ok(clauses)
} }
fn parse_output(&mut self) -> Result<OutputClause, ParserError> {
self.expect_keyword_is(Keyword::OUTPUT)?;
let select_items = self.parse_projection()?;
self.expect_keyword_is(Keyword::INTO)?;
let into_table = self.parse_select_into()?;
Ok(OutputClause {
select_items,
into_table,
})
}
fn parse_select_into(&mut self) -> Result<SelectInto, ParserError> {
let temporary = self
.parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY])
.is_some();
let unlogged = self.parse_keyword(Keyword::UNLOGGED);
let table = self.parse_keyword(Keyword::TABLE);
let name = self.parse_object_name(false)?;
Ok(SelectInto {
temporary,
unlogged,
table,
name,
})
}
pub fn parse_merge(&mut self) -> Result<Statement, ParserError> { pub fn parse_merge(&mut self) -> Result<Statement, ParserError> {
let into = self.parse_keyword(Keyword::INTO); let into = self.parse_keyword(Keyword::INTO);
@ -14620,6 +14636,11 @@ impl<'a> Parser<'a> {
self.expect_keyword_is(Keyword::ON)?; self.expect_keyword_is(Keyword::ON)?;
let on = self.parse_expr()?; let on = self.parse_expr()?;
let clauses = self.parse_merge_clauses()?; let clauses = self.parse_merge_clauses()?;
let output = if self.peek_keyword(Keyword::OUTPUT) {
Some(self.parse_output()?)
} else {
None
};
Ok(Statement::Merge { Ok(Statement::Merge {
into, into,
@ -14627,6 +14648,7 @@ impl<'a> Parser<'a> {
source, source,
on: Box::new(on), on: Box::new(on),
clauses, clauses,
output,
}) })
} }

View file

@ -1735,6 +1735,7 @@ fn parse_merge() {
}, },
], ],
}; };
match bigquery_and_generic().verified_stmt(sql) { match bigquery_and_generic().verified_stmt(sql) {
Statement::Merge { Statement::Merge {
into, into,
@ -1742,6 +1743,7 @@ fn parse_merge() {
source, source,
on, on,
clauses, clauses,
..
} => { } => {
assert!(!into); assert!(!into);
assert_eq!( assert_eq!(

View file

@ -9359,6 +9359,7 @@ fn parse_merge() {
source, source,
on, on,
clauses, clauses,
..
}, },
Statement::Merge { Statement::Merge {
into: no_into, into: no_into,
@ -9366,6 +9367,7 @@ fn parse_merge() {
source: source_no_into, source: source_no_into,
on: on_no_into, on: on_no_into,
clauses: clauses_no_into, clauses: clauses_no_into,
..
}, },
) => { ) => {
assert!(into); assert!(into);
@ -9558,6 +9560,19 @@ fn parse_merge() {
verified_stmt(sql); verified_stmt(sql);
} }
#[test]
fn test_merge_with_output() {
let sql = "MERGE INTO target_table USING source_table \
ON target_table.id = source_table.oooid \
WHEN MATCHED THEN \
UPDATE SET target_table.description = source_table.description \
WHEN NOT MATCHED THEN \
INSERT (ID, description) VALUES (source_table.id, source_table.description) \
OUTPUT inserted.* INTO log_target";
verified_stmt(sql);
}
#[test] #[test]
fn test_merge_into_using_table() { fn test_merge_into_using_table() {
let sql = "MERGE INTO target_table USING source_table \ let sql = "MERGE INTO target_table USING source_table \

View file

@ -1921,3 +1921,19 @@ fn ms() -> TestedDialects {
fn ms_and_generic() -> TestedDialects { fn ms_and_generic() -> TestedDialects {
TestedDialects::new(vec![Box::new(MsSqlDialect {}), Box::new(GenericDialect {})]) TestedDialects::new(vec![Box::new(MsSqlDialect {}), Box::new(GenericDialect {})])
} }
#[test]
fn parse_mssql_merge_with_output() {
let stmt = "MERGE dso.products AS t \
USING dsi.products AS \
s ON s.ProductID = t.ProductID \
WHEN MATCHED AND \
NOT (t.ProductName = s.ProductName OR (ISNULL(t.ProductName, s.ProductName) IS NULL)) \
THEN UPDATE SET t.ProductName = s.ProductName \
WHEN NOT MATCHED BY TARGET \
THEN INSERT (ProductID, ProductName) \
VALUES (s.ProductID, s.ProductName) \
WHEN NOT MATCHED BY SOURCE THEN DELETE \
OUTPUT $action, deleted.ProductID INTO dsi.temp_products";
ms_and_generic().verified_stmt(stmt);
}

View file

@ -395,5 +395,5 @@ fn test_parse_nested_quoted_identifier() {
#[test] #[test]
fn parse_extract_single_quotes() { fn parse_extract_single_quotes() {
let sql = "SELECT EXTRACT('month' FROM my_timestamp) FROM my_table"; let sql = "SELECT EXTRACT('month' FROM my_timestamp) FROM my_table";
redshift().verified_stmt(&sql); redshift().verified_stmt(sql);
} }