From 010dbd72fa228605e2238f6e677a45568d64bfa4 Mon Sep 17 00:00:00 2001 From: Luca Date: Thu, 18 Sep 2025 08:18:00 +0200 Subject: [PATCH 01/10] Added support for SQLite triggers, including TEMP and optional FOR EACH ROW --- src/ast/ddl.rs | 46 ++++-- src/dialect/mssql.rs | 5 +- src/parser/mod.rs | 45 ++++-- tests/sqlparser_mssql.rs | 5 +- tests/sqlparser_mysql.rs | 5 +- tests/sqlparser_postgres.rs | 30 ++-- tests/sqlparser_sqlite.rs | 281 ++++++++++++++++++++++++++++++++++++ 7 files changed, 373 insertions(+), 44 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index a15474755..d59732183 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3199,6 +3199,22 @@ pub struct CreateTrigger { /// /// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql?view=sql-server-ver16#arguments) pub or_alter: bool, + /// True if this is a temporary trigger, which is supported in SQLite. + /// + /// The possible syntaxes are two: + /// + /// ```sql + /// CREATE TEMP TRIGGER trigger_name + /// ``` + /// + /// or + /// + /// ```sql + /// CREATE TEMPORARY TRIGGER trigger_name + /// ``` + /// + /// [Temporary Triggers in SQLite](https://sqlite.org/lang_createtrigger.html#temp_triggers_on_non_temp_tables) + pub temporary: bool, /// The `OR REPLACE` clause is used to re-create the trigger if it already exists. /// /// Example: @@ -3243,14 +3259,16 @@ pub struct CreateTrigger { /// ``` pub period: TriggerPeriod, /// Whether the trigger period was specified before the target table name. + /// This does not refer to whether the period is BEFORE, AFTER, or INSTEAD OF, + /// but rather the position of the period clause in relation to the table name. /// /// ```sql - /// -- period_before_table == true: Postgres, MySQL, and standard SQL + /// -- period_specified_before_table == true: Postgres, MySQL, and standard SQL /// CREATE TRIGGER t BEFORE INSERT ON table_name ...; - /// -- period_before_table == false: MSSQL + /// -- period_specified_before_table == false: MSSQL /// CREATE TRIGGER t ON table_name BEFORE INSERT ...; /// ``` - pub period_before_table: bool, + pub period_specified_before_table: bool, /// Multiple events can be specified using OR, such as `INSERT`, `UPDATE`, `DELETE`, or `TRUNCATE`. pub events: Vec, /// The table on which the trigger is to be created. @@ -3262,7 +3280,9 @@ pub struct CreateTrigger { pub referencing: Vec, /// This specifies whether the trigger function should be fired once for /// every row affected by the trigger event, or just once per SQL statement. - pub trigger_object: TriggerObject, + /// This is optional in some SQL dialects, such as SQLite, and if not specified, in + /// those cases, the implied default is `FOR EACH ROW`. + pub trigger_object: Option, /// Whether to include the `EACH` term of the `FOR EACH`, as it is optional syntax. pub include_each: bool, /// Triggering conditions @@ -3281,10 +3301,11 @@ impl Display for CreateTrigger { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let CreateTrigger { or_alter, + temporary, or_replace, is_constraint, name, - period_before_table, + period_specified_before_table, period, events, table_name, @@ -3300,13 +3321,14 @@ impl Display for CreateTrigger { } = self; write!( f, - "CREATE {or_alter}{or_replace}{is_constraint}TRIGGER {name} ", + "CREATE {temporary}{or_alter}{or_replace}{is_constraint}TRIGGER {name} ", + temporary = if *temporary { "TEMPORARY " } else { "" }, or_alter = if *or_alter { "OR ALTER " } else { "" }, or_replace = if *or_replace { "OR REPLACE " } else { "" }, is_constraint = if *is_constraint { "CONSTRAINT " } else { "" }, )?; - if *period_before_table { + if *period_specified_before_table { write!(f, "{period}")?; if !events.is_empty() { write!(f, " {}", display_separated(events, " OR "))?; @@ -3332,10 +3354,12 @@ impl Display for CreateTrigger { write!(f, " REFERENCING {}", display_separated(referencing, " "))?; } - if *include_each { - write!(f, " FOR EACH {trigger_object}")?; - } else if exec_body.is_some() { - write!(f, " FOR {trigger_object}")?; + if let Some(trigger_object) = trigger_object { + if *include_each { + write!(f, " FOR EACH {trigger_object}")?; + } else if exec_body.is_some() { + write!(f, " FOR {trigger_object}")?; + } } if let Some(condition) = condition { write!(f, " WHEN {condition}")?; diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 4fcc0e4b6..6ee46c442 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -254,16 +254,17 @@ impl MsSqlDialect { Ok(CreateTrigger { or_alter, + temporary: false, or_replace: false, is_constraint: false, name, period, - period_before_table: false, + period_specified_before_table: false, events, table_name, referenced_table_name: None, referencing: Vec::new(), - trigger_object: TriggerObject::Statement, + trigger_object: Some(TriggerObject::Statement), include_each: false, condition: None, exec_body: None, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 819819f9d..82217ec4e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4750,9 +4750,9 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::DOMAIN) { self.parse_create_domain() } else if self.parse_keyword(Keyword::TRIGGER) { - self.parse_create_trigger(or_alter, or_replace, false) + self.parse_create_trigger(temporary, or_alter, or_replace, false) } else if self.parse_keywords(&[Keyword::CONSTRAINT, Keyword::TRIGGER]) { - self.parse_create_trigger(or_alter, or_replace, true) + self.parse_create_trigger(temporary, 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) { @@ -5546,7 +5546,8 @@ impl<'a> Parser<'a> { /// DROP TRIGGER [ IF EXISTS ] name ON table_name [ CASCADE | RESTRICT ] /// ``` pub fn parse_drop_trigger(&mut self) -> Result { - if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect | MsSqlDialect) { + if !dialect_of!(self is PostgreSqlDialect | SQLiteDialect | GenericDialect | MySqlDialect | MsSqlDialect) + { self.prev_token(); return self.expected("an object type after DROP", self.peek_token()); } @@ -5574,11 +5575,13 @@ impl<'a> Parser<'a> { pub fn parse_create_trigger( &mut self, + temporary: bool, or_alter: bool, or_replace: bool, is_constraint: bool, ) -> Result { - if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect | MsSqlDialect) { + if !dialect_of!(self is PostgreSqlDialect | SQLiteDialect | GenericDialect | MySqlDialect | MsSqlDialect) + { self.prev_token(); return self.expected("an object type after CREATE", self.peek_token()); } @@ -5605,14 +5608,24 @@ impl<'a> Parser<'a> { } } - self.expect_keyword_is(Keyword::FOR)?; - let include_each = self.parse_keyword(Keyword::EACH); - let trigger_object = - match self.expect_one_of_keywords(&[Keyword::ROW, Keyword::STATEMENT])? { - Keyword::ROW => TriggerObject::Row, - Keyword::STATEMENT => TriggerObject::Statement, - _ => unreachable!(), - }; + let (include_each, trigger_object) = if self.parse_keyword(Keyword::FOR) { + ( + self.parse_keyword(Keyword::EACH), + Some( + match self.expect_one_of_keywords(&[Keyword::ROW, Keyword::STATEMENT])? { + Keyword::ROW => TriggerObject::Row, + Keyword::STATEMENT => TriggerObject::Statement, + _ => unreachable!(), + }, + ), + ) + } else { + if !dialect_of!(self is SQLiteDialect ) { + self.expect_keyword_is(Keyword::FOR)?; + } + + (false, None) + }; let condition = self .parse_keyword(Keyword::WHEN) @@ -5627,13 +5640,14 @@ impl<'a> Parser<'a> { statements = Some(self.parse_conditional_statements(&[Keyword::END])?); } - Ok(Statement::CreateTrigger(CreateTrigger { + Ok(CreateTrigger { or_alter, + temporary, or_replace, is_constraint, name, period, - period_before_table: true, + period_specified_before_table: true, events, table_name, referenced_table_name, @@ -5645,7 +5659,8 @@ impl<'a> Parser<'a> { statements_as: false, statements, characteristics, - })) + } + .into()) } pub fn parse_trigger_period(&mut self) -> Result { diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index b1ad422ec..7ba1c3987 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2386,16 +2386,17 @@ fn parse_create_trigger() { create_stmt, Statement::CreateTrigger(CreateTrigger { or_alter: true, + temporary: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("reminder1")]), period: TriggerPeriod::After, - period_before_table: false, + period_specified_before_table: false, 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, + trigger_object: Some(TriggerObject::Statement), include_each: false, condition: None, exec_body: None, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 0857bae3b..219d314ef 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3924,16 +3924,17 @@ fn parse_create_trigger() { create_stmt, Statement::CreateTrigger(CreateTrigger { or_alter: false, + temporary: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("emp_stamp")]), period: TriggerPeriod::Before, - period_before_table: true, + period_specified_before_table: true, events: vec![TriggerEvent::Insert], table_name: ObjectName::from(vec![Ident::new("emp")]), referenced_table_name: None, referencing: vec![], - trigger_object: TriggerObject::Row, + trigger_object: Some(TriggerObject::Row), include_each: true, condition: None, exec_body: Some(TriggerExecBody { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 5b95bb300..72eee1de8 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5673,16 +5673,17 @@ 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(CreateTrigger { or_alter: false, + temporary: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_insert")]), period: TriggerPeriod::Before, - period_before_table: true, + period_specified_before_table: true, events: vec![TriggerEvent::Insert], table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], - trigger_object: TriggerObject::Row, + trigger_object: Some(TriggerObject::Row), include_each: true, condition: None, exec_body: Some(TriggerExecBody { @@ -5705,16 +5706,17 @@ 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(CreateTrigger { or_alter: false, + temporary: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_update")]), period: TriggerPeriod::After, - period_before_table: true, + period_specified_before_table: true, events: vec![TriggerEvent::Update(vec![])], table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], - trigger_object: TriggerObject::Row, + trigger_object: Some(TriggerObject::Row), include_each: true, condition: Some(Expr::Nested(Box::new(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ @@ -5744,16 +5746,17 @@ 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(CreateTrigger { or_alter: false, + temporary: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_delete")]), period: TriggerPeriod::InsteadOf, - period_before_table: true, + period_specified_before_table: true, events: vec![TriggerEvent::Delete], table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], - trigger_object: TriggerObject::Row, + trigger_object: Some(TriggerObject::Row), include_each: true, condition: None, exec_body: Some(TriggerExecBody { @@ -5776,11 +5779,12 @@ 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(CreateTrigger { or_alter: false, + temporary: false, or_replace: false, is_constraint: true, name: ObjectName::from(vec![Ident::new("check_multiple_events")]), period: TriggerPeriod::Before, - period_before_table: true, + period_specified_before_table: true, events: vec![ TriggerEvent::Insert, TriggerEvent::Update(vec![]), @@ -5789,7 +5793,7 @@ fn parse_create_trigger_with_multiple_events_and_deferrable() { table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], - trigger_object: TriggerObject::Row, + trigger_object: Some(TriggerObject::Row), include_each: true, condition: None, exec_body: Some(TriggerExecBody { @@ -5816,11 +5820,12 @@ 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(CreateTrigger { or_alter: false, + temporary: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_referencing")]), period: TriggerPeriod::Before, - period_before_table: true, + period_specified_before_table: true, events: vec![TriggerEvent::Insert], table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, @@ -5836,7 +5841,7 @@ fn parse_create_trigger_with_referencing() { transition_relation_name: ObjectName::from(vec![Ident::new("old_accounts")]), }, ], - trigger_object: TriggerObject::Row, + trigger_object: Some(TriggerObject::Row), include_each: true, condition: None, exec_body: Some(TriggerExecBody { @@ -6132,16 +6137,17 @@ fn parse_trigger_related_functions() { create_trigger, Statement::CreateTrigger(CreateTrigger { or_alter: false, + temporary: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("emp_stamp")]), period: TriggerPeriod::Before, - period_before_table: true, + period_specified_before_table: true, events: vec![TriggerEvent::Insert, TriggerEvent::Update(vec![])], table_name: ObjectName::from(vec![Ident::new("emp")]), referenced_table_name: None, referencing: vec![], - trigger_object: TriggerObject::Row, + trigger_object: Some(TriggerObject::Row), include_each: true, condition: None, exec_body: Some(TriggerExecBody { diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 114aca03a..183ce6627 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -610,6 +610,287 @@ fn test_update_delete_limit() { } } +#[test] +fn test_create_trigger() { + let statement1 = "CREATE TRIGGER trg_inherit_asset_models AFTER INSERT ON assets FOR EACH ROW BEGIN INSERT INTO users (name) SELECT pam.name FROM users AS pam; END"; + + match sqlite().verified_stmt(statement1) { + Statement::CreateTrigger(CreateTrigger { + or_alter, + temporary, + or_replace, + is_constraint, + name, + period, + period_specified_before_table, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + include_each, + condition, + exec_body: _, + statements_as, + statements: _, + characteristics, + }) => { + assert!(!or_alter); + assert!(!temporary); + assert!(!or_replace); + assert!(!is_constraint); + assert_eq!(name.to_string(), "trg_inherit_asset_models"); + assert_eq!(period, TriggerPeriod::After); + assert_eq!(period_specified_before_table, true); + assert_eq!(events, vec![TriggerEvent::Insert]); + assert_eq!(table_name.to_string(), "assets"); + assert!(referenced_table_name.is_none()); + assert!(referencing.is_empty()); + assert_eq!(trigger_object, Some(TriggerObject::Row)); + assert!(include_each); + assert!(condition.is_none()); + assert!(!statements_as); + assert!(characteristics.is_none()); + } + _ => unreachable!("Expected CREATE TRIGGER statement"), + } + + // Here we check that the variant of CREATE TRIGGER that omits the `FOR EACH ROW` clause, + // which in SQLite may be implicitly assumed, is parsed correctly. + let statement2 = "CREATE TRIGGER log_new_user AFTER INSERT ON users BEGIN INSERT INTO user_log (user_id, action, timestamp) VALUES (NEW.id, 'created', datetime('now')); END"; + + match sqlite().verified_stmt(statement2) { + Statement::CreateTrigger(CreateTrigger { + or_alter, + temporary, + or_replace, + is_constraint, + name, + period, + period_specified_before_table, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + include_each, + condition, + exec_body: _, + statements_as, + statements: _, + characteristics, + }) => { + assert!(!or_alter); + assert!(!temporary); + assert!(!or_replace); + assert!(!is_constraint); + assert_eq!(name.to_string(), "log_new_user"); + assert_eq!(period, TriggerPeriod::After); + assert_eq!(period_specified_before_table, true); + assert_eq!(events, vec![TriggerEvent::Insert]); + assert_eq!(table_name.to_string(), "users"); + assert!(referenced_table_name.is_none()); + assert!(referencing.is_empty()); + assert!(trigger_object.is_none()); + assert!(!include_each); + assert!(condition.is_none()); + assert!(!statements_as); + assert!(characteristics.is_none()); + } + _ => unreachable!("Expected CREATE TRIGGER statement"), + } + + let statement3 = "CREATE TRIGGER cleanup_orders AFTER DELETE ON customers BEGIN DELETE FROM orders WHERE customer_id = OLD.id; DELETE FROM invoices WHERE customer_id = OLD.id; END"; + match sqlite().verified_stmt(statement3) { + Statement::CreateTrigger(CreateTrigger { + or_alter, + temporary, + or_replace, + is_constraint, + name, + period, + period_specified_before_table, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + include_each, + condition, + exec_body: _, + statements_as, + statements: _, + characteristics, + }) => { + assert!(!or_alter); + assert!(!temporary); + assert!(!or_replace); + assert!(!is_constraint); + assert_eq!(name.to_string(), "cleanup_orders"); + assert_eq!(period, TriggerPeriod::After); + assert_eq!(period_specified_before_table, true); + assert_eq!(events, vec![TriggerEvent::Delete]); + assert_eq!(table_name.to_string(), "customers"); + assert!(referenced_table_name.is_none()); + assert!(referencing.is_empty()); + assert!(trigger_object.is_none()); + assert!(!include_each); + assert!(condition.is_none()); + assert!(!statements_as); + assert!(characteristics.is_none()); + } + _ => unreachable!("Expected CREATE TRIGGER statement"), + } + + let statement4 = "CREATE TRIGGER trg_before_update BEFORE UPDATE ON products FOR EACH ROW WHEN NEW.price < 0 BEGIN SELECT RAISE(ABORT, 'Price cannot be negative'); END"; + match sqlite().verified_stmt(statement4) { + Statement::CreateTrigger(CreateTrigger { + or_alter, + temporary, + or_replace, + is_constraint, + name, + period, + period_specified_before_table, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + include_each, + condition, + exec_body: _, + statements_as, + statements: _, + characteristics, + }) => { + assert!(!or_alter); + assert!(!temporary); + assert!(!or_replace); + assert!(!is_constraint); + assert_eq!(name.to_string(), "trg_before_update"); + assert_eq!(period, TriggerPeriod::Before); + assert_eq!(period_specified_before_table, true); + assert_eq!(events, vec![TriggerEvent::Update(Vec::new())]); + assert_eq!(table_name.to_string(), "products"); + assert!(referenced_table_name.is_none()); + assert!(referencing.is_empty()); + assert_eq!(trigger_object, Some(TriggerObject::Row)); + assert!(include_each); + assert!(condition.is_some()); + assert!(!statements_as); + assert!(characteristics.is_none()); + } + _ => unreachable!("Expected CREATE TRIGGER statement"), + } + + // We test a INSTEAD OF trigger on a view + let statement5 = "CREATE TRIGGER trg_instead_of_insert INSTEAD OF INSERT ON my_view BEGIN INSERT INTO my_table (col1, col2) VALUES (NEW.col1, NEW.col2); END"; + match sqlite().verified_stmt(statement5) { + Statement::CreateTrigger(CreateTrigger { + or_alter, + temporary, + or_replace, + is_constraint, + name, + period, + period_specified_before_table, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + include_each, + condition, + exec_body: _, + statements_as, + statements: _, + characteristics, + }) => { + assert!(!or_alter); + assert!(!temporary); + assert!(!or_replace); + assert!(!is_constraint); + assert_eq!(name.to_string(), "trg_instead_of_insert"); + assert_eq!(period, TriggerPeriod::InsteadOf); + assert_eq!(period_specified_before_table, true); + assert_eq!(events, vec![TriggerEvent::Insert]); + assert_eq!(table_name.to_string(), "my_view"); + assert!(referenced_table_name.is_none()); + assert!(referencing.is_empty()); + assert!(trigger_object.is_none()); + assert!(!include_each); + assert!(condition.is_none()); + assert!(!statements_as); + assert!(characteristics.is_none()); + } + _ => unreachable!("Expected CREATE TRIGGER statement"), + } + + // We test a temporary trigger + let statement6 = "CREATE TEMPORARY TRIGGER temp_trigger AFTER INSERT ON temp_table BEGIN UPDATE log_table SET count = count + 1; END"; + match sqlite().verified_stmt(statement6) { + Statement::CreateTrigger(CreateTrigger { + or_alter, + temporary, + or_replace, + is_constraint, + name, + period, + period_specified_before_table, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + include_each, + condition, + exec_body: _, + statements_as, + statements: _, + characteristics, + }) => { + assert!(!or_alter); + assert!(temporary); + assert!(!or_replace); + assert!(!is_constraint); + assert_eq!(name.to_string(), "temp_trigger"); + assert_eq!(period, TriggerPeriod::After); + assert_eq!(period_specified_before_table, true); + assert_eq!(events, vec![TriggerEvent::Insert]); + assert_eq!(table_name.to_string(), "temp_table"); + assert!(referenced_table_name.is_none()); + assert!(referencing.is_empty()); + assert!(trigger_object.is_none()); + assert!(!include_each); + assert!(condition.is_none()); + assert!(!statements_as); + assert!(characteristics.is_none()); + } + _ => unreachable!("Expected CREATE TRIGGER statement"), + } +} + +#[test] +fn test_drop_trigger() { + let statement = "DROP TRIGGER IF EXISTS trg_inherit_asset_models"; + + match sqlite().verified_stmt(statement) { + Statement::DropTrigger(DropTrigger { + if_exists, + trigger_name, + table_name, + option, + }) => { + assert!(if_exists); + assert_eq!(trigger_name.to_string(), "trg_inherit_asset_models"); + assert!(table_name.is_none()); + assert!(option.is_none()); + } + _ => unreachable!("Expected DROP TRIGGER statement"), + } +} + fn sqlite() -> TestedDialects { TestedDialects::new(vec![Box::new(SQLiteDialect {})]) } From 1c83a65987bec184167fa387d17a8677acd5bbbb Mon Sep 17 00:00:00 2001 From: Luca Date: Thu, 18 Sep 2025 08:24:44 +0200 Subject: [PATCH 02/10] Resolved code smell --- tests/sqlparser_sqlite.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 183ce6627..9ea20945b 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -641,7 +641,7 @@ fn test_create_trigger() { assert!(!is_constraint); assert_eq!(name.to_string(), "trg_inherit_asset_models"); assert_eq!(period, TriggerPeriod::After); - assert_eq!(period_specified_before_table, true); + assert!(period_specified_before_table); assert_eq!(events, vec![TriggerEvent::Insert]); assert_eq!(table_name.to_string(), "assets"); assert!(referenced_table_name.is_none()); @@ -686,7 +686,7 @@ fn test_create_trigger() { assert!(!is_constraint); assert_eq!(name.to_string(), "log_new_user"); assert_eq!(period, TriggerPeriod::After); - assert_eq!(period_specified_before_table, true); + assert!(period_specified_before_table); assert_eq!(events, vec![TriggerEvent::Insert]); assert_eq!(table_name.to_string(), "users"); assert!(referenced_table_name.is_none()); @@ -728,7 +728,7 @@ fn test_create_trigger() { assert!(!is_constraint); assert_eq!(name.to_string(), "cleanup_orders"); assert_eq!(period, TriggerPeriod::After); - assert_eq!(period_specified_before_table, true); + assert!(period_specified_before_table); assert_eq!(events, vec![TriggerEvent::Delete]); assert_eq!(table_name.to_string(), "customers"); assert!(referenced_table_name.is_none()); @@ -770,7 +770,7 @@ fn test_create_trigger() { assert!(!is_constraint); assert_eq!(name.to_string(), "trg_before_update"); assert_eq!(period, TriggerPeriod::Before); - assert_eq!(period_specified_before_table, true); + assert!(period_specified_before_table); assert_eq!(events, vec![TriggerEvent::Update(Vec::new())]); assert_eq!(table_name.to_string(), "products"); assert!(referenced_table_name.is_none()); @@ -813,7 +813,7 @@ fn test_create_trigger() { assert!(!is_constraint); assert_eq!(name.to_string(), "trg_instead_of_insert"); assert_eq!(period, TriggerPeriod::InsteadOf); - assert_eq!(period_specified_before_table, true); + assert!(period_specified_before_table); assert_eq!(events, vec![TriggerEvent::Insert]); assert_eq!(table_name.to_string(), "my_view"); assert!(referenced_table_name.is_none()); @@ -856,7 +856,7 @@ fn test_create_trigger() { assert!(!is_constraint); assert_eq!(name.to_string(), "temp_trigger"); assert_eq!(period, TriggerPeriod::After); - assert_eq!(period_specified_before_table, true); + assert!(period_specified_before_table); assert_eq!(events, vec![TriggerEvent::Insert]); assert_eq!(table_name.to_string(), "temp_table"); assert!(referenced_table_name.is_none()); From fe0c23f8a253a7f4bb0b268c2eaa53ec75478f64 Mon Sep 17 00:00:00 2001 From: Luca Date: Thu, 18 Sep 2025 10:03:25 +0200 Subject: [PATCH 03/10] Now raising an error if parsing a CREATE TRIGGER statement alongside unsupported modifiers --- src/parser/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 82217ec4e..2a66ac5a0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5581,6 +5581,7 @@ impl<'a> Parser<'a> { is_constraint: bool, ) -> Result { if !dialect_of!(self is PostgreSqlDialect | SQLiteDialect | GenericDialect | MySqlDialect | MsSqlDialect) + || dialect_of!(self is SQLiteDialect) && (or_alter || or_replace || is_constraint) { self.prev_token(); return self.expected("an object type after CREATE", self.peek_token()); From c893bda671a2dcf90cb0b7c22270782bcaaaa5fa Mon Sep 17 00:00:00 2001 From: Luca Date: Wed, 24 Sep 2025 11:53:47 +0200 Subject: [PATCH 04/10] Introduced `TriggerObjectKind` and removed `include_each` in `CreateTrigger` --- src/ast/ddl.rs | 31 ++++++++++++++++++++++--------- src/ast/mod.rs | 4 ++-- src/dialect/mssql.rs | 5 ++--- src/parser/mod.rs | 28 +++++++++++++++------------- tests/sqlparser_mssql.rs | 3 +-- tests/sqlparser_mysql.rs | 3 +-- tests/sqlparser_postgres.rs | 18 ++++++------------ tests/sqlparser_sqlite.rs | 22 ++++++++-------------- 8 files changed, 57 insertions(+), 57 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index d59732183..bced575cc 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3178,6 +3178,26 @@ impl Spanned for RenameTableNameKind { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// Whether the syntax used for the trigger object (ROW or STATEMENT) is `FOR` or `FOR EACH`. +pub enum TriggerObjectKind { + /// The `FOR` syntax is used. + For(TriggerObject), + /// The `FOR EACH` syntax is used. + ForEach(TriggerObject), +} + +impl Display for TriggerObjectKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TriggerObjectKind::For(obj) => write!(f, "FOR {obj}"), + TriggerObjectKind::ForEach(obj) => write!(f, "FOR EACH {obj}"), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -3282,9 +3302,7 @@ pub struct CreateTrigger { /// every row affected by the trigger event, or just once per SQL statement. /// This is optional in some SQL dialects, such as SQLite, and if not specified, in /// those cases, the implied default is `FOR EACH ROW`. - pub trigger_object: Option, - /// Whether to include the `EACH` term of the `FOR EACH`, as it is optional syntax. - pub include_each: bool, + pub trigger_object: Option, /// Triggering conditions pub condition: Option, /// Execute logic block @@ -3313,7 +3331,6 @@ impl Display for CreateTrigger { referencing, trigger_object, condition, - include_each, exec_body, statements_as, statements, @@ -3355,11 +3372,7 @@ impl Display for CreateTrigger { } if let Some(trigger_object) = trigger_object { - if *include_each { - write!(f, " FOR EACH {trigger_object}")?; - } else if exec_body.is_some() { - write!(f, " FOR {trigger_object}")?; - } + write!(f, " {trigger_object}")?; } if let Some(condition) = condition { write!(f, " WHEN {condition}")?; diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8df636f8e..d3b5c1c7e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -69,8 +69,8 @@ pub use self::ddl::{ IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, Partition, ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, TableConstraint, - TagsColumnOption, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, - ViewColumnDef, + TagsColumnOption, TriggerObjectKind, UserDefinedTypeCompositeAttributeDef, + UserDefinedTypeRepresentation, ViewColumnDef, }; pub use self::dml::{Delete, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 6ee46c442..ede7b5c22 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -18,7 +18,7 @@ use crate::ast::helpers::attached_token::AttachedToken; use crate::ast::{ BeginEndStatements, ConditionalStatementBlock, ConditionalStatements, CreateTrigger, - GranteesType, IfStatement, Statement, TriggerObject, + GranteesType, IfStatement, Statement, }; use crate::dialect::Dialect; use crate::keywords::{self, Keyword}; @@ -264,8 +264,7 @@ impl MsSqlDialect { table_name, referenced_table_name: None, referencing: Vec::new(), - trigger_object: Some(TriggerObject::Statement), - include_each: false, + trigger_object: None, condition: None, exec_body: None, statements_as: true, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2a66ac5a0..9276a6524 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5609,23 +5609,26 @@ impl<'a> Parser<'a> { } } - let (include_each, trigger_object) = if self.parse_keyword(Keyword::FOR) { - ( - self.parse_keyword(Keyword::EACH), - Some( - match self.expect_one_of_keywords(&[Keyword::ROW, Keyword::STATEMENT])? { - Keyword::ROW => TriggerObject::Row, - Keyword::STATEMENT => TriggerObject::Statement, - _ => unreachable!(), - }, - ), - ) + let trigger_object = if self.parse_keyword(Keyword::FOR) { + let include_each = self.parse_keyword(Keyword::EACH); + let trigger_object = + match self.expect_one_of_keywords(&[Keyword::ROW, Keyword::STATEMENT])? { + Keyword::ROW => TriggerObject::Row, + Keyword::STATEMENT => TriggerObject::Statement, + _ => unreachable!(), + }; + + Some(if include_each { + TriggerObjectKind::ForEach(trigger_object) + } else { + TriggerObjectKind::For(trigger_object) + }) } else { if !dialect_of!(self is SQLiteDialect ) { self.expect_keyword_is(Keyword::FOR)?; } - (false, None) + None }; let condition = self @@ -5654,7 +5657,6 @@ impl<'a> Parser<'a> { referenced_table_name, referencing, trigger_object, - include_each, condition, exec_body, statements_as: false, diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 7ba1c3987..68f32465e 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2396,8 +2396,7 @@ fn parse_create_trigger() { table_name: ObjectName::from(vec![Ident::new("Sales"), Ident::new("Customer")]), referenced_table_name: None, referencing: vec![], - trigger_object: Some(TriggerObject::Statement), - include_each: false, + trigger_object: None, condition: None, exec_body: None, statements_as: true, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 219d314ef..53c75723f 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3934,8 +3934,7 @@ fn parse_create_trigger() { table_name: ObjectName::from(vec![Ident::new("emp")]), referenced_table_name: None, referencing: vec![], - trigger_object: Some(TriggerObject::Row), - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: None, exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 72eee1de8..1b618ea08 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5683,8 +5683,7 @@ fn parse_create_simple_before_insert_trigger() { table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], - trigger_object: Some(TriggerObject::Row), - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: None, exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, @@ -5716,8 +5715,7 @@ fn parse_create_after_update_trigger_with_condition() { table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], - trigger_object: Some(TriggerObject::Row), - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: Some(Expr::Nested(Box::new(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("NEW"), @@ -5756,8 +5754,7 @@ fn parse_create_instead_of_delete_trigger() { table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], - trigger_object: Some(TriggerObject::Row), - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: None, exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, @@ -5793,8 +5790,7 @@ fn parse_create_trigger_with_multiple_events_and_deferrable() { table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], - trigger_object: Some(TriggerObject::Row), - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: None, exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, @@ -5841,8 +5837,7 @@ fn parse_create_trigger_with_referencing() { transition_relation_name: ObjectName::from(vec![Ident::new("old_accounts")]), }, ], - trigger_object: Some(TriggerObject::Row), - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: None, exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, @@ -6147,8 +6142,7 @@ fn parse_trigger_related_functions() { table_name: ObjectName::from(vec![Ident::new("emp")]), referenced_table_name: None, referencing: vec![], - trigger_object: Some(TriggerObject::Row), - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: None, exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 9ea20945b..812e664cd 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -628,7 +628,6 @@ fn test_create_trigger() { referenced_table_name, referencing, trigger_object, - include_each, condition, exec_body: _, statements_as, @@ -646,8 +645,10 @@ fn test_create_trigger() { assert_eq!(table_name.to_string(), "assets"); assert!(referenced_table_name.is_none()); assert!(referencing.is_empty()); - assert_eq!(trigger_object, Some(TriggerObject::Row)); - assert!(include_each); + assert_eq!( + trigger_object, + Some(TriggerObjectKind::ForEach(TriggerObject::Row)) + ); assert!(condition.is_none()); assert!(!statements_as); assert!(characteristics.is_none()); @@ -673,7 +674,6 @@ fn test_create_trigger() { referenced_table_name, referencing, trigger_object, - include_each, condition, exec_body: _, statements_as, @@ -692,7 +692,6 @@ fn test_create_trigger() { assert!(referenced_table_name.is_none()); assert!(referencing.is_empty()); assert!(trigger_object.is_none()); - assert!(!include_each); assert!(condition.is_none()); assert!(!statements_as); assert!(characteristics.is_none()); @@ -715,7 +714,6 @@ fn test_create_trigger() { referenced_table_name, referencing, trigger_object, - include_each, condition, exec_body: _, statements_as, @@ -734,7 +732,6 @@ fn test_create_trigger() { assert!(referenced_table_name.is_none()); assert!(referencing.is_empty()); assert!(trigger_object.is_none()); - assert!(!include_each); assert!(condition.is_none()); assert!(!statements_as); assert!(characteristics.is_none()); @@ -757,7 +754,6 @@ fn test_create_trigger() { referenced_table_name, referencing, trigger_object, - include_each, condition, exec_body: _, statements_as, @@ -775,8 +771,10 @@ fn test_create_trigger() { assert_eq!(table_name.to_string(), "products"); assert!(referenced_table_name.is_none()); assert!(referencing.is_empty()); - assert_eq!(trigger_object, Some(TriggerObject::Row)); - assert!(include_each); + assert_eq!( + trigger_object, + Some(TriggerObjectKind::ForEach(TriggerObject::Row)) + ); assert!(condition.is_some()); assert!(!statements_as); assert!(characteristics.is_none()); @@ -800,7 +798,6 @@ fn test_create_trigger() { referenced_table_name, referencing, trigger_object, - include_each, condition, exec_body: _, statements_as, @@ -819,7 +816,6 @@ fn test_create_trigger() { assert!(referenced_table_name.is_none()); assert!(referencing.is_empty()); assert!(trigger_object.is_none()); - assert!(!include_each); assert!(condition.is_none()); assert!(!statements_as); assert!(characteristics.is_none()); @@ -843,7 +839,6 @@ fn test_create_trigger() { referenced_table_name, referencing, trigger_object, - include_each, condition, exec_body: _, statements_as, @@ -862,7 +857,6 @@ fn test_create_trigger() { assert!(referenced_table_name.is_none()); assert!(referencing.is_empty()); assert!(trigger_object.is_none()); - assert!(!include_each); assert!(condition.is_none()); assert!(!statements_as); assert!(characteristics.is_none()); From 87e1c66df12da98995c94e5bf48b82331d959fe2 Mon Sep 17 00:00:00 2001 From: Luca Date: Wed, 24 Sep 2025 11:56:56 +0200 Subject: [PATCH 05/10] Restored `period_before_table` in place of `period_specified_before_table` --- src/ast/ddl.rs | 10 +++++----- src/dialect/mssql.rs | 2 +- src/parser/mod.rs | 2 +- tests/sqlparser_mssql.rs | 2 +- tests/sqlparser_mysql.rs | 2 +- tests/sqlparser_postgres.rs | 12 ++++++------ tests/sqlparser_sqlite.rs | 24 ++++++++++++------------ 7 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index bced575cc..71e16bd7f 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3283,12 +3283,12 @@ pub struct CreateTrigger { /// but rather the position of the period clause in relation to the table name. /// /// ```sql - /// -- period_specified_before_table == true: Postgres, MySQL, and standard SQL + /// -- period_before_table == true: Postgres, MySQL, and standard SQL /// CREATE TRIGGER t BEFORE INSERT ON table_name ...; - /// -- period_specified_before_table == false: MSSQL + /// -- period_before_table == false: MSSQL /// CREATE TRIGGER t ON table_name BEFORE INSERT ...; /// ``` - pub period_specified_before_table: bool, + pub period_before_table: bool, /// Multiple events can be specified using OR, such as `INSERT`, `UPDATE`, `DELETE`, or `TRUNCATE`. pub events: Vec, /// The table on which the trigger is to be created. @@ -3323,7 +3323,7 @@ impl Display for CreateTrigger { or_replace, is_constraint, name, - period_specified_before_table, + period_before_table, period, events, table_name, @@ -3345,7 +3345,7 @@ impl Display for CreateTrigger { is_constraint = if *is_constraint { "CONSTRAINT " } else { "" }, )?; - if *period_specified_before_table { + if *period_before_table { write!(f, "{period}")?; if !events.is_empty() { write!(f, " {}", display_separated(events, " OR "))?; diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index ede7b5c22..f1d54cd67 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -259,7 +259,7 @@ impl MsSqlDialect { is_constraint: false, name, period, - period_specified_before_table: false, + period_before_table: false, events, table_name, referenced_table_name: None, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9276a6524..0d9918e0b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5651,7 +5651,7 @@ impl<'a> Parser<'a> { is_constraint, name, period, - period_specified_before_table: true, + period_before_table: true, events, table_name, referenced_table_name, diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 68f32465e..e9dc200ec 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2391,7 +2391,7 @@ fn parse_create_trigger() { is_constraint: false, name: ObjectName::from(vec![Ident::new("reminder1")]), period: TriggerPeriod::After, - period_specified_before_table: false, + period_before_table: false, events: vec![TriggerEvent::Insert, TriggerEvent::Update(vec![]),], table_name: ObjectName::from(vec![Ident::new("Sales"), Ident::new("Customer")]), referenced_table_name: None, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 53c75723f..360dc252e 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3929,7 +3929,7 @@ fn parse_create_trigger() { is_constraint: false, name: ObjectName::from(vec![Ident::new("emp_stamp")]), period: TriggerPeriod::Before, - period_specified_before_table: true, + period_before_table: true, events: vec![TriggerEvent::Insert], table_name: ObjectName::from(vec![Ident::new("emp")]), referenced_table_name: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 1b618ea08..9e960ceae 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5678,7 +5678,7 @@ fn parse_create_simple_before_insert_trigger() { is_constraint: false, name: ObjectName::from(vec![Ident::new("check_insert")]), period: TriggerPeriod::Before, - period_specified_before_table: true, + period_before_table: true, events: vec![TriggerEvent::Insert], table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, @@ -5710,7 +5710,7 @@ fn parse_create_after_update_trigger_with_condition() { is_constraint: false, name: ObjectName::from(vec![Ident::new("check_update")]), period: TriggerPeriod::After, - period_specified_before_table: true, + period_before_table: true, events: vec![TriggerEvent::Update(vec![])], table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, @@ -5749,7 +5749,7 @@ fn parse_create_instead_of_delete_trigger() { is_constraint: false, name: ObjectName::from(vec![Ident::new("check_delete")]), period: TriggerPeriod::InsteadOf, - period_specified_before_table: true, + period_before_table: true, events: vec![TriggerEvent::Delete], table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, @@ -5781,7 +5781,7 @@ fn parse_create_trigger_with_multiple_events_and_deferrable() { is_constraint: true, name: ObjectName::from(vec![Ident::new("check_multiple_events")]), period: TriggerPeriod::Before, - period_specified_before_table: true, + period_before_table: true, events: vec![ TriggerEvent::Insert, TriggerEvent::Update(vec![]), @@ -5821,7 +5821,7 @@ fn parse_create_trigger_with_referencing() { is_constraint: false, name: ObjectName::from(vec![Ident::new("check_referencing")]), period: TriggerPeriod::Before, - period_specified_before_table: true, + period_before_table: true, events: vec![TriggerEvent::Insert], table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, @@ -6137,7 +6137,7 @@ fn parse_trigger_related_functions() { is_constraint: false, name: ObjectName::from(vec![Ident::new("emp_stamp")]), period: TriggerPeriod::Before, - period_specified_before_table: true, + period_before_table: true, events: vec![TriggerEvent::Insert, TriggerEvent::Update(vec![])], table_name: ObjectName::from(vec![Ident::new("emp")]), referenced_table_name: None, diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 812e664cd..be43959fa 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -622,7 +622,7 @@ fn test_create_trigger() { is_constraint, name, period, - period_specified_before_table, + period_before_table, events, table_name, referenced_table_name, @@ -640,7 +640,7 @@ fn test_create_trigger() { assert!(!is_constraint); assert_eq!(name.to_string(), "trg_inherit_asset_models"); assert_eq!(period, TriggerPeriod::After); - assert!(period_specified_before_table); + assert!(period_before_table); assert_eq!(events, vec![TriggerEvent::Insert]); assert_eq!(table_name.to_string(), "assets"); assert!(referenced_table_name.is_none()); @@ -668,7 +668,7 @@ fn test_create_trigger() { is_constraint, name, period, - period_specified_before_table, + period_before_table, events, table_name, referenced_table_name, @@ -686,7 +686,7 @@ fn test_create_trigger() { assert!(!is_constraint); assert_eq!(name.to_string(), "log_new_user"); assert_eq!(period, TriggerPeriod::After); - assert!(period_specified_before_table); + assert!(period_before_table); assert_eq!(events, vec![TriggerEvent::Insert]); assert_eq!(table_name.to_string(), "users"); assert!(referenced_table_name.is_none()); @@ -708,7 +708,7 @@ fn test_create_trigger() { is_constraint, name, period, - period_specified_before_table, + period_before_table, events, table_name, referenced_table_name, @@ -726,7 +726,7 @@ fn test_create_trigger() { assert!(!is_constraint); assert_eq!(name.to_string(), "cleanup_orders"); assert_eq!(period, TriggerPeriod::After); - assert!(period_specified_before_table); + assert!(period_before_table); assert_eq!(events, vec![TriggerEvent::Delete]); assert_eq!(table_name.to_string(), "customers"); assert!(referenced_table_name.is_none()); @@ -748,7 +748,7 @@ fn test_create_trigger() { is_constraint, name, period, - period_specified_before_table, + period_before_table, events, table_name, referenced_table_name, @@ -766,7 +766,7 @@ fn test_create_trigger() { assert!(!is_constraint); assert_eq!(name.to_string(), "trg_before_update"); assert_eq!(period, TriggerPeriod::Before); - assert!(period_specified_before_table); + assert!(period_before_table); assert_eq!(events, vec![TriggerEvent::Update(Vec::new())]); assert_eq!(table_name.to_string(), "products"); assert!(referenced_table_name.is_none()); @@ -792,7 +792,7 @@ fn test_create_trigger() { is_constraint, name, period, - period_specified_before_table, + period_before_table, events, table_name, referenced_table_name, @@ -810,7 +810,7 @@ fn test_create_trigger() { assert!(!is_constraint); assert_eq!(name.to_string(), "trg_instead_of_insert"); assert_eq!(period, TriggerPeriod::InsteadOf); - assert!(period_specified_before_table); + assert!(period_before_table); assert_eq!(events, vec![TriggerEvent::Insert]); assert_eq!(table_name.to_string(), "my_view"); assert!(referenced_table_name.is_none()); @@ -833,7 +833,7 @@ fn test_create_trigger() { is_constraint, name, period, - period_specified_before_table, + period_before_table, events, table_name, referenced_table_name, @@ -851,7 +851,7 @@ fn test_create_trigger() { assert!(!is_constraint); assert_eq!(name.to_string(), "temp_trigger"); assert_eq!(period, TriggerPeriod::After); - assert!(period_specified_before_table); + assert!(period_before_table); assert_eq!(events, vec![TriggerEvent::Insert]); assert_eq!(table_name.to_string(), "temp_table"); assert!(referenced_table_name.is_none()); From a883cb96701a7a2071ebf2a27d2e2e013c8e8482 Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Wed, 24 Sep 2025 11:58:09 +0200 Subject: [PATCH 06/10] Update src/ast/ddl.rs Co-authored-by: Ifeanyi Ubah --- src/ast/ddl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 71e16bd7f..6652bc185 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3219,7 +3219,7 @@ pub struct CreateTrigger { /// /// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql?view=sql-server-ver16#arguments) pub or_alter: bool, - /// True if this is a temporary trigger, which is supported in SQLite. + /// True if this is a temporary trigger. /// /// The possible syntaxes are two: /// From 2163b34e020893505561293513afbe134d14b8cf Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Wed, 24 Sep 2025 11:58:25 +0200 Subject: [PATCH 07/10] Update src/ast/ddl.rs Co-authored-by: Ifeanyi Ubah --- src/ast/ddl.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 6652bc185..2b8fd9824 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3221,7 +3221,7 @@ pub struct CreateTrigger { pub or_alter: bool, /// True if this is a temporary trigger. /// - /// The possible syntaxes are two: + /// Examples: /// /// ```sql /// CREATE TEMP TRIGGER trigger_name @@ -3230,10 +3230,11 @@ pub struct CreateTrigger { /// or /// /// ```sql - /// CREATE TEMPORARY TRIGGER trigger_name + /// CREATE TEMPORARY TRIGGER trigger_name; + /// CREATE TEMP TRIGGER trigger_name; /// ``` /// - /// [Temporary Triggers in SQLite](https://sqlite.org/lang_createtrigger.html#temp_triggers_on_non_temp_tables) + /// [SQLite](https://sqlite.org/lang_createtrigger.html#temp_triggers_on_non_temp_tables) pub temporary: bool, /// The `OR REPLACE` clause is used to re-create the trigger if it already exists. /// From cb9ec89a0690beb6efb9b1ddddb117c2dc9066f7 Mon Sep 17 00:00:00 2001 From: Luca Date: Wed, 24 Sep 2025 12:03:05 +0200 Subject: [PATCH 08/10] Simplified dialect check in `parse_create_trigger` --- src/parser/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0d9918e0b..8a0249fcc 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5581,7 +5581,6 @@ impl<'a> Parser<'a> { is_constraint: bool, ) -> Result { if !dialect_of!(self is PostgreSqlDialect | SQLiteDialect | GenericDialect | MySqlDialect | MsSqlDialect) - || dialect_of!(self is SQLiteDialect) && (or_alter || or_replace || is_constraint) { self.prev_token(); return self.expected("an object type after CREATE", self.peek_token()); From c528fbcba8f52c60694d6a4adcdbc3bec0c783db Mon Sep 17 00:00:00 2001 From: Luca Date: Wed, 8 Oct 2025 13:27:44 +0200 Subject: [PATCH 09/10] Fixed merge error --- src/ast/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e77df31d2..916965cc8 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -68,9 +68,9 @@ pub use self::ddl::{ DropBehavior, DropTrigger, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, Partition, - ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, TableConstraint, - TagsColumnOption, TriggerObjectKind, UserDefinedTypeCompositeAttributeDef, - UserDefinedTypeRepresentation, ViewColumnDef, + ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, TagsColumnOption, + TriggerObjectKind, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, + ViewColumnDef, }; pub use self::dml::{Delete, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; From 7e033621e2ea10094dde8e6bc3d62091b6bf6376 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 10 Oct 2025 13:12:11 +0200 Subject: [PATCH 10/10] Refactored as requested in PR and updated fail test accordingly --- src/parser/mod.rs | 4 +--- tests/sqlparser_postgres.rs | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6fc3ac92b..0ffa968a1 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5625,9 +5625,7 @@ impl<'a> Parser<'a> { TriggerObjectKind::For(trigger_object) }) } else { - if !dialect_of!(self is SQLiteDialect ) { - self.expect_keyword_is(Keyword::FOR)?; - } + let _ = self.parse_keyword(Keyword::FOR); None }; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 5cddc6174..67c57ab8a 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5869,7 +5869,7 @@ fn parse_create_trigger_invalid_cases() { let invalid_cases = vec![ ( "CREATE TRIGGER check_update BEFORE UPDATE ON accounts FUNCTION check_account_update", - "Expected: FOR, found: FUNCTION" + "Expected: an SQL statement, found: FUNCTION" ), ( "CREATE TRIGGER check_update TOMORROW UPDATE ON accounts EXECUTE FUNCTION check_account_update",