From d1dda975f408fa741a9b0c3d462808dadd82b7c9 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Fri, 11 Apr 2025 12:43:45 -0400 Subject: [PATCH 1/6] 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 --- src/ast/mod.rs | 42 ++++++++++++---- src/ast/trigger.rs | 2 + src/parser/mod.rs | 63 +++++++++++++++++++++++- tests/sqlparser_mssql.rs | 98 +++++++++++++++++++++++++++++++++++++ tests/sqlparser_mysql.rs | 5 +- tests/sqlparser_postgres.rs | 32 +++++++----- 6 files changed, 216 insertions(+), 26 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d74d197e3..05321569d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2380,11 +2380,16 @@ impl fmt::Display for BeginEndStatements { end_token: AttachedToken(end_token), } = self; - write!(f, "{begin_token} ")?; + if begin_token.token != Token::EOF { + write!(f, "{begin_token} ")?; + } if !statements.is_empty() { format_statement_list(f, statements)?; } - write!(f, " {end_token}") + if end_token.token != Token::EOF { + write!(f, " {end_token}")?; + } + Ok(()) } } @@ -3729,6 +3734,7 @@ pub enum Statement { /// ``` /// /// Postgres: + /// SQL Server: CreateTrigger { /// The `OR REPLACE` clause is used to re-create the trigger if it already exists. /// @@ -3790,7 +3796,9 @@ pub enum Statement { /// Triggering conditions condition: Option, /// Execute logic block - exec_body: TriggerExecBody, + exec_body: Option, + /// For SQL dialects with statement(s) for a body + statements: Option, /// The characteristic of the trigger, which include whether the trigger is `DEFERRABLE`, `INITIALLY DEFERRED`, or `INITIALLY IMMEDIATE`, characteristics: Option, }, @@ -4599,19 +4607,29 @@ impl fmt::Display for Statement { condition, include_each, exec_body, + statements, characteristics, } => { write!( f, - "CREATE {or_replace}{is_constraint}TRIGGER {name} {period}", + "CREATE {or_replace}{is_constraint}TRIGGER {name} ", or_replace = if *or_replace { "OR REPLACE " } else { "" }, is_constraint = if *is_constraint { "CONSTRAINT " } else { "" }, )?; - if !events.is_empty() { - write!(f, " {}", display_separated(events, " OR "))?; + if exec_body.is_some() { + write!(f, "{period}")?; + if !events.is_empty() { + write!(f, " {}", display_separated(events, " OR "))?; + } + write!(f, " ON {table_name}")?; + } else { + write!(f, "ON {table_name}")?; + write!(f, " {period}")?; + if !events.is_empty() { + write!(f, " {}", display_separated(events, ", "))?; + } } - write!(f, " ON {table_name}")?; if let Some(referenced_table_name) = referenced_table_name { write!(f, " FROM {referenced_table_name}")?; @@ -4627,13 +4645,19 @@ impl fmt::Display for Statement { if *include_each { write!(f, " FOR EACH {trigger_object}")?; - } else { + } else if exec_body.is_some() { write!(f, " FOR {trigger_object}")?; } if let Some(condition) = condition { write!(f, " WHEN {condition}")?; } - write!(f, " EXECUTE {exec_body}") + if let Some(exec_body) = exec_body { + write!(f, " EXECUTE {exec_body}")?; + } + if let Some(statements) = statements { + write!(f, " AS {statements}")?; + } + Ok(()) } Statement::DropTrigger { if_exists, diff --git a/src/ast/trigger.rs b/src/ast/trigger.rs index cf1c8c466..2c64e4239 100644 --- a/src/ast/trigger.rs +++ b/src/ast/trigger.rs @@ -110,6 +110,7 @@ impl fmt::Display for TriggerEvent { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum TriggerPeriod { + For, After, Before, InsteadOf, @@ -118,6 +119,7 @@ pub enum TriggerPeriod { impl fmt::Display for TriggerPeriod { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { + TriggerPeriod::For => write!(f, "FOR"), TriggerPeriod::After => write!(f, "AFTER"), TriggerPeriod::Before => write!(f, "BEFORE"), TriggerPeriod::InsteadOf => write!(f, "INSTEAD OF"), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a347f3d4d..79c345bf7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5317,11 +5317,15 @@ impl<'a> Parser<'a> { or_replace: bool, is_constraint: bool, ) -> Result { - if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect) { + if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect | MsSqlDialect) { self.prev_token(); return self.expected("an object type after CREATE", self.peek_token()); } + if dialect_of!(self is MsSqlDialect) { + return self.parse_mssql_create_trigger(or_replace, is_constraint); + } + let name = self.parse_object_name(false)?; let period = self.parse_trigger_period()?; @@ -5374,18 +5378,73 @@ impl<'a> Parser<'a> { trigger_object, include_each, condition, - exec_body, + exec_body: Some(exec_body), + statements: None, characteristics, }) } + /// Parse `CREATE TRIGGER` for [MsSql] + /// + /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql + pub fn parse_mssql_create_trigger( + &mut self, + or_replace: bool, + is_constraint: bool, + ) -> Result { + let name = self.parse_object_name(false)?; + self.expect_keyword_is(Keyword::ON)?; + let table_name = self.parse_object_name(false)?; + let period = self.parse_trigger_period()?; + let events = self.parse_comma_separated(Parser::parse_trigger_event)?; + + self.expect_keyword_is(Keyword::AS)?; + + let trigger_statements_body = if self.peek_keyword(Keyword::BEGIN) { + let begin_token = self.expect_keyword(Keyword::BEGIN)?; + let statements = self.parse_statement_list(&[Keyword::END])?; + let end_token = self.expect_keyword(Keyword::END)?; + + BeginEndStatements { + begin_token: AttachedToken(begin_token), + statements, + end_token: AttachedToken(end_token), + } + } else { + BeginEndStatements { + begin_token: AttachedToken::empty(), + statements: vec![self.parse_statement()?], + end_token: AttachedToken::empty(), + } + }; + + Ok(Statement::CreateTrigger { + or_replace, + is_constraint, + name, + period, + events, + table_name, + referenced_table_name: None, + referencing: Vec::new(), + trigger_object: TriggerObject::Statement, + include_each: false, + condition: None, + exec_body: None, + statements: Some(trigger_statements_body), + characteristics: None, + }) + } + pub fn parse_trigger_period(&mut self) -> Result { Ok( match self.expect_one_of_keywords(&[ + Keyword::FOR, Keyword::BEFORE, Keyword::AFTER, Keyword::INSTEAD, ])? { + Keyword::FOR => TriggerPeriod::For, Keyword::BEFORE => TriggerPeriod::Before, Keyword::AFTER => TriggerPeriod::After, Keyword::INSTEAD => self diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 8cc5758fd..618f44c18 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -273,6 +273,16 @@ fn parse_create_function() { END\ "; let _ = ms().verified_stmt(create_or_alter_function); + + let create_function_with_return_expression = "\ + CREATE FUNCTION some_scalar_udf(@foo INT, @bar VARCHAR(256)) \ + RETURNS INT \ + AS \ + BEGIN \ + RETURN CONVERT(INT, 1) + 2; \ + END\ + "; + let _ = ms().verified_stmt(create_function_with_return_expression); } #[test] @@ -2199,6 +2209,94 @@ fn parse_mssql_merge_with_output() { ms_and_generic().verified_stmt(stmt); } +#[test] +fn parse_create_trigger() { + let create_trigger = "\ + CREATE TRIGGER reminder1 \ + ON Sales.Customer \ + AFTER INSERT, UPDATE \ + AS RAISERROR('Notify Customer Relations', 16, 10);\ + "; + let create_stmt = ms().verified_stmt(create_trigger); + assert_eq!( + create_stmt, + Statement::CreateTrigger { + or_replace: false, + is_constraint: false, + name: ObjectName::from(vec![Ident::new("reminder1")]), + period: TriggerPeriod::After, + events: vec![TriggerEvent::Insert, TriggerEvent::Update(vec![]),], + table_name: ObjectName::from(vec![Ident::new("Sales"), Ident::new("Customer")]), + referenced_table_name: None, + referencing: vec![], + trigger_object: TriggerObject::Statement, + include_each: false, + condition: None, + exec_body: None, + statements: Some(BeginEndStatements { + begin_token: AttachedToken::empty(), + statements: vec![Statement::RaisError { + message: Box::new(Expr::Value( + (Value::SingleQuotedString("Notify Customer Relations".to_string())) + .with_empty_span() + )), + severity: Box::new(Expr::Value( + (Value::Number("16".parse().unwrap(), false)).with_empty_span() + )), + state: Box::new(Expr::Value( + (Value::Number("10".parse().unwrap(), false)).with_empty_span() + )), + arguments: vec![], + options: vec![], + }], + end_token: AttachedToken::empty(), + }), + characteristics: None, + } + ); + + let multi_statement_trigger = "\ + CREATE TRIGGER some_trigger ON some_table FOR INSERT \ + AS \ + BEGIN \ + DECLARE @var INT; \ + RAISERROR('Trigger fired', 10, 1); \ + END\ + "; + let _ = ms().verified_stmt(multi_statement_trigger); + + let create_trigger_with_return = "\ + CREATE TRIGGER some_trigger ON some_table FOR INSERT \ + AS \ + BEGIN \ + RETURN; \ + END\ + "; + let _ = ms().verified_stmt(create_trigger_with_return); + + let create_trigger_with_return = "\ + CREATE TRIGGER some_trigger ON some_table FOR INSERT \ + AS \ + BEGIN \ + RETURN; \ + END\ + "; + let _ = ms().verified_stmt(create_trigger_with_return); + + let create_trigger_with_conditional = "\ + CREATE TRIGGER some_trigger ON some_table FOR INSERT \ + AS \ + BEGIN \ + IF 1 = 2 \ + BEGIN \ + RAISERROR('Trigger fired', 10, 1); \ + END; \ + RETURN; \ + END\ + "; + let _ = ms().verified_stmt(create_trigger_with_conditional); +} + #[test] fn parse_drop_trigger() { let sql_drop_trigger = "DROP TRIGGER emp_stamp;"; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 4bb1063dd..486a2fc50 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3790,13 +3790,14 @@ fn parse_create_trigger() { trigger_object: TriggerObject::Row, include_each: true, condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("emp_stamp")]), args: None, } - }, + }), + statements: None, characteristics: None, } ); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 1fb7432a4..c0beb8f0e 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5168,13 +5168,14 @@ fn parse_create_simple_before_insert_trigger() { trigger_object: TriggerObject::Row, include_each: true, condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("check_account_insert")]), args: None, }, - }, + }), + statements: None, characteristics: None, }; @@ -5203,13 +5204,14 @@ fn parse_create_after_update_trigger_with_condition() { op: BinaryOperator::Gt, right: Box::new(Expr::value(number("10000"))), }))), - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("check_account_update")]), args: None, }, - }, + }), + statements: None, characteristics: None, }; @@ -5231,13 +5233,14 @@ fn parse_create_instead_of_delete_trigger() { trigger_object: TriggerObject::Row, include_each: true, condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("check_account_deletes")]), args: None, }, - }, + }), + statements: None, characteristics: None, }; @@ -5263,13 +5266,14 @@ fn parse_create_trigger_with_multiple_events_and_deferrable() { trigger_object: TriggerObject::Row, include_each: true, condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("check_account_changes")]), args: None, }, - }, + }), + statements: None, characteristics: Some(ConstraintCharacteristics { deferrable: Some(true), initially: Some(DeferrableInitial::Deferred), @@ -5306,13 +5310,14 @@ fn parse_create_trigger_with_referencing() { trigger_object: TriggerObject::Row, include_each: true, condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("check_account_referencing")]), args: None, }, - }, + }), + statements: None, characteristics: None, }; @@ -5332,7 +5337,7 @@ fn parse_create_trigger_invalid_cases() { ), ( "CREATE TRIGGER check_update TOMORROW UPDATE ON accounts EXECUTE FUNCTION check_account_update", - "Expected: one of BEFORE or AFTER or INSTEAD, found: TOMORROW" + "Expected: one of FOR or BEFORE or AFTER or INSTEAD, found: TOMORROW" ), ( "CREATE TRIGGER check_update BEFORE SAVE ON accounts EXECUTE FUNCTION check_account_update", @@ -5601,13 +5606,14 @@ fn parse_trigger_related_functions() { trigger_object: TriggerObject::Row, include_each: true, condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("emp_stamp")]), args: None, } - }, + }), + statements: None, characteristics: None } ); From 1af22a41d30ee1984878e4d9dbb882090ad6d313 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Fri, 11 Apr 2025 15:38:17 -0400 Subject: [PATCH 2/6] Add `OR ALTER` support for `CREATE TRIGGER` --- src/ast/mod.rs | 8 +++++++- src/parser/mod.rs | 10 +++++++--- tests/sqlparser_mssql.rs | 3 ++- tests/sqlparser_mysql.rs | 1 + tests/sqlparser_postgres.rs | 6 ++++++ 5 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 05321569d..1a9acaa8d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3736,6 +3736,10 @@ pub enum Statement { /// Postgres: /// SQL Server: CreateTrigger { + /// True if this is a `CREATE OR ALTER TRIGGER` statement + /// + /// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql?view=sql-server-ver16#arguments) + or_alter: bool, /// The `OR REPLACE` clause is used to re-create the trigger if it already exists. /// /// Example: @@ -4595,6 +4599,7 @@ impl fmt::Display for Statement { } Statement::CreateFunction(create_function) => create_function.fmt(f), Statement::CreateTrigger { + or_alter, or_replace, is_constraint, name, @@ -4612,7 +4617,8 @@ impl fmt::Display for Statement { } => { write!( f, - "CREATE {or_replace}{is_constraint}TRIGGER {name} ", + "CREATE {or_alter}{or_replace}{is_constraint}TRIGGER {name} ", + or_alter = if *or_alter { "OR ALTER " } else { "" }, or_replace = if *or_replace { "OR REPLACE " } else { "" }, is_constraint = if *is_constraint { "CONSTRAINT " } else { "" }, )?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 79c345bf7..6b4f3e717 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4614,9 +4614,9 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::FUNCTION) { self.parse_create_function(or_alter, or_replace, temporary) } else if self.parse_keyword(Keyword::TRIGGER) { - self.parse_create_trigger(or_replace, false) + self.parse_create_trigger(or_alter, or_replace, false) } else if self.parse_keywords(&[Keyword::CONSTRAINT, Keyword::TRIGGER]) { - self.parse_create_trigger(or_replace, true) + self.parse_create_trigger(or_alter, or_replace, true) } else if self.parse_keyword(Keyword::MACRO) { self.parse_create_macro(or_replace, temporary) } else if self.parse_keyword(Keyword::SECRET) { @@ -5314,6 +5314,7 @@ impl<'a> Parser<'a> { pub fn parse_create_trigger( &mut self, + or_alter: bool, or_replace: bool, is_constraint: bool, ) -> Result { @@ -5323,7 +5324,7 @@ impl<'a> Parser<'a> { } if dialect_of!(self is MsSqlDialect) { - return self.parse_mssql_create_trigger(or_replace, is_constraint); + return self.parse_mssql_create_trigger(or_alter, or_replace, is_constraint); } let name = self.parse_object_name(false)?; @@ -5367,6 +5368,7 @@ impl<'a> Parser<'a> { let exec_body = self.parse_trigger_exec_body()?; Ok(Statement::CreateTrigger { + or_alter, or_replace, is_constraint, name, @@ -5389,6 +5391,7 @@ impl<'a> Parser<'a> { /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql pub fn parse_mssql_create_trigger( &mut self, + or_alter: bool, or_replace: bool, is_constraint: bool, ) -> Result { @@ -5419,6 +5422,7 @@ impl<'a> Parser<'a> { }; Ok(Statement::CreateTrigger { + or_alter, or_replace, is_constraint, name, diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 618f44c18..75a656fdd 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2212,7 +2212,7 @@ fn parse_mssql_merge_with_output() { #[test] fn parse_create_trigger() { let create_trigger = "\ - CREATE TRIGGER reminder1 \ + CREATE OR ALTER TRIGGER reminder1 \ ON Sales.Customer \ AFTER INSERT, UPDATE \ AS RAISERROR('Notify Customer Relations', 16, 10);\ @@ -2221,6 +2221,7 @@ fn parse_create_trigger() { assert_eq!( create_stmt, Statement::CreateTrigger { + or_alter: true, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("reminder1")]), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 486a2fc50..27c60b052 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3779,6 +3779,7 @@ fn parse_create_trigger() { assert_eq!( create_stmt, Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("emp_stamp")]), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index c0beb8f0e..008d0670a 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5157,6 +5157,7 @@ fn test_escaped_string_literal() { fn parse_create_simple_before_insert_trigger() { let sql = "CREATE TRIGGER check_insert BEFORE INSERT ON accounts FOR EACH ROW EXECUTE FUNCTION check_account_insert"; let expected = Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_insert")]), @@ -5186,6 +5187,7 @@ fn parse_create_simple_before_insert_trigger() { fn parse_create_after_update_trigger_with_condition() { let sql = "CREATE TRIGGER check_update AFTER UPDATE ON accounts FOR EACH ROW WHEN (NEW.balance > 10000) EXECUTE FUNCTION check_account_update"; let expected = Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_update")]), @@ -5222,6 +5224,7 @@ fn parse_create_after_update_trigger_with_condition() { fn parse_create_instead_of_delete_trigger() { let sql = "CREATE TRIGGER check_delete INSTEAD OF DELETE ON accounts FOR EACH ROW EXECUTE FUNCTION check_account_deletes"; let expected = Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_delete")]), @@ -5251,6 +5254,7 @@ fn parse_create_instead_of_delete_trigger() { fn parse_create_trigger_with_multiple_events_and_deferrable() { let sql = "CREATE CONSTRAINT TRIGGER check_multiple_events BEFORE INSERT OR UPDATE OR DELETE ON accounts DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION check_account_changes"; let expected = Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: true, name: ObjectName::from(vec![Ident::new("check_multiple_events")]), @@ -5288,6 +5292,7 @@ fn parse_create_trigger_with_multiple_events_and_deferrable() { fn parse_create_trigger_with_referencing() { let sql = "CREATE TRIGGER check_referencing BEFORE INSERT ON accounts REFERENCING NEW TABLE AS new_accounts OLD TABLE AS old_accounts FOR EACH ROW EXECUTE FUNCTION check_account_referencing"; let expected = Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_referencing")]), @@ -5595,6 +5600,7 @@ fn parse_trigger_related_functions() { assert_eq!( create_trigger, Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("emp_stamp")]), From 782a9de525976451ef696a0f207d142b2fe80046 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 29 Apr 2025 16:23:10 -0400 Subject: [PATCH 3/6] Move `parse_mssql_create_trigger` into `MsSqlDialect` --- src/dialect/mssql.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++ src/parser/mod.rs | 58 ---------------------------------------- 2 files changed, 63 insertions(+), 58 deletions(-) diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 31e324f06..a9ffb71f8 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -18,6 +18,7 @@ use crate::ast::helpers::attached_token::AttachedToken; use crate::ast::{ BeginEndStatements, ConditionalStatementBlock, ConditionalStatements, IfStatement, Statement, + TriggerObject, }; use crate::dialect::Dialect; use crate::keywords::{self, Keyword}; @@ -125,6 +126,15 @@ impl Dialect for MsSqlDialect { fn parse_statement(&self, parser: &mut Parser) -> Option> { if parser.peek_keyword(Keyword::IF) { Some(self.parse_if_stmt(parser)) + } else if parser.parse_keywords(&[Keyword::CREATE, Keyword::TRIGGER]) { + Some(self.parse_create_trigger(parser, false)) + } else if parser.parse_keywords(&[ + Keyword::CREATE, + Keyword::OR, + Keyword::ALTER, + Keyword::TRIGGER, + ]) { + Some(self.parse_create_trigger(parser, true)) } else { None } @@ -215,6 +225,59 @@ impl MsSqlDialect { })) } + /// Parse `CREATE TRIGGER` for [MsSql] + /// + /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql + fn parse_create_trigger( + &self, + parser: &mut Parser, + or_alter: bool, + ) -> Result { + let name = parser.parse_object_name(false)?; + parser.expect_keyword_is(Keyword::ON)?; + let table_name = parser.parse_object_name(false)?; + let period = parser.parse_trigger_period()?; + let events = parser.parse_comma_separated(Parser::parse_trigger_event)?; + + parser.expect_keyword_is(Keyword::AS)?; + + let trigger_statements_body = if parser.peek_keyword(Keyword::BEGIN) { + let begin_token = parser.expect_keyword(Keyword::BEGIN)?; + let statements = parser.parse_statement_list(&[Keyword::END])?; + let end_token = parser.expect_keyword(Keyword::END)?; + + BeginEndStatements { + begin_token: AttachedToken(begin_token), + statements, + end_token: AttachedToken(end_token), + } + } else { + BeginEndStatements { + begin_token: AttachedToken::empty(), + statements: vec![parser.parse_statement()?], + end_token: AttachedToken::empty(), + } + }; + + Ok(Statement::CreateTrigger { + or_alter, + or_replace: false, + is_constraint: false, + name, + period, + events, + table_name, + referenced_table_name: None, + referencing: Vec::new(), + trigger_object: TriggerObject::Statement, + include_each: false, + condition: None, + exec_body: None, + statements: Some(trigger_statements_body), + characteristics: None, + }) + } + /// Parse a sequence of statements, optionally separated by semicolon. /// /// Stops parsing when reaching EOF or the given keyword. diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6b4f3e717..6044d7ae0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5323,10 +5323,6 @@ impl<'a> Parser<'a> { return self.expected("an object type after CREATE", self.peek_token()); } - if dialect_of!(self is MsSqlDialect) { - return self.parse_mssql_create_trigger(or_alter, or_replace, is_constraint); - } - let name = self.parse_object_name(false)?; let period = self.parse_trigger_period()?; @@ -5386,60 +5382,6 @@ impl<'a> Parser<'a> { }) } - /// Parse `CREATE TRIGGER` for [MsSql] - /// - /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql - pub fn parse_mssql_create_trigger( - &mut self, - or_alter: bool, - or_replace: bool, - is_constraint: bool, - ) -> Result { - let name = self.parse_object_name(false)?; - self.expect_keyword_is(Keyword::ON)?; - let table_name = self.parse_object_name(false)?; - let period = self.parse_trigger_period()?; - let events = self.parse_comma_separated(Parser::parse_trigger_event)?; - - self.expect_keyword_is(Keyword::AS)?; - - let trigger_statements_body = if self.peek_keyword(Keyword::BEGIN) { - let begin_token = self.expect_keyword(Keyword::BEGIN)?; - let statements = self.parse_statement_list(&[Keyword::END])?; - let end_token = self.expect_keyword(Keyword::END)?; - - BeginEndStatements { - begin_token: AttachedToken(begin_token), - statements, - end_token: AttachedToken(end_token), - } - } else { - BeginEndStatements { - begin_token: AttachedToken::empty(), - statements: vec![self.parse_statement()?], - end_token: AttachedToken::empty(), - } - }; - - Ok(Statement::CreateTrigger { - or_alter, - or_replace, - is_constraint, - name, - period, - events, - table_name, - referenced_table_name: None, - referencing: Vec::new(), - trigger_object: TriggerObject::Statement, - include_each: false, - condition: None, - exec_body: None, - statements: Some(trigger_statements_body), - characteristics: None, - }) - } - pub fn parse_trigger_period(&mut self) -> Result { Ok( match self.expect_one_of_keywords(&[ From b25a8d2b4fab85724fdafaf6663cafc0cbef7bf0 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Wed, 30 Apr 2025 17:24:49 -0400 Subject: [PATCH 4/6] Enable parsing multiple statements without BEGIN/END --- src/dialect/mssql.rs | 2 +- tests/sqlparser_mssql.rs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index a9ffb71f8..6c90b714c 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -254,7 +254,7 @@ impl MsSqlDialect { } else { BeginEndStatements { begin_token: AttachedToken::empty(), - statements: vec![parser.parse_statement()?], + statements: parser.parse_statements()?, end_token: AttachedToken::empty(), } }; diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 75a656fdd..bca13fde0 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2256,6 +2256,14 @@ fn parse_create_trigger() { } ); + let multi_statement_as_trigger = "\ + CREATE TRIGGER some_trigger ON some_table FOR INSERT \ + AS \ + DECLARE @var INT; \ + RAISERROR('Trigger fired', 10, 1);\ + "; + let _ = ms().verified_stmt(multi_statement_as_trigger); + let multi_statement_trigger = "\ CREATE TRIGGER some_trigger ON some_table FOR INSERT \ AS \ From 000fe3f6a769dd5424e91a87c1e6ed632d4dfd37 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Fri, 2 May 2025 11:14:05 -0400 Subject: [PATCH 5/6] Switch from empty tokens to existing statement enum option --- src/ast/mod.rs | 2 +- src/dialect/mssql.rs | 8 +++----- tests/sqlparser_mssql.rs | 4 +--- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1a9acaa8d..c3009743d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3802,7 +3802,7 @@ pub enum Statement { /// Execute logic block exec_body: Option, /// For SQL dialects with statement(s) for a body - statements: Option, + statements: Option, /// The characteristic of the trigger, which include whether the trigger is `DEFERRABLE`, `INITIALLY DEFERRED`, or `INITIALLY IMMEDIATE`, characteristics: Option, }, diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 6c90b714c..d0cb74e3d 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -246,16 +246,14 @@ impl MsSqlDialect { let statements = parser.parse_statement_list(&[Keyword::END])?; let end_token = parser.expect_keyword(Keyword::END)?; - BeginEndStatements { + ConditionalStatements::BeginEnd(BeginEndStatements { begin_token: AttachedToken(begin_token), statements, end_token: AttachedToken(end_token), - } + }) } else { - BeginEndStatements { - begin_token: AttachedToken::empty(), + ConditionalStatements::Sequence { statements: parser.parse_statements()?, - end_token: AttachedToken::empty(), } }; diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index bca13fde0..9ff55198f 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2234,8 +2234,7 @@ fn parse_create_trigger() { include_each: false, condition: None, exec_body: None, - statements: Some(BeginEndStatements { - begin_token: AttachedToken::empty(), + statements: Some(ConditionalStatements::Sequence { statements: vec![Statement::RaisError { message: Box::new(Expr::Value( (Value::SingleQuotedString("Notify Customer Relations".to_string())) @@ -2250,7 +2249,6 @@ fn parse_create_trigger() { arguments: vec![], options: vec![], }], - end_token: AttachedToken::empty(), }), characteristics: None, } From ef6aa55dec737ecb53053d94cf9569e15db8ea9d Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Fri, 2 May 2025 11:24:35 -0400 Subject: [PATCH 6/6] Add `parse_conditional_statements` & use it --- src/dialect/mssql.rs | 19 ++----------------- src/parser/mod.rs | 30 +++++++++++++++++++++--------- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index d0cb74e3d..647e82a2a 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -240,22 +240,7 @@ impl MsSqlDialect { let events = parser.parse_comma_separated(Parser::parse_trigger_event)?; parser.expect_keyword_is(Keyword::AS)?; - - let trigger_statements_body = if parser.peek_keyword(Keyword::BEGIN) { - let begin_token = parser.expect_keyword(Keyword::BEGIN)?; - let statements = parser.parse_statement_list(&[Keyword::END])?; - let end_token = parser.expect_keyword(Keyword::END)?; - - ConditionalStatements::BeginEnd(BeginEndStatements { - begin_token: AttachedToken(begin_token), - statements, - end_token: AttachedToken(end_token), - }) - } else { - ConditionalStatements::Sequence { - statements: parser.parse_statements()?, - } - }; + let statements = Some(parser.parse_conditional_statements(&[Keyword::END])?); Ok(Statement::CreateTrigger { or_alter, @@ -271,7 +256,7 @@ impl MsSqlDialect { include_each: false, condition: None, exec_body: None, - statements: Some(trigger_statements_body), + statements, characteristics: None, }) } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6044d7ae0..2011d31e3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -745,26 +745,38 @@ impl<'a> Parser<'a> { } }; + let conditional_statements = self.parse_conditional_statements(terminal_keywords)?; + + Ok(ConditionalStatementBlock { + start_token: AttachedToken(start_token), + condition, + then_token, + conditional_statements, + }) + } + + /// Parse a BEGIN/END block or a sequence of statements + /// This could be inside of a conditional (IF, CASE, WHILE etc.) or an object body defined optionally BEGIN/END and one or more statements. + pub(crate) fn parse_conditional_statements( + &mut self, + terminal_keywords: &[Keyword], + ) -> Result { let conditional_statements = if self.peek_keyword(Keyword::BEGIN) { let begin_token = self.expect_keyword(Keyword::BEGIN)?; let statements = self.parse_statement_list(terminal_keywords)?; let end_token = self.expect_keyword(Keyword::END)?; + ConditionalStatements::BeginEnd(BeginEndStatements { begin_token: AttachedToken(begin_token), statements, end_token: AttachedToken(end_token), }) } else { - let statements = self.parse_statement_list(terminal_keywords)?; - ConditionalStatements::Sequence { statements } + ConditionalStatements::Sequence { + statements: self.parse_statement_list(terminal_keywords)?, + } }; - - Ok(ConditionalStatementBlock { - start_token: AttachedToken(start_token), - condition, - then_token, - conditional_statements, - }) + Ok(conditional_statements) } /// Parse a `RAISE` statement.