Skip to content

Commit d1cbf67

Browse files
committed
MSSQL: Add support for OUTPUT clause on INSERT/UPDATE/DELETE
Signed-off-by: Guan-Ming Chiu <guanmingchiu@gmail.com>
1 parent bd7f70e commit d1cbf67

11 files changed

Lines changed: 120 additions & 7 deletions

File tree

src/ast/dml.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ pub struct Insert {
7979
pub on: Option<OnInsert>,
8080
/// RETURNING
8181
pub returning: Option<Vec<SelectItem>>,
82+
/// OUTPUT (MSSQL)
83+
pub output: Option<OutputClause>,
8284
/// Only for mysql
8385
pub replace_into: bool,
8486
/// Only for mysql
@@ -203,6 +205,11 @@ impl Display for Insert {
203205
SpaceOrNewline.fmt(f)?;
204206
}
205207

208+
if let Some(output) = &self.output {
209+
write!(f, "{output}")?;
210+
SpaceOrNewline.fmt(f)?;
211+
}
212+
206213
if let Some(settings) = &self.settings {
207214
write!(f, "SETTINGS {}", display_comma_separated(settings))?;
208215
SpaceOrNewline.fmt(f)?;
@@ -289,6 +296,8 @@ pub struct Delete {
289296
pub selection: Option<Expr>,
290297
/// RETURNING
291298
pub returning: Option<Vec<SelectItem>>,
299+
/// OUTPUT (MSSQL)
300+
pub output: Option<OutputClause>,
292301
/// ORDER BY (MySQL)
293302
pub order_by: Vec<OrderByExpr>,
294303
/// LIMIT (MySQL)
@@ -314,6 +323,10 @@ impl Display for Delete {
314323
indented_list(f, from)?;
315324
}
316325
}
326+
if let Some(output) = &self.output {
327+
SpaceOrNewline.fmt(f)?;
328+
write!(f, "{output}")?;
329+
}
317330
if let Some(using) = &self.using {
318331
SpaceOrNewline.fmt(f)?;
319332
f.write_str("USING")?;
@@ -367,6 +380,8 @@ pub struct Update {
367380
pub selection: Option<Expr>,
368381
/// RETURNING
369382
pub returning: Option<Vec<SelectItem>>,
383+
/// OUTPUT (MSSQL)
384+
pub output: Option<OutputClause>,
370385
/// SQLite-specific conflict resolution clause
371386
pub or: Option<SqliteOnConflict>,
372387
/// LIMIT
@@ -396,6 +411,10 @@ impl Display for Update {
396411
f.write_str("SET")?;
397412
indented_list(f, &self.assignments)?;
398413
}
414+
if let Some(output) = &self.output {
415+
SpaceOrNewline.fmt(f)?;
416+
write!(f, "{output}")?;
417+
}
399418
if let Some(UpdateTableFromKind::AfterSet(from)) = &self.from {
400419
SpaceOrNewline.fmt(f)?;
401420
f.write_str("FROM")?;
@@ -717,11 +736,11 @@ impl Display for MergeUpdateExpr {
717736
}
718737
}
719738

720-
/// A `OUTPUT` Clause in the end of a `MERGE` Statement
739+
/// An `OUTPUT` clause on `MERGE`, `INSERT`, `UPDATE`, or `DELETE` (MSSQL).
721740
///
722741
/// Example:
723742
/// OUTPUT $action, deleted.* INTO dbo.temp_products;
724-
/// [mssql](https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql)
743+
/// <https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql>
725744
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
726745
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
727746
#[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
@@ -1780,6 +1780,7 @@ fn parse_multi_table_insert(
17801780
has_table_keyword: false,
17811781
on: None,
17821782
returning: None,
1783+
output: None,
17831784
replace_into: false,
17841785
priority: None,
17851786
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ impl Parser<'_> {
218218
self.parse_parenthesized_qualified_column_list(IsOptional::Optional, allow_empty)
219219
}
220220

221-
fn parse_output(
221+
pub(super) fn parse_output(
222222
&mut self,
223223
start_keyword: Keyword,
224224
start_token: TokenWithSpan,

src/parser/mod.rs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13289,6 +13289,15 @@ impl<'a> Parser<'a> {
1328913289
};
1329013290

1329113291
let from = self.parse_comma_separated(Parser::parse_table_and_joins)?;
13292+
13293+
// MSSQL OUTPUT clause appears after FROM table, before USING/WHERE
13294+
// https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql
13295+
let output = if self.parse_keyword(Keyword::OUTPUT) {
13296+
Some(self.parse_output(Keyword::OUTPUT, self.get_current_token().clone())?)
13297+
} else {
13298+
None
13299+
};
13300+
1329213301
let using = if self.parse_keyword(Keyword::USING) {
1329313302
Some(self.parse_comma_separated(Parser::parse_table_and_joins)?)
1329413303
} else {
@@ -13327,6 +13336,7 @@ impl<'a> Parser<'a> {
1332713336
using,
1332813337
selection,
1332913338
returning,
13339+
output,
1333013340
order_by,
1333113341
limit,
1333213342
}))
@@ -17255,10 +17265,10 @@ impl<'a> Parser<'a> {
1725517265

1725617266
let is_mysql = dialect_of!(self is MySqlDialect);
1725717267

17258-
let (columns, partitioned, after_columns, source, assignments) = if self
17268+
let (columns, partitioned, after_columns, output, source, assignments) = if self
1725917269
.parse_keywords(&[Keyword::DEFAULT, Keyword::VALUES])
1726017270
{
17261-
(vec![], None, vec![], None, vec![])
17271+
(vec![], None, vec![], None, None, vec![])
1726217272
} else {
1726317273
let (columns, partitioned, after_columns) = if !self.peek_subquery_start() {
1726417274
let columns = self.parse_parenthesized_column_list(Optional, is_mysql)?;
@@ -17275,6 +17285,14 @@ impl<'a> Parser<'a> {
1727517285
Default::default()
1727617286
};
1727717287

17288+
// MSSQL OUTPUT clause appears between columns and source
17289+
// https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql
17290+
let output = if self.parse_keyword(Keyword::OUTPUT) {
17291+
Some(self.parse_output(Keyword::OUTPUT, self.get_current_token().clone())?)
17292+
} else {
17293+
None
17294+
};
17295+
1727817296
let (source, assignments) = if self.peek_keyword(Keyword::FORMAT)
1727917297
|| self.peek_keyword(Keyword::SETTINGS)
1728017298
{
@@ -17285,7 +17303,14 @@ impl<'a> Parser<'a> {
1728517303
(Some(self.parse_query()?), vec![])
1728617304
};
1728717305

17288-
(columns, partitioned, after_columns, source, assignments)
17306+
(
17307+
columns,
17308+
partitioned,
17309+
after_columns,
17310+
output,
17311+
source,
17312+
assignments,
17313+
)
1728917314
};
1729017315

1729117316
let (format_clause, settings) = if self.dialect.supports_insert_format() {
@@ -17387,6 +17412,7 @@ impl<'a> Parser<'a> {
1738717412
has_table_keyword: table,
1738817413
on,
1738917414
returning,
17415+
output,
1739017416
replace_into,
1739117417
priority,
1739217418
insert_alias,
@@ -17492,6 +17518,15 @@ impl<'a> Parser<'a> {
1749217518
};
1749317519
self.expect_keyword(Keyword::SET)?;
1749417520
let assignments = self.parse_comma_separated(Parser::parse_assignment)?;
17521+
17522+
// MSSQL OUTPUT clause appears after SET, before FROM/WHERE
17523+
// https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql
17524+
let output = if self.parse_keyword(Keyword::OUTPUT) {
17525+
Some(self.parse_output(Keyword::OUTPUT, self.get_current_token().clone())?)
17526+
} else {
17527+
None
17528+
};
17529+
1749517530
let from = if from_before_set.is_none() && self.parse_keyword(Keyword::FROM) {
1749617531
Some(UpdateTableFromKind::AfterSet(
1749717532
self.parse_table_with_joins()?,
@@ -17522,6 +17557,7 @@ impl<'a> Parser<'a> {
1752217557
from,
1752317558
selection,
1752417559
returning,
17560+
output,
1752517561
or,
1752617562
limit,
1752717563
}

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
@@ -2783,3 +2783,45 @@ fn test_tsql_statement_keywords_not_implicit_aliases() {
27832783
);
27842784
}
27852785
}
2786+
2787+
// MSSQL OUTPUT clause on INSERT/UPDATE/DELETE
2788+
// https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql
2789+
#[test]
2790+
fn parse_mssql_insert_with_output() {
2791+
ms_and_generic().verified_stmt(
2792+
"INSERT INTO customers (name, email) OUTPUT INSERTED.id, INSERTED.name VALUES ('John', 'john@example.com')",
2793+
);
2794+
}
2795+
2796+
#[test]
2797+
fn parse_mssql_insert_with_output_into() {
2798+
ms_and_generic().verified_stmt(
2799+
"INSERT INTO customers (name, email) OUTPUT INSERTED.id, INSERTED.name INTO @new_ids VALUES ('John', 'john@example.com')",
2800+
);
2801+
}
2802+
2803+
#[test]
2804+
fn parse_mssql_delete_with_output() {
2805+
ms_and_generic().verified_stmt("DELETE FROM customers OUTPUT DELETED.* WHERE id = 1");
2806+
}
2807+
2808+
#[test]
2809+
fn parse_mssql_delete_with_output_into() {
2810+
ms_and_generic().verified_stmt(
2811+
"DELETE FROM customers OUTPUT DELETED.id, DELETED.name INTO @deleted_rows WHERE active = 0",
2812+
);
2813+
}
2814+
2815+
#[test]
2816+
fn parse_mssql_update_with_output() {
2817+
ms_and_generic().verified_stmt(
2818+
"UPDATE employees SET salary = salary * 1.1 OUTPUT INSERTED.id, DELETED.salary, INSERTED.salary WHERE department = 'Engineering'",
2819+
);
2820+
}
2821+
2822+
#[test]
2823+
fn parse_mssql_update_with_output_into() {
2824+
ms_and_generic().verified_stmt(
2825+
"UPDATE employees SET salary = salary * 1.1 OUTPUT INSERTED.id, DELETED.salary, INSERTED.salary INTO @changes WHERE department = 'Engineering'",
2826+
);
2827+
}

tests/sqlparser_mysql.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2662,6 +2662,7 @@ fn parse_update_with_joins() {
26622662
limit: None,
26632663
optimizer_hints,
26642664
update_token: _,
2665+
output: _,
26652666
}) if optimizer_hints.is_empty() => {
26662667
assert_eq!(
26672668
TableWithJoins {

tests/sqlparser_postgres.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5491,6 +5491,7 @@ fn test_simple_postgres_insert_with_alias() {
54915491
has_table_keyword: false,
54925492
on: None,
54935493
returning: None,
5494+
output: None,
54945495
replace_into: false,
54955496
priority: None,
54965497
insert_alias: None,
@@ -5573,6 +5574,7 @@ fn test_simple_postgres_insert_with_alias() {
55735574
has_table_keyword: false,
55745575
on: None,
55755576
returning: None,
5577+
output: None,
55765578
replace_into: false,
55775579
priority: None,
55785580
insert_alias: None,
@@ -5653,6 +5655,7 @@ fn test_simple_insert_with_quoted_alias() {
56535655
has_table_keyword: false,
56545656
on: None,
56555657
returning: None,
5658+
output: None,
56565659
replace_into: false,
56575660
priority: None,
56585661
insert_alias: None,

0 commit comments

Comments
 (0)