diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index a0e69ad8a..c8109ba5a 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -593,6 +593,43 @@ impl fmt::Display for AlterPolicyOperation { } } +/// An `ALTER TRIGGER` (`Statement::AlterTrigger`) operation +/// +/// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-altertrigger.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterTriggerOperation { + /// `RENAME TO new_name` + Rename { + /// The new identifier for the trigger. + new_name: Ident, + }, + /// `[ NO ] DEPENDS ON EXTENSION extension_name` + DependsOnExtension { + /// `true` when `NO DEPENDS ON EXTENSION`. + no: bool, + /// Extension name. + extension_name: ObjectName, + }, +} + +impl fmt::Display for AlterTriggerOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterTriggerOperation::Rename { new_name } => { + write!(f, "RENAME TO {new_name}") + } + AlterTriggerOperation::DependsOnExtension { no, extension_name } => { + if *no { + write!(f, "NO ")?; + } + write!(f, "DEPENDS ON EXTENSION {extension_name}") + } + } + } +} + /// [MySQL] `ALTER TABLE` algorithm. /// /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/alter-table.html @@ -5994,3 +6031,40 @@ impl From for crate::ast::Statement { crate::ast::Statement::AlterPolicy(v) } } + +/// ALTER TRIGGER statement. +/// +/// ```sql +/// ALTER TRIGGER ON +/// ``` +/// (Postgresql-specific) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterTrigger { + /// Trigger name to alter. + pub name: Ident, + /// Target table name the trigger is defined on. + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub table_name: ObjectName, + /// Operation specific to the trigger alteration. + pub operation: AlterTriggerOperation, +} + +impl fmt::Display for AlterTrigger { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "ALTER TRIGGER {name} ON {table_name} {operation}", + name = self.name, + table_name = self.table_name, + operation = self.operation + ) + } +} + +impl From for crate::ast::Statement { + fn from(v: AlterTrigger) -> Self { + crate::ast::Statement::AlterTrigger(v) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8a9a67a74..2aec3600e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -68,11 +68,11 @@ pub use self::ddl::{ AlterOperatorFamily, AlterOperatorFamilyOperation, AlterOperatorOperation, AlterPolicy, AlterPolicyOperation, AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, AlterTableLock, AlterTableOperation, AlterTableType, AlterTextSearch, AlterTextSearchOperation, - AlterTextSearchOption, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, - AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef, - ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, - ConstraintCharacteristics, CreateCollation, CreateCollationDefinition, CreateConnector, - CreateDomain, CreateExtension, CreateFunction, CreateIndex, CreateOperator, + AlterTextSearchOption, AlterTrigger, AlterTriggerOperation, AlterType, AlterTypeAddValue, + AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, + ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, + ColumnPolicyProperty, ConstraintCharacteristics, CreateCollation, CreateCollationDefinition, + CreateConnector, CreateDomain, CreateExtension, CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreatePolicy, CreatePolicyCommand, CreatePolicyType, CreateTable, CreateTextSearch, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, DistStyle, DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, @@ -3881,6 +3881,11 @@ pub enum Statement { /// (Postgresql-specific) AlterPolicy(AlterPolicy), /// ```sql + /// ALTER TRIGGER ON
+ /// ``` + /// (Postgresql-specific) + AlterTrigger(AlterTrigger), + /// ```sql /// ALTER CONNECTOR connector_name SET DCPROPERTIES(property_name=property_value, ...); /// or /// ALTER CONNECTOR connector_name SET URL new_url; @@ -5673,6 +5678,7 @@ impl fmt::Display for Statement { write!(f, "ALTER ROLE {name} {operation}") } Statement::AlterPolicy(alter_policy) => write!(f, "{alter_policy}"), + Statement::AlterTrigger(alter_trigger) => write!(f, "{alter_trigger}"), Statement::AlterConnector { name, properties, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index a34fe66d9..2a0629799 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -487,6 +487,7 @@ impl Spanned for Statement { Statement::OptimizeTable { .. } => Span::empty(), Statement::CreatePolicy { .. } => Span::empty(), Statement::AlterPolicy { .. } => Span::empty(), + Statement::AlterTrigger { .. } => Span::empty(), Statement::AlterConnector { .. } => Span::empty(), Statement::DropPolicy { .. } => Span::empty(), Statement::DropConnector { .. } => Span::empty(), diff --git a/src/parser/alter.rs b/src/parser/alter.rs index 4000eb26b..047023b89 100644 --- a/src/parser/alter.rs +++ b/src/parser/alter.rs @@ -19,10 +19,11 @@ use super::{Parser, ParserError}; use crate::{ ast::{ helpers::key_value_options::{KeyValueOptions, KeyValueOptionsDelimiter}, - AlterConnectorOwner, AlterPolicy, AlterPolicyOperation, AlterRoleOperation, AlterUser, - AlterUserAddMfaMethodOtp, AlterUserAddRoleDelegation, AlterUserModifyMfaMethod, - AlterUserPassword, AlterUserRemoveRoleDelegation, AlterUserSetPolicy, Expr, MfaMethodKind, - Password, ResetConfig, RoleOption, SetConfigValue, Statement, UserPolicyKind, + AlterConnectorOwner, AlterPolicy, AlterPolicyOperation, AlterRoleOperation, AlterTrigger, + AlterTriggerOperation, AlterUser, AlterUserAddMfaMethodOtp, AlterUserAddRoleDelegation, + AlterUserModifyMfaMethod, AlterUserPassword, AlterUserRemoveRoleDelegation, + AlterUserSetPolicy, Expr, MfaMethodKind, Password, ResetConfig, RoleOption, SetConfigValue, + Statement, UserPolicyKind, }, dialect::{MsSqlDialect, PostgreSqlDialect}, keywords::Keyword, @@ -103,6 +104,50 @@ impl Parser<'_> { } } + /// Parse ALTER TRIGGER statement + /// ```sql + /// ALTER TRIGGER trigger_name ON table_name RENAME TO new_name + /// or + /// ALTER TRIGGER trigger_name ON table_name [ NO ] DEPENDS ON EXTENSION extension_name + /// ``` + /// + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-altertrigger.html) + pub fn parse_alter_trigger(&mut self) -> Result { + let name = self.parse_identifier()?; + self.expect_keyword_is(Keyword::ON)?; + let table_name = self.parse_object_name(false)?; + + let operation = if self.parse_keyword(Keyword::RENAME) { + self.expect_keyword_is(Keyword::TO)?; + AlterTriggerOperation::Rename { + new_name: self.parse_identifier()?, + } + } else { + let no = self.parse_keyword(Keyword::NO); + if !self.parse_keyword(Keyword::DEPENDS) { + return self.expected_ref( + if no { + "DEPENDS after NO" + } else { + "RENAME, DEPENDS or NO DEPENDS after ALTER TRIGGER" + }, + self.peek_token_ref(), + ); + } + self.expect_keywords(&[Keyword::ON, Keyword::EXTENSION])?; + AlterTriggerOperation::DependsOnExtension { + no, + extension_name: self.parse_object_name(false)?, + } + }; + + Ok(AlterTrigger { + name, + table_name, + operation, + }) + } + /// Parse an `ALTER CONNECTOR` statement /// ```sql /// ALTER CONNECTOR connector_name SET DCPROPERTIES(property_name=property_value, ...); diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b2b3f42bb..32f67988e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11154,6 +11154,7 @@ impl<'a> Parser<'a> { Keyword::AGGREGATE, Keyword::ROLE, Keyword::POLICY, + Keyword::TRIGGER, Keyword::CONNECTOR, Keyword::ICEBERG, Keyword::SCHEMA, @@ -11205,6 +11206,7 @@ impl<'a> Parser<'a> { } Keyword::ROLE => self.parse_alter_role(), Keyword::POLICY => self.parse_alter_policy().map(Into::into), + Keyword::TRIGGER => self.parse_alter_trigger().map(Into::into), Keyword::CONNECTOR => self.parse_alter_connector(), Keyword::USER if self.dialect.supports_alter_user_as_alter_role() => { self.parse_alter_role() @@ -11212,7 +11214,7 @@ impl<'a> Parser<'a> { Keyword::USER => self.parse_alter_user().map(Into::into), // unreachable because expect_one_of_keywords used above unexpected_keyword => Err(ParserError::ParserError( - format!("Internal parser error: expected any of {{TEXT SEARCH, VIEW, TYPE, COLLATION, TABLE, INDEX, FUNCTION, AGGREGATE, ROLE, POLICY, CONNECTOR, ICEBERG, SCHEMA, USER, OPERATOR}}, got {unexpected_keyword:?}"), + format!("Internal parser error: expected any of {{TEXT SEARCH, VIEW, TYPE, COLLATION, TABLE, INDEX, FUNCTION, AGGREGATE, ROLE, POLICY, TRIGGER, CONNECTOR, ICEBERG, SCHEMA, USER, OPERATOR}}, got {unexpected_keyword:?}"), )), } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 0800bc41f..6b4143258 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -14761,6 +14761,93 @@ fn test_alter_policy() { ); } +#[test] +fn test_alter_trigger() { + match verified_stmt("ALTER TRIGGER old_trigger ON my_table RENAME TO new_trigger") { + Statement::AlterTrigger(AlterTrigger { + name, + table_name, + operation, + }) => { + assert_eq!(name.to_string(), "old_trigger"); + assert_eq!(table_name.to_string(), "my_table"); + assert_eq!( + operation, + AlterTriggerOperation::Rename { + new_name: Ident::new("new_trigger") + } + ); + } + _ => unreachable!(), + } + + match verified_stmt("ALTER TRIGGER my_trigger ON my_table DEPENDS ON EXTENSION my_extension") { + Statement::AlterTrigger(AlterTrigger { operation, .. }) => { + assert_eq!( + operation, + AlterTriggerOperation::DependsOnExtension { + no: false, + extension_name: ObjectName::from(Ident::new("my_extension")) + } + ); + } + _ => unreachable!(), + } + + match verified_stmt("ALTER TRIGGER my_trigger ON my_table NO DEPENDS ON EXTENSION my_extension") + { + Statement::AlterTrigger(AlterTrigger { operation, .. }) => { + assert_eq!( + operation, + AlterTriggerOperation::DependsOnExtension { + no: true, + extension_name: ObjectName::from(Ident::new("my_extension")) + } + ); + } + _ => unreachable!(), + } + + // a qualified table name is preserved + verified_stmt("ALTER TRIGGER my_trigger ON my_schema.my_table RENAME TO new_trigger"); + + // an operation is required + assert_eq!( + parse_sql_statements("ALTER TRIGGER my_trigger ON my_table") + .unwrap_err() + .to_string(), + "sql parser error: Expected: RENAME, DEPENDS or NO DEPENDS after ALTER TRIGGER, found: EOF" + ); + // missing TO in RENAME TO + assert_eq!( + parse_sql_statements("ALTER TRIGGER my_trigger ON my_table RENAME") + .unwrap_err() + .to_string(), + "sql parser error: Expected: TO, found: EOF" + ); + // missing new name in RENAME TO + assert_eq!( + parse_sql_statements("ALTER TRIGGER my_trigger ON my_table RENAME TO") + .unwrap_err() + .to_string(), + "sql parser error: Expected: identifier, found: EOF" + ); + // NO must be followed by DEPENDS + assert_eq!( + parse_sql_statements("ALTER TRIGGER my_trigger ON my_table NO EXTENSION my_extension") + .unwrap_err() + .to_string(), + "sql parser error: Expected: DEPENDS after NO, found: EXTENSION" + ); + // missing the extension name + assert_eq!( + parse_sql_statements("ALTER TRIGGER my_trigger ON my_table DEPENDS ON EXTENSION") + .unwrap_err() + .to_string(), + "sql parser error: Expected: identifier, found: EOF" + ); +} + #[test] fn test_create_connector() { let sql = "CREATE CONNECTOR my_connector \