From 38c1ad3011e9636eddda849d0107a3b859b9a0d2 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 2 Oct 2026 18:15:02 +0200 Subject: [PATCH 1/4] PostgreSQL: parse `ALTER TRIGGER RENAME TO` and `DEPENDS ON EXTENSION` --- src/ast/ddl.rs | 74 +++++++++++++++++++++++++++++++++ src/ast/mod.rs | 39 +++++++++++------- src/ast/spans.rs | 1 + src/parser/alter.rs | 53 ++++++++++++++++++++++-- src/parser/mod.rs | 4 +- tests/sqlparser_common.rs | 87 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 237 insertions(+), 21 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index e45c5f3cb7..ca23b230fa 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -601,6 +601,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 @@ -6041,6 +6078,43 @@ impl From for crate::ast::Statement { } } +/// 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) + } +} + /// A `CREATE FOREIGN TABLE` statement. /// /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createforeigntable.html) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8e798524c9..817a7fca1b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -68,22 +68,23 @@ pub use self::ddl::{ AlterOperatorClassOperation, 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, CreateAggregate, CreateAggregateArgs, - CreateAggregateOption, CreateCollation, CreateCollationDefinition, CreateConnector, - CreateDomain, CreateExtension, CreateForeignTable, CreateFunction, CreateIndex, CreateOperator, - CreateOperatorClass, CreateOperatorFamily, CreatePolicy, CreatePolicyCommand, CreatePolicyType, - CreateTable, CreateTextSearch, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, - DistStyle, DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, - DropOperatorFamily, DropOperatorSignature, DropPolicy, DropTrigger, ForValues, - FunctionReturnType, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, - IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, - IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, - OperatorClassItem, OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose, - Owner, Partition, PartitionBoundValue, ProcedureParam, ReferentialAction, RenameTableNameKind, - ReplicaIdentity, SqliteTableOption, SqliteTableOptions, TagsColumnOption, TextSearchObjectType, + AlterTextSearch, AlterTextSearchOperation, AlterTextSearchOption, AlterTrigger, + AlterTriggerOperation, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, + AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef, + ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, + ConstraintCharacteristics, CreateAggregate, CreateAggregateArgs, CreateAggregateOption, + CreateCollation, CreateCollationDefinition, CreateConnector, CreateDomain, CreateExtension, + CreateForeignTable, CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass, + CreateOperatorFamily, CreatePolicy, CreatePolicyCommand, CreatePolicyType, CreateTable, + CreateTextSearch, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, DistStyle, + DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily, + DropOperatorSignature, DropPolicy, DropTrigger, ForValues, FunctionReturnType, GeneratedAs, + GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, + IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, + KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, OperatorClassItem, + OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose, Owner, Partition, + PartitionBoundValue, ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, + SqliteTableOption, SqliteTableOptions, TagsColumnOption, TextSearchObjectType, TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, WithData, @@ -3936,6 +3937,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; @@ -5730,6 +5736,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 7638d7d6c0..1e46da6fde 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -488,6 +488,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 4000eb26ba..047023b893 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 3efb22a349..95c014e354 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11651,6 +11651,7 @@ impl<'a> Parser<'a> { Keyword::AGGREGATE, Keyword::ROLE, Keyword::POLICY, + Keyword::TRIGGER, Keyword::CONNECTOR, Keyword::ICEBERG, Keyword::SCHEMA, @@ -11702,6 +11703,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() @@ -11709,7 +11711,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 f71869e820..9cf700ad8b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15521,6 +15521,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 \ From d3cb06098ec3d796e3c47cc3364391c95673c0f9 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 20 Oct 2026 13:13:28 +0200 Subject: [PATCH 2/4] Track trigger alteration spans and cover parser boundaries --- src/ast/ddl.rs | 2 ++ src/ast/spans.rs | 59 +++++++++++++++++++----------- src/parser/mod.rs | 34 +++++++----------- tests/sqlparser_common.rs | 75 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 41 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index ca23b230fa..56eec4fbd7 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -6080,6 +6080,8 @@ impl From for crate::ast::Statement { /// ALTER TRIGGER statement. /// +/// Its source span covers `name`, `table_name` and `operation`. +/// /// ```sql /// ALTER TRIGGER ON
/// ``` diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 1e46da6fde..cb27dfd0a9 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -29,25 +29,26 @@ use crate::tokenizer::Span; use super::{ comments, dcl::SecondaryRoles, value::ValueWithSpan, AccessExpr, AlterColumnOperation, - AlterIndexOperation, AlterTableOperation, Analyze, Array, Assignment, AssignmentTarget, - AttachedToken, BeginEndStatements, CaseStatement, CloseCursor, ClusteredIndex, ColumnDef, - ColumnOption, ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, - ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, - CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeConstraintElement, - ExcludeSelectItem, Expr, ExprWithAlias, Fetch, ForValues, FromTable, Function, FunctionArg, - FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, - HavingBound, IfStatement, IlikeSelectItem, InTarget, IndexColumn, Insert, Interpolate, - InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, - LimitClause, MatchRecognizePattern, Measure, Merge, MergeAction, MergeClause, MergeInsertExpr, - MergeInsertKind, MergeUpdateExpr, MergeUpdateKind, NamedParenthesizedList, - NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, - OnInsert, OpenStatement, OperateFunctionArg, OrderBy, OrderByExpr, OrderByKind, OutputClause, - Parens, Partition, PartitionBoundValue, PivotValueSource, ProjectionSelect, Query, - RaiseStatement, RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, - ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, Setting, SqlOption, Statement, - Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, - TableObject, TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, Values, - ViewColumnDef, WhileStatement, WildcardAdditionalOptions, With, WithFill, + AlterIndexOperation, AlterTableOperation, AlterTrigger, AlterTriggerOperation, Analyze, Array, + Assignment, AssignmentTarget, AttachedToken, BeginEndStatements, CaseStatement, CloseCursor, + ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, ConditionalStatementBlock, + ConditionalStatements, ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, + CreateIndex, CreateTable, CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, + ExcludeConstraintElement, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, ForValues, FromTable, + Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, + FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, InTarget, + IndexColumn, Insert, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, + JsonPath, JsonPathElem, LateralView, LimitClause, MatchRecognizePattern, Measure, Merge, + MergeAction, MergeClause, MergeInsertExpr, MergeInsertKind, MergeUpdateExpr, MergeUpdateKind, + NamedParenthesizedList, NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, + OnConflictAction, OnInsert, OpenStatement, OperateFunctionArg, OrderBy, OrderByExpr, + OrderByKind, OutputClause, Parens, Partition, PartitionBoundValue, PivotValueSource, + ProjectionSelect, Query, RaiseStatement, RaiseStatementValue, ReferentialAction, + RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, + SetExpr, Setting, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, + TableAliasColumnDef, TableConstraint, TableFactor, TableObject, TableOptionsClustered, + TableWithJoins, Update, UpdateTableFromKind, Use, Values, ViewColumnDef, WhileStatement, + WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -488,7 +489,7 @@ impl Spanned for Statement { Statement::OptimizeTable { .. } => Span::empty(), Statement::CreatePolicy { .. } => Span::empty(), Statement::AlterPolicy { .. } => Span::empty(), - Statement::AlterTrigger { .. } => Span::empty(), + Statement::AlterTrigger(trigger) => trigger.span(), Statement::AlterConnector { .. } => Span::empty(), Statement::DropPolicy { .. } => Span::empty(), Statement::DropConnector { .. } => Span::empty(), @@ -530,6 +531,24 @@ impl Spanned for Statement { } } +impl Spanned for AlterTrigger { + fn span(&self) -> Span { + self.name + .span + .union(&self.table_name.span()) + .union(&self.operation.span()) + } +} + +impl Spanned for AlterTriggerOperation { + fn span(&self) -> Span { + match self { + Self::Rename { new_name } => new_name.span, + Self::DependsOnExtension { extension_name, .. } => extension_name.span(), + } + } +} + impl Spanned for Use { fn span(&self) -> Span { match self { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 95c014e354..308eebfac4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11641,23 +11641,12 @@ impl<'a> Parser<'a> { return self.parse_alter_text_search().map(Into::into); } - let object_type = self.expect_one_of_keywords(&[ - Keyword::VIEW, - Keyword::TYPE, - Keyword::COLLATION, - Keyword::TABLE, - Keyword::INDEX, - Keyword::FUNCTION, - Keyword::AGGREGATE, - Keyword::ROLE, - Keyword::POLICY, - Keyword::TRIGGER, - Keyword::CONNECTOR, - Keyword::ICEBERG, - Keyword::SCHEMA, - Keyword::USER, - Keyword::OPERATOR, - ])?; + let index = self.index; + self.advance_token(); + let object_type = match &self.get_current_token().token { + Token::Word(word) => word.keyword, + _ => Keyword::NoKeyword, + }; match object_type { Keyword::SCHEMA => { self.prev_token(); @@ -11709,10 +11698,13 @@ impl<'a> Parser<'a> { self.parse_alter_role() } 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, TRIGGER, CONNECTOR, ICEBERG, SCHEMA, USER, OPERATOR}}, got {unexpected_keyword:?}"), - )), + _ => { + self.index = index; + self.expected_ref( + "one of VIEW or TYPE or COLLATION or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or SCHEMA or USER or OPERATOR", + self.peek_token_ref(), + ) + } } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 9cf700ad8b..4b2530b6f6 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15608,6 +15608,66 @@ fn test_alter_trigger() { ); } +#[test] +fn parse_alter_trigger_rejects_malformed_statements() { + for (sql, expected) in [ + ("ALTER TRIGGER", "Expected: identifier, found: EOF"), + ("ALTER TRIGGER t", "Expected: ON, found: EOF"), + ("ALTER TRIGGER t ON", "Expected: identifier, found: EOF"), + ("ALTER TRIGGER t ON tbl DEPENDS", "Expected: ON, found: EOF"), + ] { + assert_eq!( + parse_sql_statements(sql).unwrap_err().to_string(), + format!("sql parser error: {expected}"), + ); + } +} + +#[test] +fn display_alter_trigger_propagates_write_errors() { + use core::fmt::{self, Write}; + + struct RejectingWriter<'a>(&'a str); + + impl Write for RejectingWriter<'_> { + fn write_str(&mut self, value: &str) -> fmt::Result { + if value.contains(self.0) { + Err(fmt::Error) + } else { + Ok(()) + } + } + } + + let statement = verified_stmt("ALTER TRIGGER t ON tbl NO DEPENDS ON EXTENSION ext"); + let mut writer = RejectingWriter("NO "); + assert_eq!(write!(writer, "{statement}"), Err(fmt::Error)); +} + +#[test] +fn parse_alter_trigger_spans() { + let dialects = all_dialects(); + for (sql, end) in [ + ( + "ALTER TRIGGER trg ON t RENAME TO trg2", + Location::new(1, 38), + ), + ( + "ALTER TRIGGER trg ON t DEPENDS ON EXTENSION ext", + Location::new(1, 48), + ), + ] { + for dialect in &dialects.dialects { + let statements = Parser::parse_sql(dialect.as_ref(), sql).unwrap(); + assert_eq!( + statements[0].span(), + Span::new(Location::new(1, 15), end), + "{dialect:?} {sql}" + ); + } + } +} + #[test] fn test_create_connector() { let sql = "CREATE CONNECTOR my_connector \ @@ -21616,3 +21676,18 @@ fn parse_qualified_literal_prefix_requires_adjacent_period() { dialects.tokenizes_to(sql, vec![token]); } } + +#[test] +fn parse_alter_rejects_missing_or_unknown_object_type() { + let objects = "one of VIEW or TYPE or COLLATION or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or SCHEMA or USER or OPERATOR"; + for (sql, found) in [ + ("ALTER", "EOF"), + ("ALTER 1", "1"), + ("ALTER unknown", "unknown"), + ] { + assert_eq!( + all_dialects().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(format!("Expected: {objects}, found: {found}")), + ); + } +} From 4bcf80ba77018f98f72e35bd97ffedfc5d3f0cea Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 00:30:24 +0200 Subject: [PATCH 3/4] Place regression tests beside related syntax cases --- tests/sqlparser_common.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 4b2530b6f6..547f145230 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15668,6 +15668,21 @@ fn parse_alter_trigger_spans() { } } +#[test] +fn parse_alter_rejects_missing_or_unknown_object_type() { + let objects = "one of VIEW or TYPE or COLLATION or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or SCHEMA or USER or OPERATOR"; + for (sql, found) in [ + ("ALTER", "EOF"), + ("ALTER 1", "1"), + ("ALTER unknown", "unknown"), + ] { + assert_eq!( + all_dialects().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(format!("Expected: {objects}, found: {found}")), + ); + } +} + #[test] fn test_create_connector() { let sql = "CREATE CONNECTOR my_connector \ @@ -21676,18 +21691,3 @@ fn parse_qualified_literal_prefix_requires_adjacent_period() { dialects.tokenizes_to(sql, vec![token]); } } - -#[test] -fn parse_alter_rejects_missing_or_unknown_object_type() { - let objects = "one of VIEW or TYPE or COLLATION or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or SCHEMA or USER or OPERATOR"; - for (sql, found) in [ - ("ALTER", "EOF"), - ("ALTER 1", "1"), - ("ALTER unknown", "unknown"), - ] { - assert_eq!( - all_dialects().parse_sql_statements(sql).unwrap_err(), - ParserError::ParserError(format!("Expected: {objects}, found: {found}")), - ); - } -} From 9274f81f2de95685deb10755b7c586ce038a315f Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 13:43:21 +0200 Subject: [PATCH 4/4] Add fuzz seeds for pg alter trigger --- fuzz/fuzz_seeds/postgres_2430_alter_trigger.sql | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 fuzz/fuzz_seeds/postgres_2430_alter_trigger.sql diff --git a/fuzz/fuzz_seeds/postgres_2430_alter_trigger.sql b/fuzz/fuzz_seeds/postgres_2430_alter_trigger.sql new file mode 100644 index 0000000000..6cc175f4c4 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_2430_alter_trigger.sql @@ -0,0 +1,4 @@ +ALTER TRIGGER old_trigger ON my_table RENAME TO new_trigger; +ALTER TRIGGER my_trigger ON my_table DEPENDS ON EXTENSION my_extension; +ALTER TRIGGER my_trigger ON my_table NO DEPENDS ON EXTENSION my_extension; +ALTER TRIGGER my_trigger ON my_schema.my_table RENAME TO new_trigger