Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions src/ast/dml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ pub struct Insert {
pub on: Option<OnInsert>,
/// RETURNING
pub returning: Option<Vec<SelectItem>>,
/// OUTPUT (MSSQL)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for these comments, could we include the link to the mssql docs describing the syntax? it would help with nagivation when folks look at the struct

pub output: Option<OutputClause>,
/// Only for mysql
pub replace_into: bool,
/// Only for mysql
Expand Down Expand Up @@ -196,6 +198,11 @@ impl Display for Insert {
SpaceOrNewline.fmt(f)?;
}

if let Some(output) = &self.output {
write!(f, "{output}")?;
SpaceOrNewline.fmt(f)?;
}

if let Some(settings) = &self.settings {
write!(f, "SETTINGS {}", display_comma_separated(settings))?;
SpaceOrNewline.fmt(f)?;
Expand Down Expand Up @@ -282,6 +289,8 @@ pub struct Delete {
pub selection: Option<Expr>,
/// RETURNING
pub returning: Option<Vec<SelectItem>>,
/// OUTPUT (MSSQL)
pub output: Option<OutputClause>,
/// ORDER BY (MySQL)
pub order_by: Vec<OrderByExpr>,
/// LIMIT (MySQL)
Expand All @@ -307,6 +316,10 @@ impl Display for Delete {
indented_list(f, from)?;
}
}
if let Some(output) = &self.output {
SpaceOrNewline.fmt(f)?;
write!(f, "{output}")?;
}
if let Some(using) = &self.using {
SpaceOrNewline.fmt(f)?;
f.write_str("USING")?;
Expand Down Expand Up @@ -360,6 +373,8 @@ pub struct Update {
pub selection: Option<Expr>,
/// RETURNING
pub returning: Option<Vec<SelectItem>>,
/// OUTPUT (MSSQL)
pub output: Option<OutputClause>,
/// SQLite-specific conflict resolution clause
pub or: Option<SqliteOnConflict>,
/// LIMIT
Expand Down Expand Up @@ -389,6 +404,10 @@ impl Display for Update {
f.write_str("SET")?;
indented_list(f, &self.assignments)?;
}
if let Some(output) = &self.output {
SpaceOrNewline.fmt(f)?;
write!(f, "{output}")?;
}
if let Some(UpdateTableFromKind::AfterSet(from)) = &self.from {
SpaceOrNewline.fmt(f)?;
f.write_str("FROM")?;
Expand Down Expand Up @@ -710,11 +729,11 @@ impl Display for MergeUpdateExpr {
}
}

/// A `OUTPUT` Clause in the end of a `MERGE` Statement
/// An `OUTPUT` clause on `MERGE`, `INSERT`, `UPDATE`, or `DELETE` (MSSQL).
///
/// Example:
/// OUTPUT $action, deleted.* INTO dbo.temp_products;
/// [mssql](https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql)
/// <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))]
Expand Down
8 changes: 7 additions & 1 deletion src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,7 @@ impl Spanned for Delete {
using,
selection,
returning,
output,
order_by,
limit,
} = self;
Expand All @@ -923,6 +924,7 @@ impl Spanned for Delete {
)
.chain(selection.iter().map(|i| i.span()))
.chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span())))
.chain(output.iter().map(|i| i.span()))
.chain(order_by.iter().map(|i| i.span()))
.chain(limit.iter().map(|i| i.span())),
),
Expand All @@ -940,6 +942,7 @@ impl Spanned for Update {
from,
selection,
returning,
output,
or: _,
limit,
} = self;
Expand All @@ -951,6 +954,7 @@ impl Spanned for Update {
.chain(from.iter().map(|i| i.span()))
.chain(selection.iter().map(|i| i.span()))
.chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span())))
.chain(output.iter().map(|i| i.span()))
.chain(limit.iter().map(|i| i.span())),
)
}
Expand Down Expand Up @@ -1312,6 +1316,7 @@ impl Spanned for Insert {
has_table_keyword: _, // bool
on,
returning,
output,
replace_into: _, // bool
priority: _, // todo, mysql specific
insert_alias: _, // todo, mysql specific
Expand All @@ -1334,7 +1339,8 @@ impl Spanned for Insert {
.chain(partitioned.iter().flat_map(|i| i.iter().map(|k| k.span())))
.chain(after_columns.iter().map(|i| i.span))
.chain(on.as_ref().map(|i| i.span()))
.chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span()))),
.chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span())))
.chain(output.iter().map(|i| i.span())),
)
}
}
Expand Down
1 change: 1 addition & 0 deletions src/dialect/snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1780,6 +1780,7 @@ fn parse_multi_table_insert(
has_table_keyword: false,
on: None,
returning: None,
output: None,
replace_into: false,
priority: None,
insert_alias: None,
Expand Down
2 changes: 2 additions & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[
Keyword::ANTI,
Keyword::SEMI,
Keyword::RETURNING,
Keyword::OUTPUT,
Keyword::ASOF,
Keyword::MATCH_CONDITION,
// for MSSQL-specific OUTER APPLY (seems reserved in most dialects)
Expand Down Expand Up @@ -1264,6 +1265,7 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[
Keyword::CLUSTER,
Keyword::DISTRIBUTE,
Keyword::RETURNING,
Keyword::VALUES,
// Reserved only as a column alias in the `SELECT` clause
Keyword::FROM,
Keyword::INTO,
Expand Down
2 changes: 1 addition & 1 deletion src/parser/merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ impl Parser<'_> {
self.parse_parenthesized_qualified_column_list(IsOptional::Optional, allow_empty)
}

fn parse_output(
pub(super) fn parse_output(
&mut self,
start_keyword: Keyword,
start_token: TokenWithSpan,
Expand Down
42 changes: 39 additions & 3 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13288,6 +13288,15 @@ impl<'a> Parser<'a> {
};

let from = self.parse_comma_separated(Parser::parse_table_and_joins)?;

// MSSQL OUTPUT clause appears after FROM table, before USING/WHERE
// https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql
Comment on lines +13292 to +13293
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// MSSQL OUTPUT clause appears after FROM table, before USING/WHERE
// https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql

let output = if self.parse_keyword(Keyword::OUTPUT) {
Some(self.parse_output(Keyword::OUTPUT, self.get_current_token().clone())?)
} else {
None
};

let using = if self.parse_keyword(Keyword::USING) {
Some(self.parse_comma_separated(Parser::parse_table_and_joins)?)
} else {
Expand Down Expand Up @@ -13326,6 +13335,7 @@ impl<'a> Parser<'a> {
using,
selection,
returning,
output,
order_by,
limit,
}))
Expand Down Expand Up @@ -17238,10 +17248,10 @@ impl<'a> Parser<'a> {

let is_mysql = dialect_of!(self is MySqlDialect);

let (columns, partitioned, after_columns, source, assignments) = if self
let (columns, partitioned, after_columns, output, source, assignments) = if self
.parse_keywords(&[Keyword::DEFAULT, Keyword::VALUES])
{
(vec![], None, vec![], None, vec![])
(vec![], None, vec![], None, None, vec![])
} else {
let (columns, partitioned, after_columns) = if !self.peek_subquery_start() {
let columns = self.parse_parenthesized_column_list(Optional, is_mysql)?;
Expand All @@ -17258,6 +17268,14 @@ impl<'a> Parser<'a> {
Default::default()
};

// MSSQL OUTPUT clause appears between columns and source
// https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql
Comment on lines +17271 to +17272
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// MSSQL OUTPUT clause appears between columns and source
// https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql

let output = if self.parse_keyword(Keyword::OUTPUT) {
Some(self.parse_output(Keyword::OUTPUT, self.get_current_token().clone())?)
} else {
None
};

let (source, assignments) = if self.peek_keyword(Keyword::FORMAT)
|| self.peek_keyword(Keyword::SETTINGS)
{
Expand All @@ -17268,7 +17286,14 @@ impl<'a> Parser<'a> {
(Some(self.parse_query()?), vec![])
};

(columns, partitioned, after_columns, source, assignments)
(
columns,
partitioned,
after_columns,
output,
source,
assignments,
)
};

let (format_clause, settings) = if self.dialect.supports_insert_format() {
Expand Down Expand Up @@ -17370,6 +17395,7 @@ impl<'a> Parser<'a> {
has_table_keyword: table,
on,
returning,
output,
replace_into,
priority,
insert_alias,
Expand Down Expand Up @@ -17475,6 +17501,15 @@ impl<'a> Parser<'a> {
};
self.expect_keyword(Keyword::SET)?;
let assignments = self.parse_comma_separated(Parser::parse_assignment)?;

// MSSQL OUTPUT clause appears after SET, before FROM/WHERE
// https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql
Comment on lines +17505 to +17506
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// MSSQL OUTPUT clause appears after SET, before FROM/WHERE
// https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql

maybe we can introduce a helper function maybe_parse_output_clause() that can be called from the relevant statement parsing functions?

let output = if self.parse_keyword(Keyword::OUTPUT) {
Some(self.parse_output(Keyword::OUTPUT, self.get_current_token().clone())?)
} else {
None
};

let from = if from_before_set.is_none() && self.parse_keyword(Keyword::FROM) {
Some(UpdateTableFromKind::AfterSet(
self.parse_table_with_joins()?,
Expand Down Expand Up @@ -17505,6 +17540,7 @@ impl<'a> Parser<'a> {
from,
selection,
returning,
output,
or,
limit,
}
Expand Down
2 changes: 2 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ fn parse_update_set_from() {
])),
}),
returning: None,
output: None,
or: None,
limit: None
})
Expand All @@ -553,6 +554,7 @@ fn parse_update_with_table_alias() {
limit: None,
optimizer_hints,
update_token: _,
output: _,
}) if optimizer_hints.is_empty() => {
assert_eq!(
TableWithJoins {
Expand Down
43 changes: 43 additions & 0 deletions tests/sqlparser_mssql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2730,3 +2730,46 @@ fn parse_mssql_tran_shorthand() {
// ROLLBACK TRAN normalizes to ROLLBACK (same as ROLLBACK TRANSACTION)
ms().one_statement_parses_to("ROLLBACK TRAN", "ROLLBACK");
}

// MSSQL OUTPUT clause on INSERT/UPDATE/DELETE
// https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql

Comment on lines +2734 to +2736
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// MSSQL OUTPUT clause on INSERT/UPDATE/DELETE
// https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql

#[test]
fn parse_mssql_insert_with_output() {
ms_and_generic().verified_stmt(
"INSERT INTO customers (name, email) OUTPUT INSERTED.id, INSERTED.name VALUES ('John', 'john@example.com')",
);
}

#[test]
fn parse_mssql_insert_with_output_into() {
ms_and_generic().verified_stmt(
"INSERT INTO customers (name, email) OUTPUT INSERTED.id, INSERTED.name INTO @new_ids VALUES ('John', 'john@example.com')",
);
}

#[test]
fn parse_mssql_delete_with_output() {
ms_and_generic().verified_stmt("DELETE FROM customers OUTPUT DELETED.* WHERE id = 1");
}

#[test]
fn parse_mssql_delete_with_output_into() {
ms_and_generic().verified_stmt(
"DELETE FROM customers OUTPUT DELETED.id, DELETED.name INTO @deleted_rows WHERE active = 0",
);
}

#[test]
fn parse_mssql_update_with_output() {
ms_and_generic().verified_stmt(
"UPDATE employees SET salary = salary * 1.1 OUTPUT INSERTED.id, DELETED.salary, INSERTED.salary WHERE department = 'Engineering'",
);
}

#[test]
fn parse_mssql_update_with_output_into() {
ms_and_generic().verified_stmt(
"UPDATE employees SET salary = salary * 1.1 OUTPUT INSERTED.id, DELETED.salary, INSERTED.salary INTO @changes WHERE department = 'Engineering'",
);
}
1 change: 1 addition & 0 deletions tests/sqlparser_mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2662,6 +2662,7 @@ fn parse_update_with_joins() {
limit: None,
optimizer_hints,
update_token: _,
output: _,
}) if optimizer_hints.is_empty() => {
assert_eq!(
TableWithJoins {
Expand Down
3 changes: 3 additions & 0 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5488,6 +5488,7 @@ fn test_simple_postgres_insert_with_alias() {
has_table_keyword: false,
on: None,
returning: None,
output: None,
replace_into: false,
priority: None,
insert_alias: None,
Expand Down Expand Up @@ -5567,6 +5568,7 @@ fn test_simple_postgres_insert_with_alias() {
has_table_keyword: false,
on: None,
returning: None,
output: None,
replace_into: false,
priority: None,
insert_alias: None,
Expand Down Expand Up @@ -5644,6 +5646,7 @@ fn test_simple_insert_with_quoted_alias() {
has_table_keyword: false,
on: None,
returning: None,
output: None,
replace_into: false,
priority: None,
insert_alias: None,
Expand Down
1 change: 1 addition & 0 deletions tests/sqlparser_sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ fn parse_update_tuple_row_values() {
},
from: None,
returning: None,
output: None,
limit: None,
update_token: AttachedToken::empty()
})
Expand Down
Loading