Skip to content

Commit d2a8544

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`
1 parent 1b50dd9 commit d2a8544

File tree

6 files changed

+308
-23
lines changed

6 files changed

+308
-23
lines changed

src/ast/mod.rs

+24-2
Original file line numberDiff line numberDiff line change
@@ -3624,6 +3624,7 @@ pub enum Statement {
36243624
/// ```
36253625
///
36263626
/// Postgres: <https://www.postgresql.org/docs/current/sql-createtrigger.html>
3627+
/// SQL Server: <https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql>
36273628
CreateTrigger {
36283629
/// The `OR REPLACE` clause is used to re-create the trigger if it already exists.
36293630
///
@@ -3685,7 +3686,9 @@ pub enum Statement {
36853686
/// Triggering conditions
36863687
condition: Option<Expr>,
36873688
/// Execute logic block
3688-
exec_body: TriggerExecBody,
3689+
exec_body: Option<TriggerExecBody>,
3690+
/// For SQL dialects with statement(s) for a body
3691+
statements: Vec<Statement>,
36893692
/// The characteristic of the trigger, which include whether the trigger is `DEFERRABLE`, `INITIALLY DEFERRED`, or `INITIALLY IMMEDIATE`,
36903693
characteristics: Option<ConstraintCharacteristics>,
36913694
},
@@ -4057,6 +4060,11 @@ pub enum Statement {
40574060
/// RETURN scalar_expression
40584061
///
40594062
/// See: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql
4063+
///
4064+
/// for Triggers:
4065+
/// RETURN
4066+
///
4067+
/// See: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql
40604068
Return(ReturnStatement),
40614069
}
40624070

@@ -4486,6 +4494,7 @@ impl fmt::Display for Statement {
44864494
condition,
44874495
include_each,
44884496
exec_body,
4497+
statements,
44894498
characteristics,
44904499
} => {
44914500
write!(
@@ -4520,7 +4529,20 @@ impl fmt::Display for Statement {
45204529
if let Some(condition) = condition {
45214530
write!(f, " WHEN {condition}")?;
45224531
}
4523-
write!(f, " EXECUTE {exec_body}")
4532+
if let Some(exec_body) = exec_body {
4533+
write!(f, " EXECUTE {exec_body}")?;
4534+
}
4535+
if !statements.is_empty() {
4536+
write!(f, " AS ")?;
4537+
if statements.len() > 1 {
4538+
write!(f, "BEGIN ")?;
4539+
}
4540+
write!(f, "{}", display_separated(statements, "; "))?;
4541+
if statements.len() > 1 {
4542+
write!(f, " END")?;
4543+
}
4544+
}
4545+
Ok(())
45244546
}
45254547
Statement::DropTrigger {
45264548
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

+65-6
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,69 @@ impl<'a> Parser<'a> {
53355339
trigger_object,
53365340
include_each,
53375341
condition,
5338-
exec_body,
5342+
exec_body: Some(exec_body),
5343+
statements: vec![],
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 statements = if self.peek_keyword(Keyword::BEGIN) {
5365+
self.next_token();
5366+
let mut result = self.parse_statements()?;
5367+
// note: `parse_statements` will consume the `END` token & produce a Commit statement...
5368+
if let Some(Statement::Commit{ chain, end, modifier }) = result.last() {
5369+
if *chain == false && *end == true && *modifier == None {
5370+
result = result[..result.len() - 1].to_vec();
5371+
}
5372+
}
5373+
result
5374+
} else {
5375+
vec![self.parse_statement()?]
5376+
};
5377+
5378+
Ok(Statement::CreateTrigger {
5379+
or_replace,
5380+
is_constraint,
5381+
name,
5382+
period,
5383+
events,
5384+
table_name,
5385+
referenced_table_name: None,
5386+
referencing: Vec::new(),
5387+
trigger_object: TriggerObject::Statement,
5388+
include_each: false,
5389+
condition: None,
5390+
exec_body: None,
5391+
statements,
5392+
characteristics: None,
5393+
})
5394+
}
5395+
53435396
pub fn parse_trigger_period(&mut self) -> Result<TriggerPeriod, ParserError> {
53445397
Ok(
53455398
match self.expect_one_of_keywords(&[
5399+
Keyword::FOR,
53465400
Keyword::BEFORE,
53475401
Keyword::AFTER,
53485402
Keyword::INSTEAD,
53495403
])? {
5404+
Keyword::FOR => TriggerPeriod::For,
53505405
Keyword::BEFORE => TriggerPeriod::Before,
53515406
Keyword::AFTER => TriggerPeriod::After,
53525407
Keyword::INSTEAD => self
@@ -15088,10 +15143,14 @@ impl<'a> Parser<'a> {
1508815143
}
1508915144

1509015145
fn parse_return(&mut self) -> Result<Statement, ParserError> {
15091-
let expr = self.parse_expr()?;
15092-
Ok(Statement::Return(ReturnStatement {
15093-
value: Some(ReturnStatementValue::Expr(expr)),
15094-
}))
15146+
match self.maybe_parse(|p| p.parse_expr())? {
15147+
Some(expr) => Ok(Statement::Return(ReturnStatement {
15148+
value: Some(ReturnStatementValue::Expr(expr)),
15149+
})),
15150+
None => Ok(Statement::Return(ReturnStatement {
15151+
value: None,
15152+
}))
15153+
}
1509515154
}
1509615155

1509715156
/// Consume the parser and return its underlying token buffer

tests/sqlparser_mssql.rs

+195
Original file line numberDiff line numberDiff line change
@@ -2341,3 +2341,198 @@ fn parse_mssql_merge_with_output() {
23412341
OUTPUT $action, deleted.ProductID INTO dsi.temp_products";
23422342
ms_and_generic().verified_stmt(stmt);
23432343
}
2344+
2345+
#[test]
2346+
fn parse_create_trigger() {
2347+
let create_trigger = r#"
2348+
CREATE TRIGGER reminder1
2349+
ON Sales.Customer
2350+
AFTER INSERT, UPDATE
2351+
AS RAISERROR ('Notify Customer Relations', 16, 10);
2352+
"#;
2353+
let create_stmt = ms().one_statement_parses_to(create_trigger, "");
2354+
assert_eq!(
2355+
create_stmt,
2356+
Statement::CreateTrigger {
2357+
or_replace: false,
2358+
is_constraint: false,
2359+
name: ObjectName::from(vec![Ident::new("reminder1")]),
2360+
period: TriggerPeriod::After,
2361+
events: vec![
2362+
TriggerEvent::Insert,
2363+
TriggerEvent::Update(vec![]),
2364+
],
2365+
table_name: ObjectName::from(vec![Ident::new("Sales"), Ident::new("Customer")]),
2366+
referenced_table_name: None,
2367+
referencing: vec![],
2368+
trigger_object: TriggerObject::Statement,
2369+
include_each: false,
2370+
condition: None,
2371+
exec_body: None,
2372+
statements: vec![Statement::RaisError {
2373+
message: Box::new(Expr::Value(
2374+
(Value::SingleQuotedString("Notify Customer Relations".to_string())).with_empty_span()
2375+
)),
2376+
severity: Box::new(Expr::Value(
2377+
(Value::Number("16".parse().unwrap(), false)).with_empty_span()
2378+
)),
2379+
state: Box::new(Expr::Value(
2380+
(Value::Number("10".parse().unwrap(), false)).with_empty_span()
2381+
)),
2382+
arguments: vec![],
2383+
options: vec![],
2384+
}],
2385+
characteristics: None,
2386+
}
2387+
);
2388+
2389+
let multi_statement_trigger = r#"
2390+
CREATE TRIGGER some_trigger ON some_table FOR INSERT
2391+
AS
2392+
BEGIN
2393+
RAISERROR('Trigger fired', 10, 1);
2394+
END
2395+
"#;
2396+
let create_stmt = ms().one_statement_parses_to(multi_statement_trigger, "");
2397+
assert_eq!(
2398+
create_stmt,
2399+
Statement::CreateTrigger {
2400+
or_replace: false,
2401+
is_constraint: false,
2402+
name: ObjectName::from(vec![Ident::new("some_trigger")]),
2403+
period: TriggerPeriod::For,
2404+
events: vec![TriggerEvent::Insert],
2405+
table_name: ObjectName::from(vec![Ident::new("some_table")]),
2406+
referenced_table_name: None,
2407+
referencing: vec![],
2408+
trigger_object: TriggerObject::Statement,
2409+
include_each: false,
2410+
condition: None,
2411+
exec_body: None,
2412+
statements: vec![Statement::RaisError {
2413+
message: Box::new(Expr::Value(
2414+
(Value::SingleQuotedString("Trigger fired".to_string())).with_empty_span()
2415+
)),
2416+
severity: Box::new(Expr::Value(
2417+
(Value::Number("10".parse().unwrap(), false)).with_empty_span()
2418+
)),
2419+
state: Box::new(Expr::Value(
2420+
(Value::Number("1".parse().unwrap(), false)).with_empty_span()
2421+
)),
2422+
arguments: vec![],
2423+
options: vec![],
2424+
}],
2425+
characteristics: None,
2426+
}
2427+
);
2428+
2429+
let create_trigger_with_return = r#"
2430+
CREATE TRIGGER some_trigger ON some_table FOR INSERT
2431+
AS
2432+
BEGIN
2433+
RETURN;
2434+
END
2435+
"#;
2436+
let create_stmt = ms().one_statement_parses_to(create_trigger_with_return, "");
2437+
assert_eq!(
2438+
create_stmt,
2439+
Statement::CreateTrigger {
2440+
or_replace: false,
2441+
is_constraint: false,
2442+
name: ObjectName::from(vec![Ident::new("some_trigger")]),
2443+
period: TriggerPeriod::For,
2444+
events: vec![TriggerEvent::Insert],
2445+
table_name: ObjectName::from(vec![Ident::new("some_table")]),
2446+
referenced_table_name: None,
2447+
referencing: vec![],
2448+
trigger_object: TriggerObject::Statement,
2449+
include_each: false,
2450+
condition: None,
2451+
exec_body: None,
2452+
statements: vec![Statement::Return(ReturnStatement {
2453+
value: None,
2454+
})],
2455+
characteristics: None,
2456+
}
2457+
);
2458+
2459+
let create_trigger_with_conditional = r#"
2460+
CREATE TRIGGER some_trigger ON some_table FOR INSERT
2461+
AS
2462+
BEGIN
2463+
IF 1=2
2464+
BEGIN
2465+
RAISERROR('Trigger fired', 10, 1);
2466+
END
2467+
2468+
RETURN;
2469+
END
2470+
"#;
2471+
let create_stmt = ms().one_statement_parses_to(create_trigger_with_conditional, "");
2472+
assert_eq!(
2473+
create_stmt,
2474+
Statement::CreateTrigger {
2475+
or_replace: false,
2476+
is_constraint: false,
2477+
name: ObjectName::from(vec![Ident::new("some_trigger")]),
2478+
period: TriggerPeriod::For,
2479+
events: vec![TriggerEvent::Insert],
2480+
table_name: ObjectName::from(vec![Ident::new("some_table")]),
2481+
referenced_table_name: None,
2482+
referencing: vec![],
2483+
trigger_object: TriggerObject::Statement,
2484+
include_each: false,
2485+
condition: None,
2486+
exec_body: None,
2487+
statements: vec![
2488+
Statement::If(IfStatement {
2489+
if_block: ConditionalStatementBlock {
2490+
start_token: AttachedToken(TokenWithSpan::wrap(sqlparser::tokenizer::Token::Word(sqlparser::tokenizer::Word {
2491+
value: "IF".to_string(),
2492+
quote_style: None,
2493+
keyword: Keyword::IF
2494+
}))),
2495+
condition: Some(Expr::BinaryOp {
2496+
left: Box::new(Expr::Value(Value::Number("1".to_string(), false).with_empty_span())),
2497+
op: sqlparser::ast::BinaryOperator::Eq,
2498+
right: Box::new(Expr::Value(Value::Number("2".to_string(), false).with_empty_span())),
2499+
}),
2500+
then_token: None,
2501+
conditional_statements: ConditionalStatements::BeginEnd {
2502+
begin_token: AttachedToken(TokenWithSpan::wrap(sqlparser::tokenizer::Token::Word(sqlparser::tokenizer::Word {
2503+
value: "BEGIN".to_string(),
2504+
quote_style: None,
2505+
keyword: Keyword::BEGIN
2506+
}))),
2507+
statements: vec![Statement::RaisError {
2508+
message: Box::new(Expr::Value(
2509+
(Value::SingleQuotedString("Trigger fired".to_string())).with_empty_span()
2510+
)),
2511+
severity: Box::new(Expr::Value(
2512+
(Value::Number("10".parse().unwrap(), false)).with_empty_span()
2513+
)),
2514+
state: Box::new(Expr::Value(
2515+
(Value::Number("1".parse().unwrap(), false)).with_empty_span()
2516+
)),
2517+
arguments: vec![],
2518+
options: vec![],
2519+
}],
2520+
end_token: AttachedToken(TokenWithSpan::wrap(sqlparser::tokenizer::Token::Word(sqlparser::tokenizer::Word {
2521+
value: "END".to_string(),
2522+
quote_style: None,
2523+
keyword: Keyword::END
2524+
}))),
2525+
},
2526+
},
2527+
elseif_blocks: vec![],
2528+
else_block: None,
2529+
end_token: None,
2530+
}),
2531+
Statement::Return(ReturnStatement {
2532+
value: None
2533+
}),
2534+
],
2535+
characteristics: None,
2536+
}
2537+
);
2538+
}

tests/sqlparser_mysql.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -3410,13 +3410,14 @@ fn parse_create_trigger() {
34103410
trigger_object: TriggerObject::Row,
34113411
include_each: true,
34123412
condition: None,
3413-
exec_body: TriggerExecBody {
3413+
exec_body: Some(TriggerExecBody {
34143414
exec_type: TriggerExecBodyType::Function,
34153415
func_desc: FunctionDesc {
34163416
name: ObjectName::from(vec![Ident::new("emp_stamp")]),
34173417
args: None,
34183418
}
3419-
},
3419+
}),
3420+
statements: vec![],
34203421
characteristics: None,
34213422
}
34223423
);

0 commit comments

Comments
 (0)