Skip to content

Commit 31e1942

Browse files
guan404mingiffyio
andauthored
MSSQL: Add support for OUTPUT clause on INSERT/UPDATE/DELETE (apache#2228)
Signed-off-by: Guan-Ming Chiu <guanmingchiu@gmail.com> Signed-off-by: Guan-Ming (Wesley) Chiu <105915352+guan404ming@users.noreply.github.com> Co-authored-by: Ifeanyi Ubah <ifeanyi@validio.io>
1 parent 49bdb5c commit 31e1942

File tree

11 files changed

+119
-7
lines changed

11 files changed

+119
-7
lines changed

src/ast/dml.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ pub struct Insert {
7979
pub on: Option<OnInsert>,
8080
/// RETURNING
8181
pub returning: Option<Vec<SelectItem>>,
82+
/// OUTPUT (MSSQL)
83+
/// See <https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql>
84+
pub output: Option<OutputClause>,
8285
/// Only for mysql
8386
pub replace_into: bool,
8487
/// Only for mysql
@@ -203,6 +206,11 @@ impl Display for Insert {
203206
SpaceOrNewline.fmt(f)?;
204207
}
205208

209+
if let Some(output) = &self.output {
210+
write!(f, "{output}")?;
211+
SpaceOrNewline.fmt(f)?;
212+
}
213+
206214
if let Some(settings) = &self.settings {
207215
write!(f, "SETTINGS {}", display_comma_separated(settings))?;
208216
SpaceOrNewline.fmt(f)?;
@@ -289,6 +297,9 @@ pub struct Delete {
289297
pub selection: Option<Expr>,
290298
/// RETURNING
291299
pub returning: Option<Vec<SelectItem>>,
300+
/// OUTPUT (MSSQL)
301+
/// See <https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql>
302+
pub output: Option<OutputClause>,
292303
/// ORDER BY (MySQL)
293304
pub order_by: Vec<OrderByExpr>,
294305
/// LIMIT (MySQL)
@@ -314,6 +325,10 @@ impl Display for Delete {
314325
indented_list(f, from)?;
315326
}
316327
}
328+
if let Some(output) = &self.output {
329+
SpaceOrNewline.fmt(f)?;
330+
write!(f, "{output}")?;
331+
}
317332
if let Some(using) = &self.using {
318333
SpaceOrNewline.fmt(f)?;
319334
f.write_str("USING")?;
@@ -367,6 +382,9 @@ pub struct Update {
367382
pub selection: Option<Expr>,
368383
/// RETURNING
369384
pub returning: Option<Vec<SelectItem>>,
385+
/// OUTPUT (MSSQL)
386+
/// See <https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql>
387+
pub output: Option<OutputClause>,
370388
/// SQLite-specific conflict resolution clause
371389
pub or: Option<SqliteOnConflict>,
372390
/// LIMIT
@@ -396,6 +414,10 @@ impl Display for Update {
396414
f.write_str("SET")?;
397415
indented_list(f, &self.assignments)?;
398416
}
417+
if let Some(output) = &self.output {
418+
SpaceOrNewline.fmt(f)?;
419+
write!(f, "{output}")?;
420+
}
399421
if let Some(UpdateTableFromKind::AfterSet(from)) = &self.from {
400422
SpaceOrNewline.fmt(f)?;
401423
f.write_str("FROM")?;
@@ -717,11 +739,11 @@ impl Display for MergeUpdateExpr {
717739
}
718740
}
719741

720-
/// A `OUTPUT` Clause in the end of a `MERGE` Statement
742+
/// An `OUTPUT` clause on `MERGE`, `INSERT`, `UPDATE`, or `DELETE` (MSSQL).
721743
///
722744
/// Example:
723745
/// OUTPUT $action, deleted.* INTO dbo.temp_products;
724-
/// [mssql](https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql)
746+
/// <https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql>
725747
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
726748
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
727749
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]

src/ast/spans.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,7 @@ impl Spanned for Delete {
906906
using,
907907
selection,
908908
returning,
909+
output,
909910
order_by,
910911
limit,
911912
} = self;
@@ -923,6 +924,7 @@ impl Spanned for Delete {
923924
)
924925
.chain(selection.iter().map(|i| i.span()))
925926
.chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span())))
927+
.chain(output.iter().map(|i| i.span()))
926928
.chain(order_by.iter().map(|i| i.span()))
927929
.chain(limit.iter().map(|i| i.span())),
928930
),
@@ -940,6 +942,7 @@ impl Spanned for Update {
940942
from,
941943
selection,
942944
returning,
945+
output,
943946
or: _,
944947
limit,
945948
} = self;
@@ -951,6 +954,7 @@ impl Spanned for Update {
951954
.chain(from.iter().map(|i| i.span()))
952955
.chain(selection.iter().map(|i| i.span()))
953956
.chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span())))
957+
.chain(output.iter().map(|i| i.span()))
954958
.chain(limit.iter().map(|i| i.span())),
955959
)
956960
}
@@ -1312,6 +1316,7 @@ impl Spanned for Insert {
13121316
has_table_keyword: _, // bool
13131317
on,
13141318
returning,
1319+
output,
13151320
replace_into: _, // bool
13161321
priority: _, // todo, mysql specific
13171322
insert_alias: _, // todo, mysql specific
@@ -1334,7 +1339,8 @@ impl Spanned for Insert {
13341339
.chain(partitioned.iter().flat_map(|i| i.iter().map(|k| k.span())))
13351340
.chain(after_columns.iter().map(|i| i.span))
13361341
.chain(on.as_ref().map(|i| i.span()))
1337-
.chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span()))),
1342+
.chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span())))
1343+
.chain(output.iter().map(|i| i.span())),
13381344
)
13391345
}
13401346
}

src/dialect/snowflake.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1784,6 +1784,7 @@ fn parse_multi_table_insert(
17841784
has_table_keyword: false,
17851785
on: None,
17861786
returning: None,
1787+
output: None,
17871788
replace_into: false,
17881789
priority: None,
17891790
insert_alias: None,

src/keywords.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,6 +1210,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[
12101210
Keyword::ANTI,
12111211
Keyword::SEMI,
12121212
Keyword::RETURNING,
1213+
Keyword::OUTPUT,
12131214
Keyword::ASOF,
12141215
Keyword::MATCH_CONDITION,
12151216
// for MSSQL-specific OUTER APPLY (seems reserved in most dialects)
@@ -1264,6 +1265,7 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[
12641265
Keyword::CLUSTER,
12651266
Keyword::DISTRIBUTE,
12661267
Keyword::RETURNING,
1268+
Keyword::VALUES,
12671269
// Reserved only as a column alias in the `SELECT` clause
12681270
Keyword::FROM,
12691271
Keyword::INTO,

src/parser/merge.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,21 @@ impl Parser<'_> {
218218
self.parse_parenthesized_qualified_column_list(IsOptional::Optional, allow_empty)
219219
}
220220

221-
fn parse_output(
221+
/// Parses an `OUTPUT` clause if present (MSSQL).
222+
pub(super) fn maybe_parse_output_clause(
223+
&mut self,
224+
) -> Result<Option<OutputClause>, ParserError> {
225+
if self.parse_keyword(Keyword::OUTPUT) {
226+
Ok(Some(self.parse_output(
227+
Keyword::OUTPUT,
228+
self.get_current_token().clone(),
229+
)?))
230+
} else {
231+
Ok(None)
232+
}
233+
}
234+
235+
pub(super) fn parse_output(
222236
&mut self,
223237
start_keyword: Keyword,
224238
start_token: TokenWithSpan,

src/parser/mod.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13309,6 +13309,9 @@ impl<'a> Parser<'a> {
1330913309
};
1331013310

1331113311
let from = self.parse_comma_separated(Parser::parse_table_and_joins)?;
13312+
13313+
let output = self.maybe_parse_output_clause()?;
13314+
1331213315
let using = if self.parse_keyword(Keyword::USING) {
1331313316
Some(self.parse_comma_separated(Parser::parse_table_and_joins)?)
1331413317
} else {
@@ -13347,6 +13350,7 @@ impl<'a> Parser<'a> {
1334713350
using,
1334813351
selection,
1334913352
returning,
13353+
output,
1335013354
order_by,
1335113355
limit,
1335213356
}))
@@ -17275,10 +17279,10 @@ impl<'a> Parser<'a> {
1727517279

1727617280
let is_mysql = dialect_of!(self is MySqlDialect);
1727717281

17278-
let (columns, partitioned, after_columns, source, assignments) = if self
17282+
let (columns, partitioned, after_columns, output, source, assignments) = if self
1727917283
.parse_keywords(&[Keyword::DEFAULT, Keyword::VALUES])
1728017284
{
17281-
(vec![], None, vec![], None, vec![])
17285+
(vec![], None, vec![], None, None, vec![])
1728217286
} else {
1728317287
let (columns, partitioned, after_columns) = if !self.peek_subquery_start() {
1728417288
let columns = self.parse_parenthesized_column_list(Optional, is_mysql)?;
@@ -17295,6 +17299,8 @@ impl<'a> Parser<'a> {
1729517299
Default::default()
1729617300
};
1729717301

17302+
let output = self.maybe_parse_output_clause()?;
17303+
1729817304
let (source, assignments) = if self.peek_keyword(Keyword::FORMAT)
1729917305
|| self.peek_keyword(Keyword::SETTINGS)
1730017306
{
@@ -17305,7 +17311,14 @@ impl<'a> Parser<'a> {
1730517311
(Some(self.parse_query()?), vec![])
1730617312
};
1730717313

17308-
(columns, partitioned, after_columns, source, assignments)
17314+
(
17315+
columns,
17316+
partitioned,
17317+
after_columns,
17318+
output,
17319+
source,
17320+
assignments,
17321+
)
1730917322
};
1731017323

1731117324
let (format_clause, settings) = if self.dialect.supports_insert_format() {
@@ -17407,6 +17420,7 @@ impl<'a> Parser<'a> {
1740717420
has_table_keyword: table,
1740817421
on,
1740917422
returning,
17423+
output,
1741017424
replace_into,
1741117425
priority,
1741217426
insert_alias,
@@ -17512,6 +17526,9 @@ impl<'a> Parser<'a> {
1751217526
};
1751317527
self.expect_keyword(Keyword::SET)?;
1751417528
let assignments = self.parse_comma_separated(Parser::parse_assignment)?;
17529+
17530+
let output = self.maybe_parse_output_clause()?;
17531+
1751517532
let from = if from_before_set.is_none() && self.parse_keyword(Keyword::FROM) {
1751617533
Some(UpdateTableFromKind::AfterSet(
1751717534
self.parse_table_with_joins()?,
@@ -17542,6 +17559,7 @@ impl<'a> Parser<'a> {
1754217559
from,
1754317560
selection,
1754417561
returning,
17562+
output,
1754517563
or,
1754617564
limit,
1754717565
}

tests/sqlparser_common.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,7 @@ fn parse_update_set_from() {
530530
])),
531531
}),
532532
returning: None,
533+
output: None,
533534
or: None,
534535
limit: None
535536
})
@@ -553,6 +554,7 @@ fn parse_update_with_table_alias() {
553554
limit: None,
554555
optimizer_hints,
555556
update_token: _,
557+
output: _,
556558
}) if optimizer_hints.is_empty() => {
557559
assert_eq!(
558560
TableWithJoins {

tests/sqlparser_mssql.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2806,3 +2806,45 @@ fn test_exec_dynamic_sql() {
28062806
.expect("EXEC (@sql) followed by DROP TABLE should parse");
28072807
assert_eq!(stmts.len(), 2);
28082808
}
2809+
2810+
// MSSQL OUTPUT clause on INSERT/UPDATE/DELETE
2811+
// https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql
2812+
#[test]
2813+
fn parse_mssql_insert_with_output() {
2814+
ms_and_generic().verified_stmt(
2815+
"INSERT INTO customers (name, email) OUTPUT INSERTED.id, INSERTED.name VALUES ('John', 'john@example.com')",
2816+
);
2817+
}
2818+
2819+
#[test]
2820+
fn parse_mssql_insert_with_output_into() {
2821+
ms_and_generic().verified_stmt(
2822+
"INSERT INTO customers (name, email) OUTPUT INSERTED.id, INSERTED.name INTO @new_ids VALUES ('John', 'john@example.com')",
2823+
);
2824+
}
2825+
2826+
#[test]
2827+
fn parse_mssql_delete_with_output() {
2828+
ms_and_generic().verified_stmt("DELETE FROM customers OUTPUT DELETED.* WHERE id = 1");
2829+
}
2830+
2831+
#[test]
2832+
fn parse_mssql_delete_with_output_into() {
2833+
ms_and_generic().verified_stmt(
2834+
"DELETE FROM customers OUTPUT DELETED.id, DELETED.name INTO @deleted_rows WHERE active = 0",
2835+
);
2836+
}
2837+
2838+
#[test]
2839+
fn parse_mssql_update_with_output() {
2840+
ms_and_generic().verified_stmt(
2841+
"UPDATE employees SET salary = salary * 1.1 OUTPUT INSERTED.id, DELETED.salary, INSERTED.salary WHERE department = 'Engineering'",
2842+
);
2843+
}
2844+
2845+
#[test]
2846+
fn parse_mssql_update_with_output_into() {
2847+
ms_and_generic().verified_stmt(
2848+
"UPDATE employees SET salary = salary * 1.1 OUTPUT INSERTED.id, DELETED.salary, INSERTED.salary INTO @changes WHERE department = 'Engineering'",
2849+
);
2850+
}

tests/sqlparser_mysql.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2671,6 +2671,7 @@ fn parse_update_with_joins() {
26712671
limit: None,
26722672
optimizer_hints,
26732673
update_token: _,
2674+
output: _,
26742675
}) if optimizer_hints.is_empty() => {
26752676
assert_eq!(
26762677
TableWithJoins {

tests/sqlparser_postgres.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5530,6 +5530,7 @@ fn test_simple_postgres_insert_with_alias() {
55305530
has_table_keyword: false,
55315531
on: None,
55325532
returning: None,
5533+
output: None,
55335534
replace_into: false,
55345535
priority: None,
55355536
insert_alias: None,
@@ -5612,6 +5613,7 @@ fn test_simple_postgres_insert_with_alias() {
56125613
has_table_keyword: false,
56135614
on: None,
56145615
returning: None,
5616+
output: None,
56155617
replace_into: false,
56165618
priority: None,
56175619
insert_alias: None,
@@ -5692,6 +5694,7 @@ fn test_simple_insert_with_quoted_alias() {
56925694
has_table_keyword: false,
56935695
on: None,
56945696
returning: None,
5697+
output: None,
56955698
replace_into: false,
56965699
priority: None,
56975700
insert_alias: None,

0 commit comments

Comments
 (0)