Skip to content

Commit 0d25d93

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

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
@@ -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: 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
@@ -13309,6 +13309,15 @@ impl<'a> Parser<'a> {
1330913309
};
1331013310

1331113311
let from = self.parse_comma_separated(Parser::parse_table_and_joins)?;
13312+
13313+
// MSSQL OUTPUT clause appears after FROM table, before USING/WHERE
13314+
// https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql
13315+
let output = if self.parse_keyword(Keyword::OUTPUT) {
13316+
Some(self.parse_output(Keyword::OUTPUT, self.get_current_token().clone())?)
13317+
} else {
13318+
None
13319+
};
13320+
1331213321
let using = if self.parse_keyword(Keyword::USING) {
1331313322
Some(self.parse_comma_separated(Parser::parse_table_and_joins)?)
1331413323
} else {
@@ -13347,6 +13356,7 @@ impl<'a> Parser<'a> {
1334713356
using,
1334813357
selection,
1334913358
returning,
13359+
output,
1335013360
order_by,
1335113361
limit,
1335213362
}))
@@ -17275,10 +17285,10 @@ impl<'a> Parser<'a> {
1727517285

1727617286
let is_mysql = dialect_of!(self is MySqlDialect);
1727717287

17278-
let (columns, partitioned, after_columns, source, assignments) = if self
17288+
let (columns, partitioned, after_columns, output, source, assignments) = if self
1727917289
.parse_keywords(&[Keyword::DEFAULT, Keyword::VALUES])
1728017290
{
17281-
(vec![], None, vec![], None, vec![])
17291+
(vec![], None, vec![], None, None, vec![])
1728217292
} else {
1728317293
let (columns, partitioned, after_columns) = if !self.peek_subquery_start() {
1728417294
let columns = self.parse_parenthesized_column_list(Optional, is_mysql)?;
@@ -17295,6 +17305,14 @@ impl<'a> Parser<'a> {
1729517305
Default::default()
1729617306
};
1729717307

17308+
// MSSQL OUTPUT clause appears between columns and source
17309+
// https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql
17310+
let output = if self.parse_keyword(Keyword::OUTPUT) {
17311+
Some(self.parse_output(Keyword::OUTPUT, self.get_current_token().clone())?)
17312+
} else {
17313+
None
17314+
};
17315+
1729817316
let (source, assignments) = if self.peek_keyword(Keyword::FORMAT)
1729917317
|| self.peek_keyword(Keyword::SETTINGS)
1730017318
{
@@ -17305,7 +17323,14 @@ impl<'a> Parser<'a> {
1730517323
(Some(self.parse_query()?), vec![])
1730617324
};
1730717325

17308-
(columns, partitioned, after_columns, source, assignments)
17326+
(
17327+
columns,
17328+
partitioned,
17329+
after_columns,
17330+
output,
17331+
source,
17332+
assignments,
17333+
)
1730917334
};
1731017335

1731117336
let (format_clause, settings) = if self.dialect.supports_insert_format() {
@@ -17407,6 +17432,7 @@ impl<'a> Parser<'a> {
1740717432
has_table_keyword: table,
1740817433
on,
1740917434
returning,
17435+
output,
1741017436
replace_into,
1741117437
priority,
1741217438
insert_alias,
@@ -17512,6 +17538,15 @@ impl<'a> Parser<'a> {
1751217538
};
1751317539
self.expect_keyword(Keyword::SET)?;
1751417540
let assignments = self.parse_comma_separated(Parser::parse_assignment)?;
17541+
17542+
// MSSQL OUTPUT clause appears after SET, before FROM/WHERE
17543+
// https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql
17544+
let output = if self.parse_keyword(Keyword::OUTPUT) {
17545+
Some(self.parse_output(Keyword::OUTPUT, self.get_current_token().clone())?)
17546+
} else {
17547+
None
17548+
};
17549+
1751517550
let from = if from_before_set.is_none() && self.parse_keyword(Keyword::FROM) {
1751617551
Some(UpdateTableFromKind::AfterSet(
1751717552
self.parse_table_with_joins()?,
@@ -17542,6 +17577,7 @@ impl<'a> Parser<'a> {
1754217577
from,
1754317578
selection,
1754417579
returning,
17580+
output,
1754517581
or,
1754617582
limit,
1754717583
}

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)