Skip to content

Commit d1dda97

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 728645f commit d1dda97

File tree

6 files changed

+216
-26
lines changed

6 files changed

+216
-26
lines changed

src/ast/mod.rs

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

2383-
write!(f, "{begin_token} ")?;
2383+
if begin_token.token != Token::EOF {
2384+
write!(f, "{begin_token} ")?;
2385+
}
23842386
if !statements.is_empty() {
23852387
format_statement_list(f, statements)?;
23862388
}
2387-
write!(f, " {end_token}")
2389+
if end_token.token != Token::EOF {
2390+
write!(f, " {end_token}")?;
2391+
}
2392+
Ok(())
23882393
}
23892394
}
23902395

@@ -3729,6 +3734,7 @@ pub enum Statement {
37293734
/// ```
37303735
///
37313736
/// Postgres: <https://www.postgresql.org/docs/current/sql-createtrigger.html>
3737+
/// SQL Server: <https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql>
37323738
CreateTrigger {
37333739
/// The `OR REPLACE` clause is used to re-create the trigger if it already exists.
37343740
///
@@ -3790,7 +3796,9 @@ pub enum Statement {
37903796
/// Triggering conditions
37913797
condition: Option<Expr>,
37923798
/// Execute logic block
3793-
exec_body: TriggerExecBody,
3799+
exec_body: Option<TriggerExecBody>,
3800+
/// For SQL dialects with statement(s) for a body
3801+
statements: Option<BeginEndStatements>,
37943802
/// The characteristic of the trigger, which include whether the trigger is `DEFERRABLE`, `INITIALLY DEFERRED`, or `INITIALLY IMMEDIATE`,
37953803
characteristics: Option<ConstraintCharacteristics>,
37963804
},
@@ -4599,19 +4607,29 @@ impl fmt::Display for Statement {
45994607
condition,
46004608
include_each,
46014609
exec_body,
4610+
statements,
46024611
characteristics,
46034612
} => {
46044613
write!(
46054614
f,
4606-
"CREATE {or_replace}{is_constraint}TRIGGER {name} {period}",
4615+
"CREATE {or_replace}{is_constraint}TRIGGER {name} ",
46074616
or_replace = if *or_replace { "OR REPLACE " } else { "" },
46084617
is_constraint = if *is_constraint { "CONSTRAINT " } else { "" },
46094618
)?;
46104619

4611-
if !events.is_empty() {
4612-
write!(f, " {}", display_separated(events, " OR "))?;
4620+
if exec_body.is_some() {
4621+
write!(f, "{period}")?;
4622+
if !events.is_empty() {
4623+
write!(f, " {}", display_separated(events, " OR "))?;
4624+
}
4625+
write!(f, " ON {table_name}")?;
4626+
} else {
4627+
write!(f, "ON {table_name}")?;
4628+
write!(f, " {period}")?;
4629+
if !events.is_empty() {
4630+
write!(f, " {}", display_separated(events, ", "))?;
4631+
}
46134632
}
4614-
write!(f, " ON {table_name}")?;
46154633

46164634
if let Some(referenced_table_name) = referenced_table_name {
46174635
write!(f, " FROM {referenced_table_name}")?;
@@ -4627,13 +4645,19 @@ impl fmt::Display for Statement {
46274645

46284646
if *include_each {
46294647
write!(f, " FOR EACH {trigger_object}")?;
4630-
} else {
4648+
} else if exec_body.is_some() {
46314649
write!(f, " FOR {trigger_object}")?;
46324650
}
46334651
if let Some(condition) = condition {
46344652
write!(f, " WHEN {condition}")?;
46354653
}
4636-
write!(f, " EXECUTE {exec_body}")
4654+
if let Some(exec_body) = exec_body {
4655+
write!(f, " EXECUTE {exec_body}")?;
4656+
}
4657+
if let Some(statements) = statements {
4658+
write!(f, " AS {statements}")?;
4659+
}
4660+
Ok(())
46374661
}
46384662
Statement::DropTrigger {
46394663
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
@@ -5317,11 +5317,15 @@ impl<'a> Parser<'a> {
53175317
or_replace: bool,
53185318
is_constraint: bool,
53195319
) -> Result<Statement, ParserError> {
5320-
if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect) {
5320+
if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect | MsSqlDialect) {
53215321
self.prev_token();
53225322
return self.expected("an object type after CREATE", self.peek_token());
53235323
}
53245324

5325+
if dialect_of!(self is MsSqlDialect) {
5326+
return self.parse_mssql_create_trigger(or_replace, is_constraint);
5327+
}
5328+
53255329
let name = self.parse_object_name(false)?;
53265330
let period = self.parse_trigger_period()?;
53275331

@@ -5374,18 +5378,73 @@ impl<'a> Parser<'a> {
53745378
trigger_object,
53755379
include_each,
53765380
condition,
5377-
exec_body,
5381+
exec_body: Some(exec_body),
5382+
statements: None,
53785383
characteristics,
53795384
})
53805385
}
53815386

5387+
/// Parse `CREATE TRIGGER` for [MsSql]
5388+
///
5389+
/// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql
5390+
pub fn parse_mssql_create_trigger(
5391+
&mut self,
5392+
or_replace: bool,
5393+
is_constraint: bool,
5394+
) -> Result<Statement, ParserError> {
5395+
let name = self.parse_object_name(false)?;
5396+
self.expect_keyword_is(Keyword::ON)?;
5397+
let table_name = self.parse_object_name(false)?;
5398+
let period = self.parse_trigger_period()?;
5399+
let events = self.parse_comma_separated(Parser::parse_trigger_event)?;
5400+
5401+
self.expect_keyword_is(Keyword::AS)?;
5402+
5403+
let trigger_statements_body = if self.peek_keyword(Keyword::BEGIN) {
5404+
let begin_token = self.expect_keyword(Keyword::BEGIN)?;
5405+
let statements = self.parse_statement_list(&[Keyword::END])?;
5406+
let end_token = self.expect_keyword(Keyword::END)?;
5407+
5408+
BeginEndStatements {
5409+
begin_token: AttachedToken(begin_token),
5410+
statements,
5411+
end_token: AttachedToken(end_token),
5412+
}
5413+
} else {
5414+
BeginEndStatements {
5415+
begin_token: AttachedToken::empty(),
5416+
statements: vec![self.parse_statement()?],
5417+
end_token: AttachedToken::empty(),
5418+
}
5419+
};
5420+
5421+
Ok(Statement::CreateTrigger {
5422+
or_replace,
5423+
is_constraint,
5424+
name,
5425+
period,
5426+
events,
5427+
table_name,
5428+
referenced_table_name: None,
5429+
referencing: Vec::new(),
5430+
trigger_object: TriggerObject::Statement,
5431+
include_each: false,
5432+
condition: None,
5433+
exec_body: None,
5434+
statements: Some(trigger_statements_body),
5435+
characteristics: None,
5436+
})
5437+
}
5438+
53825439
pub fn parse_trigger_period(&mut self) -> Result<TriggerPeriod, ParserError> {
53835440
Ok(
53845441
match self.expect_one_of_keywords(&[
5442+
Keyword::FOR,
53855443
Keyword::BEFORE,
53865444
Keyword::AFTER,
53875445
Keyword::INSTEAD,
53885446
])? {
5447+
Keyword::FOR => TriggerPeriod::For,
53895448
Keyword::BEFORE => TriggerPeriod::Before,
53905449
Keyword::AFTER => TriggerPeriod::After,
53915450
Keyword::INSTEAD => self

tests/sqlparser_mssql.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,16 @@ fn parse_create_function() {
273273
END\
274274
";
275275
let _ = ms().verified_stmt(create_or_alter_function);
276+
277+
let create_function_with_return_expression = "\
278+
CREATE FUNCTION some_scalar_udf(@foo INT, @bar VARCHAR(256)) \
279+
RETURNS INT \
280+
AS \
281+
BEGIN \
282+
RETURN CONVERT(INT, 1) + 2; \
283+
END\
284+
";
285+
let _ = ms().verified_stmt(create_function_with_return_expression);
276286
}
277287

278288
#[test]
@@ -2199,6 +2209,94 @@ fn parse_mssql_merge_with_output() {
21992209
ms_and_generic().verified_stmt(stmt);
22002210
}
22012211

2212+
#[test]
2213+
fn parse_create_trigger() {
2214+
let create_trigger = "\
2215+
CREATE TRIGGER reminder1 \
2216+
ON Sales.Customer \
2217+
AFTER INSERT, UPDATE \
2218+
AS RAISERROR('Notify Customer Relations', 16, 10);\
2219+
";
2220+
let create_stmt = ms().verified_stmt(create_trigger);
2221+
assert_eq!(
2222+
create_stmt,
2223+
Statement::CreateTrigger {
2224+
or_replace: false,
2225+
is_constraint: false,
2226+
name: ObjectName::from(vec![Ident::new("reminder1")]),
2227+
period: TriggerPeriod::After,
2228+
events: vec![TriggerEvent::Insert, TriggerEvent::Update(vec![]),],
2229+
table_name: ObjectName::from(vec![Ident::new("Sales"), Ident::new("Customer")]),
2230+
referenced_table_name: None,
2231+
referencing: vec![],
2232+
trigger_object: TriggerObject::Statement,
2233+
include_each: false,
2234+
condition: None,
2235+
exec_body: None,
2236+
statements: Some(BeginEndStatements {
2237+
begin_token: AttachedToken::empty(),
2238+
statements: vec![Statement::RaisError {
2239+
message: Box::new(Expr::Value(
2240+
(Value::SingleQuotedString("Notify Customer Relations".to_string()))
2241+
.with_empty_span()
2242+
)),
2243+
severity: Box::new(Expr::Value(
2244+
(Value::Number("16".parse().unwrap(), false)).with_empty_span()
2245+
)),
2246+
state: Box::new(Expr::Value(
2247+
(Value::Number("10".parse().unwrap(), false)).with_empty_span()
2248+
)),
2249+
arguments: vec![],
2250+
options: vec![],
2251+
}],
2252+
end_token: AttachedToken::empty(),
2253+
}),
2254+
characteristics: None,
2255+
}
2256+
);
2257+
2258+
let multi_statement_trigger = "\
2259+
CREATE TRIGGER some_trigger ON some_table FOR INSERT \
2260+
AS \
2261+
BEGIN \
2262+
DECLARE @var INT; \
2263+
RAISERROR('Trigger fired', 10, 1); \
2264+
END\
2265+
";
2266+
let _ = ms().verified_stmt(multi_statement_trigger);
2267+
2268+
let create_trigger_with_return = "\
2269+
CREATE TRIGGER some_trigger ON some_table FOR INSERT \
2270+
AS \
2271+
BEGIN \
2272+
RETURN; \
2273+
END\
2274+
";
2275+
let _ = ms().verified_stmt(create_trigger_with_return);
2276+
2277+
let create_trigger_with_return = "\
2278+
CREATE TRIGGER some_trigger ON some_table FOR INSERT \
2279+
AS \
2280+
BEGIN \
2281+
RETURN; \
2282+
END\
2283+
";
2284+
let _ = ms().verified_stmt(create_trigger_with_return);
2285+
2286+
let create_trigger_with_conditional = "\
2287+
CREATE TRIGGER some_trigger ON some_table FOR INSERT \
2288+
AS \
2289+
BEGIN \
2290+
IF 1 = 2 \
2291+
BEGIN \
2292+
RAISERROR('Trigger fired', 10, 1); \
2293+
END; \
2294+
RETURN; \
2295+
END\
2296+
";
2297+
let _ = ms().verified_stmt(create_trigger_with_conditional);
2298+
}
2299+
22022300
#[test]
22032301
fn parse_drop_trigger() {
22042302
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
@@ -3790,13 +3790,14 @@ fn parse_create_trigger() {
37903790
trigger_object: TriggerObject::Row,
37913791
include_each: true,
37923792
condition: None,
3793-
exec_body: TriggerExecBody {
3793+
exec_body: Some(TriggerExecBody {
37943794
exec_type: TriggerExecBodyType::Function,
37953795
func_desc: FunctionDesc {
37963796
name: ObjectName::from(vec![Ident::new("emp_stamp")]),
37973797
args: None,
37983798
}
3799-
},
3799+
}),
3800+
statements: None,
38003801
characteristics: None,
38013802
}
38023803
);

0 commit comments

Comments
 (0)