Skip to content

Commit c10157b

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 6d0ea64 commit c10157b

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
@@ -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

+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
@@ -5271,11 +5271,15 @@ impl<'a> Parser<'a> {
52715271
or_replace: bool,
52725272
is_constraint: bool,
52735273
) -> Result<Statement, ParserError> {
5274-
if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect) {
5274+
if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect | MsSqlDialect) {
52755275
self.prev_token();
52765276
return self.expected("an object type after CREATE", self.peek_token());
52775277
}
52785278

5279+
if dialect_of!(self is MsSqlDialect) {
5280+
return self.parse_mssql_create_trigger(or_replace, is_constraint);
5281+
}
5282+
52795283
let name = self.parse_object_name(false)?;
52805284
let period = self.parse_trigger_period()?;
52815285

@@ -5328,18 +5332,73 @@ impl<'a> Parser<'a> {
53285332
trigger_object,
53295333
include_each,
53305334
condition,
5331-
exec_body,
5335+
exec_body: Some(exec_body),
5336+
statements: None,
53325337
characteristics,
53335338
})
53345339
}
53355340

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