Skip to content

Commit cd7b14f

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 87d1907 commit cd7b14f

File tree

6 files changed

+206
-26
lines changed

6 files changed

+206
-26
lines changed

src/ast/mod.rs

+33-9
Original file line numberDiff line numberDiff line change
@@ -2344,11 +2344,16 @@ impl fmt::Display for BeginEndStatements {
23442344
end_token: AttachedToken(end_token),
23452345
} = self;
23462346

2347-
write!(f, "{begin_token} ")?;
2347+
if begin_token.token != Token::EOF {
2348+
write!(f, "{begin_token} ")?;
2349+
}
23482350
if !statements.is_empty() {
23492351
format_statement_list(f, statements)?;
23502352
}
2351-
write!(f, " {end_token}")
2353+
if end_token.token != Token::EOF {
2354+
write!(f, " {end_token}")?;
2355+
}
2356+
Ok(())
23522357
}
23532358
}
23542359

@@ -3658,6 +3663,7 @@ pub enum Statement {
36583663
/// ```
36593664
///
36603665
/// Postgres: <https://www.postgresql.org/docs/current/sql-createtrigger.html>
3666+
/// SQL Server: <https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql>
36613667
CreateTrigger {
36623668
/// The `OR REPLACE` clause is used to re-create the trigger if it already exists.
36633669
///
@@ -3719,7 +3725,9 @@ pub enum Statement {
37193725
/// Triggering conditions
37203726
condition: Option<Expr>,
37213727
/// Execute logic block
3722-
exec_body: TriggerExecBody,
3728+
exec_body: Option<TriggerExecBody>,
3729+
/// For SQL dialects with statement(s) for a body
3730+
statements: Option<BeginEndStatements>,
37233731
/// The characteristic of the trigger, which include whether the trigger is `DEFERRABLE`, `INITIALLY DEFERRED`, or `INITIALLY IMMEDIATE`,
37243732
characteristics: Option<ConstraintCharacteristics>,
37253733
},
@@ -4525,19 +4533,29 @@ impl fmt::Display for Statement {
45254533
condition,
45264534
include_each,
45274535
exec_body,
4536+
statements,
45284537
characteristics,
45294538
} => {
45304539
write!(
45314540
f,
4532-
"CREATE {or_replace}{is_constraint}TRIGGER {name} {period}",
4541+
"CREATE {or_replace}{is_constraint}TRIGGER {name} ",
45334542
or_replace = if *or_replace { "OR REPLACE " } else { "" },
45344543
is_constraint = if *is_constraint { "CONSTRAINT " } else { "" },
45354544
)?;
45364545

4537-
if !events.is_empty() {
4538-
write!(f, " {}", display_separated(events, " OR "))?;
4546+
if exec_body.is_some() {
4547+
write!(f, "{period}")?;
4548+
if !events.is_empty() {
4549+
write!(f, " {}", display_separated(events, " OR "))?;
4550+
}
4551+
write!(f, " ON {table_name}")?;
4552+
} else {
4553+
write!(f, "ON {table_name}")?;
4554+
write!(f, " {period}")?;
4555+
if !events.is_empty() {
4556+
write!(f, " {}", display_separated(events, ", "))?;
4557+
}
45394558
}
4540-
write!(f, " ON {table_name}")?;
45414559

45424560
if let Some(referenced_table_name) = referenced_table_name {
45434561
write!(f, " FROM {referenced_table_name}")?;
@@ -4553,13 +4571,19 @@ impl fmt::Display for Statement {
45534571

45544572
if *include_each {
45554573
write!(f, " FOR EACH {trigger_object}")?;
4556-
} else {
4574+
} else if exec_body.is_some() {
45574575
write!(f, " FOR {trigger_object}")?;
45584576
}
45594577
if let Some(condition) = condition {
45604578
write!(f, " WHEN {condition}")?;
45614579
}
4562-
write!(f, " EXECUTE {exec_body}")
4580+
if let Some(exec_body) = exec_body {
4581+
write!(f, " EXECUTE {exec_body}")?;
4582+
}
4583+
if let Some(statements) = statements {
4584+
write!(f, " AS {statements}")?;
4585+
}
4586+
Ok(())
45634587
}
45644588
Statement::DropTrigger {
45654589
if_exists,

src/ast/trigger.rs

+2
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

+61-2
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

+88
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

+3-2
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)