Skip to content

Commit 7593867

Browse files
committed
Add support for CREATE TRIGGER for SQL Server
- similar to functions & procedures, this dialect can define triggers with a multi statement block - there's no `EXECUTE` keyword here, so that means the `exec_body` used by other dialects becomes an `Option`, and our `statements` is also optional for them
1 parent ca28413 commit 7593867

File tree

6 files changed

+206
-26
lines changed

6 files changed

+206
-26
lines changed

src/ast/mod.rs

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2343,11 +2343,16 @@ impl fmt::Display for BeginEndStatements {
23432343
end_token: AttachedToken(end_token),
23442344
} = self;
23452345

2346-
write!(f, "{begin_token} ")?;
2346+
if begin_token.token != Token::EOF {
2347+
write!(f, "{begin_token} ")?;
2348+
}
23472349
if !statements.is_empty() {
23482350
format_statement_list(f, statements)?;
23492351
}
2350-
write!(f, " {end_token}")
2352+
if end_token.token != Token::EOF {
2353+
write!(f, " {end_token}")?;
2354+
}
2355+
Ok(())
23512356
}
23522357
}
23532358

@@ -3653,6 +3658,7 @@ pub enum Statement {
36533658
/// ```
36543659
///
36553660
/// Postgres: <https://www.postgresql.org/docs/current/sql-createtrigger.html>
3661+
/// SQL Server: <https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql>
36563662
CreateTrigger {
36573663
/// The `OR REPLACE` clause is used to re-create the trigger if it already exists.
36583664
///
@@ -3714,7 +3720,9 @@ pub enum Statement {
37143720
/// Triggering conditions
37153721
condition: Option<Expr>,
37163722
/// Execute logic block
3717-
exec_body: TriggerExecBody,
3723+
exec_body: Option<TriggerExecBody>,
3724+
/// For SQL dialects with statement(s) for a body
3725+
statements: Option<BeginEndStatements>,
37183726
/// The characteristic of the trigger, which include whether the trigger is `DEFERRABLE`, `INITIALLY DEFERRED`, or `INITIALLY IMMEDIATE`,
37193727
characteristics: Option<ConstraintCharacteristics>,
37203728
},
@@ -4520,19 +4528,29 @@ impl fmt::Display for Statement {
45204528
condition,
45214529
include_each,
45224530
exec_body,
4531+
statements,
45234532
characteristics,
45244533
} => {
45254534
write!(
45264535
f,
4527-
"CREATE {or_replace}{is_constraint}TRIGGER {name} {period}",
4536+
"CREATE {or_replace}{is_constraint}TRIGGER {name} ",
45284537
or_replace = if *or_replace { "OR REPLACE " } else { "" },
45294538
is_constraint = if *is_constraint { "CONSTRAINT " } else { "" },
45304539
)?;
45314540

4532-
if !events.is_empty() {
4533-
write!(f, " {}", display_separated(events, " OR "))?;
4541+
if exec_body.is_some() {
4542+
write!(f, "{period}")?;
4543+
if !events.is_empty() {
4544+
write!(f, " {}", display_separated(events, " OR "))?;
4545+
}
4546+
write!(f, " ON {table_name}")?;
4547+
} else {
4548+
write!(f, "ON {table_name}")?;
4549+
write!(f, " {period}")?;
4550+
if !events.is_empty() {
4551+
write!(f, " {}", display_separated(events, ", "))?;
4552+
}
45344553
}
4535-
write!(f, " ON {table_name}")?;
45364554

45374555
if let Some(referenced_table_name) = referenced_table_name {
45384556
write!(f, " FROM {referenced_table_name}")?;
@@ -4548,13 +4566,19 @@ impl fmt::Display for Statement {
45484566

45494567
if *include_each {
45504568
write!(f, " FOR EACH {trigger_object}")?;
4551-
} else {
4569+
} else if exec_body.is_some() {
45524570
write!(f, " FOR {trigger_object}")?;
45534571
}
45544572
if let Some(condition) = condition {
45554573
write!(f, " WHEN {condition}")?;
45564574
}
4557-
write!(f, " EXECUTE {exec_body}")
4575+
if let Some(exec_body) = exec_body {
4576+
write!(f, " EXECUTE {exec_body}")?;
4577+
}
4578+
if let Some(statements) = statements {
4579+
write!(f, " AS {statements}")?;
4580+
}
4581+
Ok(())
45584582
}
45594583
Statement::DropTrigger {
45604584
if_exists,

src/ast/trigger.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ impl fmt::Display for TriggerEvent {
110110
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
111111
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
112112
pub enum TriggerPeriod {
113+
For,
113114
After,
114115
Before,
115116
InsteadOf,
@@ -118,6 +119,7 @@ pub enum TriggerPeriod {
118119
impl fmt::Display for TriggerPeriod {
119120
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
120121
match self {
122+
TriggerPeriod::For => write!(f, "FOR"),
121123
TriggerPeriod::After => write!(f, "AFTER"),
122124
TriggerPeriod::Before => write!(f, "BEFORE"),
123125
TriggerPeriod::InsteadOf => write!(f, "INSTEAD OF"),

src/parser/mod.rs

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5259,11 +5259,15 @@ impl<'a> Parser<'a> {
52595259
or_replace: bool,
52605260
is_constraint: bool,
52615261
) -> Result<Statement, ParserError> {
5262-
if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect) {
5262+
if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect | MsSqlDialect) {
52635263
self.prev_token();
52645264
return self.expected("an object type after CREATE", self.peek_token());
52655265
}
52665266

5267+
if dialect_of!(self is MsSqlDialect) {
5268+
return self.parse_mssql_create_trigger(or_replace, is_constraint);
5269+
}
5270+
52675271
let name = self.parse_object_name(false)?;
52685272
let period = self.parse_trigger_period()?;
52695273

@@ -5316,18 +5320,73 @@ impl<'a> Parser<'a> {
53165320
trigger_object,
53175321
include_each,
53185322
condition,
5319-
exec_body,
5323+
exec_body: Some(exec_body),
5324+
statements: None,
53205325
characteristics,
53215326
})
53225327
}
53235328

5329+
/// Parse `CREATE TRIGGER` for [MsSql]
5330+
///
5331+
/// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql
5332+
pub fn parse_mssql_create_trigger(
5333+
&mut self,
5334+
or_replace: bool,
5335+
is_constraint: bool,
5336+
) -> Result<Statement, ParserError> {
5337+
let name = self.parse_object_name(false)?;
5338+
self.expect_keyword_is(Keyword::ON)?;
5339+
let table_name = self.parse_object_name(false)?;
5340+
let period = self.parse_trigger_period()?;
5341+
let events = self.parse_comma_separated(Parser::parse_trigger_event)?;
5342+
5343+
self.expect_keyword_is(Keyword::AS)?;
5344+
5345+
let trigger_statements_body = if self.peek_keyword(Keyword::BEGIN) {
5346+
let begin_token = self.expect_keyword(Keyword::BEGIN)?;
5347+
let statements = self.parse_statement_list(&[Keyword::END])?;
5348+
let end_token = self.expect_keyword(Keyword::END)?;
5349+
5350+
BeginEndStatements {
5351+
begin_token: AttachedToken(begin_token),
5352+
statements,
5353+
end_token: AttachedToken(end_token),
5354+
}
5355+
} else {
5356+
BeginEndStatements {
5357+
begin_token: AttachedToken::empty(),
5358+
statements: vec![self.parse_statement()?],
5359+
end_token: AttachedToken::empty(),
5360+
}
5361+
};
5362+
5363+
Ok(Statement::CreateTrigger {
5364+
or_replace,
5365+
is_constraint,
5366+
name,
5367+
period,
5368+
events,
5369+
table_name,
5370+
referenced_table_name: None,
5371+
referencing: Vec::new(),
5372+
trigger_object: TriggerObject::Statement,
5373+
include_each: false,
5374+
condition: None,
5375+
exec_body: None,
5376+
statements: Some(trigger_statements_body),
5377+
characteristics: None,
5378+
})
5379+
}
5380+
53245381
pub fn parse_trigger_period(&mut self) -> Result<TriggerPeriod, ParserError> {
53255382
Ok(
53265383
match self.expect_one_of_keywords(&[
5384+
Keyword::FOR,
53275385
Keyword::BEFORE,
53285386
Keyword::AFTER,
53295387
Keyword::INSTEAD,
53305388
])? {
5389+
Keyword::FOR => TriggerPeriod::For,
53315390
Keyword::BEFORE => TriggerPeriod::Before,
53325391
Keyword::AFTER => TriggerPeriod::After,
53335392
Keyword::INSTEAD => self

tests/sqlparser_mssql.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2125,6 +2125,94 @@ fn parse_mssql_merge_with_output() {
21252125
ms_and_generic().verified_stmt(stmt);
21262126
}
21272127

2128+
#[test]
2129+
fn parse_create_trigger() {
2130+
let create_trigger = "\
2131+
CREATE TRIGGER reminder1 \
2132+
ON Sales.Customer \
2133+
AFTER INSERT, UPDATE \
2134+
AS RAISERROR('Notify Customer Relations', 16, 10);\
2135+
";
2136+
let create_stmt = ms().verified_stmt(create_trigger);
2137+
assert_eq!(
2138+
create_stmt,
2139+
Statement::CreateTrigger {
2140+
or_replace: false,
2141+
is_constraint: false,
2142+
name: ObjectName::from(vec![Ident::new("reminder1")]),
2143+
period: TriggerPeriod::After,
2144+
events: vec![TriggerEvent::Insert, TriggerEvent::Update(vec![]),],
2145+
table_name: ObjectName::from(vec![Ident::new("Sales"), Ident::new("Customer")]),
2146+
referenced_table_name: None,
2147+
referencing: vec![],
2148+
trigger_object: TriggerObject::Statement,
2149+
include_each: false,
2150+
condition: None,
2151+
exec_body: None,
2152+
statements: Some(BeginEndStatements {
2153+
begin_token: AttachedToken::empty(),
2154+
statements: vec![Statement::RaisError {
2155+
message: Box::new(Expr::Value(
2156+
(Value::SingleQuotedString("Notify Customer Relations".to_string()))
2157+
.with_empty_span()
2158+
)),
2159+
severity: Box::new(Expr::Value(
2160+
(Value::Number("16".parse().unwrap(), false)).with_empty_span()
2161+
)),
2162+
state: Box::new(Expr::Value(
2163+
(Value::Number("10".parse().unwrap(), false)).with_empty_span()
2164+
)),
2165+
arguments: vec![],
2166+
options: vec![],
2167+
}],
2168+
end_token: AttachedToken::empty(),
2169+
}),
2170+
characteristics: None,
2171+
}
2172+
);
2173+
2174+
let multi_statement_trigger = "\
2175+
CREATE TRIGGER some_trigger ON some_table FOR INSERT \
2176+
AS \
2177+
BEGIN \
2178+
DECLARE @var INT; \
2179+
RAISERROR('Trigger fired', 10, 1); \
2180+
END\
2181+
";
2182+
let _ = ms().verified_stmt(multi_statement_trigger);
2183+
2184+
let create_trigger_with_return = "\
2185+
CREATE TRIGGER some_trigger ON some_table FOR INSERT \
2186+
AS \
2187+
BEGIN \
2188+
RETURN; \
2189+
END\
2190+
";
2191+
let _ = ms().verified_stmt(create_trigger_with_return);
2192+
2193+
let create_trigger_with_return = "\
2194+
CREATE TRIGGER some_trigger ON some_table FOR INSERT \
2195+
AS \
2196+
BEGIN \
2197+
RETURN; \
2198+
END\
2199+
";
2200+
let _ = ms().verified_stmt(create_trigger_with_return);
2201+
2202+
let create_trigger_with_conditional = "\
2203+
CREATE TRIGGER some_trigger ON some_table FOR INSERT \
2204+
AS \
2205+
BEGIN \
2206+
IF 1 = 2 \
2207+
BEGIN \
2208+
RAISERROR('Trigger fired', 10, 1); \
2209+
END; \
2210+
RETURN; \
2211+
END\
2212+
";
2213+
let _ = ms().verified_stmt(create_trigger_with_conditional);
2214+
}
2215+
21282216
#[test]
21292217
fn parse_drop_trigger() {
21302218
let sql_drop_trigger = "DROP TRIGGER emp_stamp;";

tests/sqlparser_mysql.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3532,13 +3532,14 @@ fn parse_create_trigger() {
35323532
trigger_object: TriggerObject::Row,
35333533
include_each: true,
35343534
condition: None,
3535-
exec_body: TriggerExecBody {
3535+
exec_body: Some(TriggerExecBody {
35363536
exec_type: TriggerExecBodyType::Function,
35373537
func_desc: FunctionDesc {
35383538
name: ObjectName::from(vec![Ident::new("emp_stamp")]),
35393539
args: None,
35403540
}
3541-
},
3541+
}),
3542+
statements: None,
35423543
characteristics: None,
35433544
}
35443545
);

0 commit comments

Comments
 (0)