Skip to content

Commit 3a9029a

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 26c99e7 commit 3a9029a

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
},
@@ -4514,19 +4522,29 @@ impl fmt::Display for Statement {
45144522
condition,
45154523
include_each,
45164524
exec_body,
4525+
statements,
45174526
characteristics,
45184527
} => {
45194528
write!(
45204529
f,
4521-
"CREATE {or_replace}{is_constraint}TRIGGER {name} {period}",
4530+
"CREATE {or_replace}{is_constraint}TRIGGER {name} ",
45224531
or_replace = if *or_replace { "OR REPLACE " } else { "" },
45234532
is_constraint = if *is_constraint { "CONSTRAINT " } else { "" },
45244533
)?;
45254534

4526-
if !events.is_empty() {
4527-
write!(f, " {}", display_separated(events, " OR "))?;
4535+
if exec_body.is_some() {
4536+
write!(f, "{period}")?;
4537+
if !events.is_empty() {
4538+
write!(f, " {}", display_separated(events, " OR "))?;
4539+
}
4540+
write!(f, " ON {table_name}")?;
4541+
} else {
4542+
write!(f, "ON {table_name}")?;
4543+
write!(f, " {period}")?;
4544+
if !events.is_empty() {
4545+
write!(f, " {}", display_separated(events, ", "))?;
4546+
}
45284547
}
4529-
write!(f, " ON {table_name}")?;
45304548

45314549
if let Some(referenced_table_name) = referenced_table_name {
45324550
write!(f, " FROM {referenced_table_name}")?;
@@ -4542,13 +4560,19 @@ impl fmt::Display for Statement {
45424560

45434561
if *include_each {
45444562
write!(f, " FOR EACH {trigger_object}")?;
4545-
} else {
4563+
} else if exec_body.is_some() {
45464564
write!(f, " FOR {trigger_object}")?;
45474565
}
45484566
if let Some(condition) = condition {
45494567
write!(f, " WHEN {condition}")?;
45504568
}
4551-
write!(f, " EXECUTE {exec_body}")
4569+
if let Some(exec_body) = exec_body {
4570+
write!(f, " EXECUTE {exec_body}")?;
4571+
}
4572+
if let Some(statements) = statements {
4573+
write!(f, " AS {statements}")?;
4574+
}
4575+
Ok(())
45524576
}
45534577
Statement::DropTrigger {
45544578
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
@@ -5278,11 +5278,15 @@ impl<'a> Parser<'a> {
52785278
or_replace: bool,
52795279
is_constraint: bool,
52805280
) -> Result<Statement, ParserError> {
5281-
if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect) {
5281+
if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect | MsSqlDialect) {
52825282
self.prev_token();
52835283
return self.expected("an object type after CREATE", self.peek_token());
52845284
}
52855285

5286+
if dialect_of!(self is MsSqlDialect) {
5287+
return self.parse_mssql_create_trigger(or_replace, is_constraint);
5288+
}
5289+
52865290
let name = self.parse_object_name(false)?;
52875291
let period = self.parse_trigger_period()?;
52885292

@@ -5335,18 +5339,73 @@ impl<'a> Parser<'a> {
53355339
trigger_object,
53365340
include_each,
53375341
condition,
5338-
exec_body,
5342+
exec_body: Some(exec_body),
5343+
statements: None,
53395344
characteristics,
53405345
})
53415346
}
53425347

5348+
/// Parse `CREATE TRIGGER` for [MsSql]
5349+
///
5350+
/// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql
5351+
pub fn parse_mssql_create_trigger(
5352+
&mut self,
5353+
or_replace: bool,
5354+
is_constraint: bool,
5355+
) -> Result<Statement, ParserError> {
5356+
let name = self.parse_object_name(false)?;
5357+
self.expect_keyword_is(Keyword::ON)?;
5358+
let table_name = self.parse_object_name(false)?;
5359+
let period = self.parse_trigger_period()?;
5360+
let events = self.parse_comma_separated(Parser::parse_trigger_event)?;
5361+
5362+
self.expect_keyword_is(Keyword::AS)?;
5363+
5364+
let trigger_statements_body = if self.peek_keyword(Keyword::BEGIN) {
5365+
let begin_token = self.expect_keyword(Keyword::BEGIN)?;
5366+
let statements = self.parse_statement_list(&[Keyword::END])?;
5367+
let end_token = self.expect_keyword(Keyword::END)?;
5368+
5369+
BeginEndStatements {
5370+
begin_token: AttachedToken(begin_token),
5371+
statements,
5372+
end_token: AttachedToken(end_token),
5373+
}
5374+
} else {
5375+
BeginEndStatements {
5376+
begin_token: AttachedToken::empty(),
5377+
statements: vec![self.parse_statement()?],
5378+
end_token: AttachedToken::empty(),
5379+
}
5380+
};
5381+
5382+
Ok(Statement::CreateTrigger {
5383+
or_replace,
5384+
is_constraint,
5385+
name,
5386+
period,
5387+
events,
5388+
table_name,
5389+
referenced_table_name: None,
5390+
referencing: Vec::new(),
5391+
trigger_object: TriggerObject::Statement,
5392+
include_each: false,
5393+
condition: None,
5394+
exec_body: None,
5395+
statements: Some(trigger_statements_body),
5396+
characteristics: None,
5397+
})
5398+
}
5399+
53435400
pub fn parse_trigger_period(&mut self) -> Result<TriggerPeriod, ParserError> {
53445401
Ok(
53455402
match self.expect_one_of_keywords(&[
5403+
Keyword::FOR,
53465404
Keyword::BEFORE,
53475405
Keyword::AFTER,
53485406
Keyword::INSTEAD,
53495407
])? {
5408+
Keyword::FOR => TriggerPeriod::For,
53505409
Keyword::BEFORE => TriggerPeriod::Before,
53515410
Keyword::AFTER => TriggerPeriod::After,
53525411
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)