Skip to content

Commit 9419e22

Browse files
dilovancelikayman-sigma
authored andcommitted
MSSQL: Add support for functionality MERGE output clause (apache#1790)
1 parent 101201d commit 9419e22

File tree

7 files changed

+109
-16
lines changed

7 files changed

+109
-16
lines changed

src/ast/mod.rs

+38-1
Original file line numberDiff line numberDiff line change
@@ -3835,6 +3835,7 @@ pub enum Statement {
38353835
/// ```
38363836
/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge)
38373837
/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement)
3838+
/// [MSSQL](https://learn.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql?view=sql-server-ver16)
38383839
Merge {
38393840
/// optional INTO keyword
38403841
into: bool,
@@ -3846,6 +3847,8 @@ pub enum Statement {
38463847
on: Box<Expr>,
38473848
/// Specifies the actions to perform when values match or do not match.
38483849
clauses: Vec<MergeClause>,
3850+
// Specifies the output to save changes in MSSQL
3851+
output: Option<OutputClause>,
38493852
},
38503853
/// ```sql
38513854
/// CACHE [ FLAG ] TABLE <table_name> [ OPTIONS('K1' = 'V1', 'K2' = V2) ] [ AS ] [ <query> ]
@@ -5425,14 +5428,19 @@ impl fmt::Display for Statement {
54255428
source,
54265429
on,
54275430
clauses,
5431+
output,
54285432
} => {
54295433
write!(
54305434
f,
54315435
"MERGE{int} {table} USING {source} ",
54325436
int = if *into { " INTO" } else { "" }
54335437
)?;
54345438
write!(f, "ON {on} ")?;
5435-
write!(f, "{}", display_separated(clauses, " "))
5439+
write!(f, "{}", display_separated(clauses, " "))?;
5440+
if let Some(output) = output {
5441+
write!(f, " {output}")?;
5442+
}
5443+
Ok(())
54365444
}
54375445
Statement::Cache {
54385446
table_name,
@@ -7963,6 +7971,35 @@ impl Display for MergeClause {
79637971
}
79647972
}
79657973

7974+
/// A Output Clause in the end of a 'MERGE' Statement
7975+
///
7976+
/// Example:
7977+
/// OUTPUT $action, deleted.* INTO dbo.temp_products;
7978+
/// [mssql](https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql)
7979+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
7980+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
7981+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
7982+
pub struct OutputClause {
7983+
pub select_items: Vec<SelectItem>,
7984+
pub into_table: SelectInto,
7985+
}
7986+
7987+
impl fmt::Display for OutputClause {
7988+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
7989+
let OutputClause {
7990+
select_items,
7991+
into_table,
7992+
} = self;
7993+
7994+
write!(
7995+
f,
7996+
"OUTPUT {} {}",
7997+
display_comma_separated(select_items),
7998+
into_table
7999+
)
8000+
}
8001+
}
8002+
79668003
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
79678004
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
79688005
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]

src/keywords.rs

+1
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,7 @@ define_keywords!(
632632
ORGANIZATION,
633633
OUT,
634634
OUTER,
635+
OUTPUT,
635636
OUTPUTFORMAT,
636637
OVER,
637638
OVERFLOW,

src/parser/mod.rs

+36-14
Original file line numberDiff line numberDiff line change
@@ -10920,18 +10920,7 @@ impl<'a> Parser<'a> {
1092010920
};
1092110921

1092210922
let into = if self.parse_keyword(Keyword::INTO) {
10923-
let temporary = self
10924-
.parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY])
10925-
.is_some();
10926-
let unlogged = self.parse_keyword(Keyword::UNLOGGED);
10927-
let table = self.parse_keyword(Keyword::TABLE);
10928-
let name = self.parse_object_name(false)?;
10929-
Some(SelectInto {
10930-
temporary,
10931-
unlogged,
10932-
table,
10933-
name,
10934-
})
10923+
Some(self.parse_select_into()?)
1093510924
} else {
1093610925
None
1093710926
};
@@ -14523,10 +14512,9 @@ impl<'a> Parser<'a> {
1452314512
pub fn parse_merge_clauses(&mut self) -> Result<Vec<MergeClause>, ParserError> {
1452414513
let mut clauses = vec![];
1452514514
loop {
14526-
if self.peek_token() == Token::EOF || self.peek_token() == Token::SemiColon {
14515+
if !(self.parse_keyword(Keyword::WHEN)) {
1452714516
break;
1452814517
}
14529-
self.expect_keyword_is(Keyword::WHEN)?;
1453014518

1453114519
let mut clause_kind = MergeClauseKind::Matched;
1453214520
if self.parse_keyword(Keyword::NOT) {
@@ -14620,6 +14608,34 @@ impl<'a> Parser<'a> {
1462014608
Ok(clauses)
1462114609
}
1462214610

14611+
fn parse_output(&mut self) -> Result<OutputClause, ParserError> {
14612+
self.expect_keyword_is(Keyword::OUTPUT)?;
14613+
let select_items = self.parse_projection()?;
14614+
self.expect_keyword_is(Keyword::INTO)?;
14615+
let into_table = self.parse_select_into()?;
14616+
14617+
Ok(OutputClause {
14618+
select_items,
14619+
into_table,
14620+
})
14621+
}
14622+
14623+
fn parse_select_into(&mut self) -> Result<SelectInto, ParserError> {
14624+
let temporary = self
14625+
.parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY])
14626+
.is_some();
14627+
let unlogged = self.parse_keyword(Keyword::UNLOGGED);
14628+
let table = self.parse_keyword(Keyword::TABLE);
14629+
let name = self.parse_object_name(false)?;
14630+
14631+
Ok(SelectInto {
14632+
temporary,
14633+
unlogged,
14634+
table,
14635+
name,
14636+
})
14637+
}
14638+
1462314639
pub fn parse_merge(&mut self) -> Result<Statement, ParserError> {
1462414640
let into = self.parse_keyword(Keyword::INTO);
1462514641

@@ -14630,13 +14646,19 @@ impl<'a> Parser<'a> {
1463014646
self.expect_keyword_is(Keyword::ON)?;
1463114647
let on = self.parse_expr()?;
1463214648
let clauses = self.parse_merge_clauses()?;
14649+
let output = if self.peek_keyword(Keyword::OUTPUT) {
14650+
Some(self.parse_output()?)
14651+
} else {
14652+
None
14653+
};
1463314654

1463414655
Ok(Statement::Merge {
1463514656
into,
1463614657
table,
1463714658
source,
1463814659
on: Box::new(on),
1463914660
clauses,
14661+
output,
1464014662
})
1464114663
}
1464214664

tests/sqlparser_bigquery.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1735,13 +1735,15 @@ fn parse_merge() {
17351735
},
17361736
],
17371737
};
1738+
17381739
match bigquery_and_generic().verified_stmt(sql) {
17391740
Statement::Merge {
17401741
into,
17411742
table,
17421743
source,
17431744
on,
17441745
clauses,
1746+
..
17451747
} => {
17461748
assert!(!into);
17471749
assert_eq!(

tests/sqlparser_common.rs

+15
Original file line numberDiff line numberDiff line change
@@ -9360,13 +9360,15 @@ fn parse_merge() {
93609360
source,
93619361
on,
93629362
clauses,
9363+
..
93639364
},
93649365
Statement::Merge {
93659366
into: no_into,
93669367
table: table_no_into,
93679368
source: source_no_into,
93689369
on: on_no_into,
93699370
clauses: clauses_no_into,
9371+
..
93709372
},
93719373
) => {
93729374
assert!(into);
@@ -9559,6 +9561,19 @@ fn parse_merge() {
95599561
verified_stmt(sql);
95609562
}
95619563

9564+
#[test]
9565+
fn test_merge_with_output() {
9566+
let sql = "MERGE INTO target_table USING source_table \
9567+
ON target_table.id = source_table.oooid \
9568+
WHEN MATCHED THEN \
9569+
UPDATE SET target_table.description = source_table.description \
9570+
WHEN NOT MATCHED THEN \
9571+
INSERT (ID, description) VALUES (source_table.id, source_table.description) \
9572+
OUTPUT inserted.* INTO log_target";
9573+
9574+
verified_stmt(sql);
9575+
}
9576+
95629577
#[test]
95639578
fn test_merge_into_using_table() {
95649579
let sql = "MERGE INTO target_table USING source_table \

tests/sqlparser_mssql.rs

+16
Original file line numberDiff line numberDiff line change
@@ -1921,3 +1921,19 @@ fn ms() -> TestedDialects {
19211921
fn ms_and_generic() -> TestedDialects {
19221922
TestedDialects::new(vec![Box::new(MsSqlDialect {}), Box::new(GenericDialect {})])
19231923
}
1924+
1925+
#[test]
1926+
fn parse_mssql_merge_with_output() {
1927+
let stmt = "MERGE dso.products AS t \
1928+
USING dsi.products AS \
1929+
s ON s.ProductID = t.ProductID \
1930+
WHEN MATCHED AND \
1931+
NOT (t.ProductName = s.ProductName OR (ISNULL(t.ProductName, s.ProductName) IS NULL)) \
1932+
THEN UPDATE SET t.ProductName = s.ProductName \
1933+
WHEN NOT MATCHED BY TARGET \
1934+
THEN INSERT (ProductID, ProductName) \
1935+
VALUES (s.ProductID, s.ProductName) \
1936+
WHEN NOT MATCHED BY SOURCE THEN DELETE \
1937+
OUTPUT $action, deleted.ProductID INTO dsi.temp_products";
1938+
ms_and_generic().verified_stmt(stmt);
1939+
}

tests/sqlparser_redshift.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -395,5 +395,5 @@ fn test_parse_nested_quoted_identifier() {
395395
#[test]
396396
fn parse_extract_single_quotes() {
397397
let sql = "SELECT EXTRACT('month' FROM my_timestamp) FROM my_table";
398-
redshift().verified_stmt(&sql);
398+
redshift().verified_stmt(sql);
399399
}

0 commit comments

Comments
 (0)