From 320ed5e9c99e37ca0a33a9a3d7cb39d01d3dc5b9 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 2 Oct 2026 18:14:57 +0200 Subject: [PATCH 1/5] Add support for PostgreSQL `DROP RULE` --- src/ast/ddl.rs | 44 +++++++++++++++++++++++++++++++++++++ src/ast/mod.rs | 22 ++++++++++++------- src/ast/spans.rs | 1 + src/parser/mod.rs | 24 +++++++++++++++++++- tests/sqlparser_common.rs | 31 ++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 4 ++-- 6 files changed, 115 insertions(+), 11 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index e2cc004032..634fcb76f5 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -6447,6 +6447,44 @@ impl From for crate::ast::Statement { } } +/// DROP RULE statement. +/// +/// ```sql +/// DROP RULE [ IF EXISTS ] name ON table_name [ CASCADE | RESTRICT ] +/// ``` +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-droprule.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct DropRule { + /// `true` when `IF EXISTS` was present. + pub if_exists: bool, + /// Name of the rule to drop. + pub name: Ident, + /// Name of the table the rule applies to. + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub table_name: ObjectName, + /// Optional drop behavior (`CASCADE` or `RESTRICT`). + pub drop_behavior: Option, +} + +impl fmt::Display for DropRule { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "DROP RULE {if_exists}{name} ON {table_name}", + if_exists = if self.if_exists { "IF EXISTS " } else { "" }, + name = self.name, + table_name = self.table_name + )?; + if let Some(behavior) = &self.drop_behavior { + write!(f, " {behavior}")?; + } + Ok(()) + } +} + /// A `CREATE FOREIGN TABLE` statement. /// /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createforeigntable.html) @@ -6558,6 +6596,12 @@ impl fmt::Display for CreateForeignDataWrapper { } } +impl From for crate::ast::Statement { + fn from(v: DropRule) -> Self { + crate::ast::Statement::DropRule(v) + } +} + impl From for crate::ast::Statement { fn from(v: CreateForeignTable) -> Self { crate::ast::Statement::CreateForeignTable(v) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index f62f09907d..047ec8ee50 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -81,14 +81,14 @@ pub use self::ddl::{ CreateRuleEvent, CreateTable, CreateTextSearch, CreateTrigger, CreateUserMapping, CreateView, Deduplicate, DeferrableInitial, DistStyle, DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily, DropOperatorSignature, DropPolicy, - DropTrigger, ForValues, ForeignDataWrapperRoutineClause, 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, + DropRule, DropTrigger, ForValues, ForeignDataWrapperRoutineClause, 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, UserMappingUser, ViewColumnDef, WithData, @@ -4107,6 +4107,11 @@ pub enum Statement { /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-droppolicy.html) DropPolicy(DropPolicy), /// ```sql + /// DROP RULE + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-droprule.html) + DropRule(DropRule), + /// ```sql /// DROP CONNECTOR /// ``` /// See [Hive](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27362034#LanguageManualDDL-DropConnector) @@ -5891,6 +5896,7 @@ impl fmt::Display for Statement { Ok(()) } Statement::DropPolicy(policy) => write!(f, "{policy}"), + Statement::DropRule(rule) => write!(f, "{rule}"), Statement::DropConnector { if_exists, name } => { write!( f, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 8b008204c1..e51456f5cf 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -491,6 +491,7 @@ impl Spanned for Statement { Statement::AlterTrigger(trigger) => trigger.span(), Statement::AlterConnector { .. } => Span::empty(), Statement::DropPolicy { .. } => Span::empty(), + Statement::DropRule { .. } => Span::empty(), Statement::DropConnector { .. } => Span::empty(), Statement::ShowCatalogs { .. } => Span::empty(), Statement::ShowDatabases { .. } => Span::empty(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4e006e26ff..11accc3996 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8179,6 +8179,8 @@ impl<'a> Parser<'a> { return self.parse_drop_function().map(Into::into); } else if self.parse_keyword(Keyword::POLICY) { return self.parse_drop_policy().map(Into::into); + } else if self.parse_keyword(Keyword::RULE) { + return self.parse_drop_rule().map(Into::into); } else if self.parse_keyword(Keyword::CONNECTOR) { return self.parse_drop_connector(); } else if self.parse_keyword(Keyword::DOMAIN) { @@ -8202,7 +8204,7 @@ impl<'a> Parser<'a> { }; } else { return self.expected_ref( - "COLLATION, CONNECTOR, DATABASE, EXTENSION, FOREIGN TABLE, FUNCTION, GROUP, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP", + "COLLATION, CONNECTOR, DATABASE, EXTENSION, FOREIGN TABLE, FUNCTION, GROUP, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, RULE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP", self.peek_token_ref(), ); }; @@ -8290,6 +8292,26 @@ impl<'a> Parser<'a> { drop_behavior, }) } + + /// ```sql + /// DROP RULE [ IF EXISTS ] name ON table_name [ CASCADE | RESTRICT ] + /// ``` + /// + /// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-droprule.html) + fn parse_drop_rule(&mut self) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = self.parse_identifier()?; + self.expect_keyword_is(Keyword::ON)?; + let table_name = self.parse_object_name(false)?; + let drop_behavior = self.parse_optional_drop_behavior(); + Ok(DropRule { + if_exists, + name, + table_name, + drop_behavior, + }) + } + /// ```sql /// DROP CONNECTOR [IF EXISTS] name /// ``` diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7a09f95979..cdbd6fd293 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -21735,6 +21735,37 @@ fn display_create_rule_propagates_write_errors() { } } +#[test] +fn parse_drop_rule() { + let sql = "DROP RULE r ON t"; + match all_dialects().verified_stmt(sql) { + Statement::DropRule(DropRule { + if_exists, + name, + table_name, + drop_behavior, + }) => { + assert!(!if_exists); + assert_eq!(name.to_string(), "r"); + assert_eq!(table_name.to_string(), "t"); + assert_eq!(drop_behavior, None); + } + _ => unreachable!("Expected DROP RULE"), + } + + all_dialects().verified_stmt("DROP RULE IF EXISTS r ON s1.t CASCADE"); + all_dialects().verified_stmt("DROP RULE r ON t RESTRICT"); + + // the table name is mandatory + assert_eq!( + all_dialects() + .parse_sql_statements("DROP RULE r") + .unwrap_err() + .to_string(), + "sql parser error: Expected: ON, found: EOF" + ); +} + #[test] fn parse_alter_table_column_position() { let dialects = all_dialects_where(|d| d.supports_alter_column_position()); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 9f85cd011e..be2c7e9563 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11389,7 +11389,7 @@ fn parse_drop_foreign_table() { .parse_sql_statements("DROP FOREIGN ft") .unwrap_err() .to_string(), - "sql parser error: Expected: COLLATION, CONNECTOR, DATABASE, EXTENSION, FOREIGN TABLE, FUNCTION, GROUP, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP, found: FOREIGN" + "sql parser error: Expected: COLLATION, CONNECTOR, DATABASE, EXTENSION, FOREIGN TABLE, FUNCTION, GROUP, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, RULE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP, found: FOREIGN" ); for (sql, expected) in [ ("DROP FOREIGN TABLE", "Expected: identifier, found: EOF"), @@ -11755,7 +11755,7 @@ fn parse_drop_group_is_dialect_gated() { all_dialects_where(|d| !d.supports_drop_group()) .parse_sql_statements("DROP GROUP staff"), Err(ParserError::ParserError( - "Expected: COLLATION, CONNECTOR, DATABASE, EXTENSION, FOREIGN TABLE, FUNCTION, GROUP, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP, found: GROUP".to_string() + "Expected: COLLATION, CONNECTOR, DATABASE, EXTENSION, FOREIGN TABLE, FUNCTION, GROUP, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, RULE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP, found: GROUP".to_string() )) ); } From 3fa6b3bcb6250463692e61a90f02ec11fc234e56 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 20 Oct 2026 13:13:29 +0200 Subject: [PATCH 2/5] Track dropped-rule spans and cover formatter boundaries --- src/ast/ddl.rs | 2 ++ src/ast/mod.rs | 8 +++--- src/ast/spans.rs | 12 ++++++--- tests/sqlparser_common.rs | 53 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 634fcb76f5..59e38e3b15 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -6449,6 +6449,8 @@ impl From for crate::ast::Statement { /// DROP RULE statement. /// +/// Its source span covers `name` and `table_name`. +/// /// ```sql /// DROP RULE [ IF EXISTS ] name ON table_name [ CASCADE | RESTRICT ] /// ``` diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 047ec8ee50..0b0893d7e4 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -85,10 +85,10 @@ pub use self::ddl::{ 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, + OperatorClassItem, OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose, + Owner, Partition, PartitionBoundValue, ProcedureParam, ReferentialAction, RenameTableNameKind, + ReplicaIdentity, SqliteTableOption, SqliteTableOptions, TagsColumnOption, TextSearchObjectType, + TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, UserMappingUser, ViewColumnDef, WithData, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index e51456f5cf..d3aa28d032 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -35,8 +35,8 @@ use super::{ ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, CreateIndex, CreateRule, CreateRuleAction, CreateTable, CreateTableOptions, CreateUserMapping, Cte, Delete, DoUpdate, - ExceptSelectItem, ExcludeConstraintElement, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, - ForValues, FromTable, Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, + DropRule, 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, @@ -491,7 +491,7 @@ impl Spanned for Statement { Statement::AlterTrigger(trigger) => trigger.span(), Statement::AlterConnector { .. } => Span::empty(), Statement::DropPolicy { .. } => Span::empty(), - Statement::DropRule { .. } => Span::empty(), + Statement::DropRule(rule) => rule.span(), Statement::DropConnector { .. } => Span::empty(), Statement::ShowCatalogs { .. } => Span::empty(), Statement::ShowDatabases { .. } => Span::empty(), @@ -550,6 +550,12 @@ impl Spanned for AlterTriggerOperation { } } +impl Spanned for DropRule { + fn span(&self) -> Span { + self.name.span.union(&self.table_name.span()) + } +} + impl Spanned for Use { fn span(&self) -> Span { match self { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index cdbd6fd293..a44ed80ec3 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -21766,6 +21766,59 @@ fn parse_drop_rule() { ); } +#[test] +fn parse_drop_rule_rejects_missing_names() { + for (sql, expected) in [ + ("DROP RULE", "Expected: identifier, found: EOF"), + ("DROP RULE r ON", "Expected: identifier, found: EOF"), + ("DROP 1", "Expected: COLLATION, CONNECTOR, DATABASE, EXTENSION, FUNCTION, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, RULE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP, found: 1"), + ] { + assert_eq!( + all_dialects().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(expected.to_string()), + ); + } +} + +#[test] +fn display_drop_rule_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 = all_dialects().verified_stmt("DROP RULE r ON t"); + let mut writer = RejectingWriter("DROP"); + assert_eq!(write!(writer, "{statement}"), Err(fmt::Error)); + + let statement = all_dialects().verified_stmt("DROP RULE r ON t CASCADE"); + let mut writer = RejectingWriter("CASCADE"); + assert_eq!(write!(writer, "{statement}"), Err(fmt::Error)); +} + +#[test] +fn parse_drop_rule_spans() { + let dialects = all_dialects(); + let sql = "DROP RULE r ON t CASCADE"; + 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, 11), Location::new(1, 17)), + "{dialect:?} {sql}" + ); + } +} + #[test] fn parse_alter_table_column_position() { let dialects = all_dialects_where(|d| d.supports_alter_column_position()); From a042456b9fd4ab493345f263d5cf4a68c98af375 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 00:31:21 +0200 Subject: [PATCH 3/5] Place regression tests beside related syntax cases --- tests/sqlparser_common.rs | 168 +++++++++++++++++++------------------- 1 file changed, 84 insertions(+), 84 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a44ed80ec3..e72e013f0a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15840,6 +15840,90 @@ fn test_drop_policy() { ); } +#[test] +fn parse_drop_rule() { + let sql = "DROP RULE r ON t"; + match all_dialects().verified_stmt(sql) { + Statement::DropRule(DropRule { + if_exists, + name, + table_name, + drop_behavior, + }) => { + assert!(!if_exists); + assert_eq!(name.to_string(), "r"); + assert_eq!(table_name.to_string(), "t"); + assert_eq!(drop_behavior, None); + } + _ => unreachable!("Expected DROP RULE"), + } + + all_dialects().verified_stmt("DROP RULE IF EXISTS r ON s1.t CASCADE"); + all_dialects().verified_stmt("DROP RULE r ON t RESTRICT"); + + // the table name is mandatory + assert_eq!( + all_dialects() + .parse_sql_statements("DROP RULE r") + .unwrap_err() + .to_string(), + "sql parser error: Expected: ON, found: EOF" + ); +} + +#[test] +fn parse_drop_rule_rejects_missing_names() { + for (sql, expected) in [ + ("DROP RULE", "Expected: identifier, found: EOF"), + ("DROP RULE r ON", "Expected: identifier, found: EOF"), + ("DROP 1", "Expected: COLLATION, CONNECTOR, DATABASE, EXTENSION, FOREIGN TABLE, FUNCTION, GROUP, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, RULE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP, found: 1"), + ] { + assert_eq!( + all_dialects().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(expected.to_string()), + ); + } +} + +#[test] +fn display_drop_rule_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 = all_dialects().verified_stmt("DROP RULE r ON t"); + let mut writer = RejectingWriter("DROP"); + assert_eq!(write!(writer, "{statement}"), Err(fmt::Error)); + + let statement = all_dialects().verified_stmt("DROP RULE r ON t CASCADE"); + let mut writer = RejectingWriter("CASCADE"); + assert_eq!(write!(writer, "{statement}"), Err(fmt::Error)); +} + +#[test] +fn parse_drop_rule_spans() { + let dialects = all_dialects(); + let sql = "DROP RULE r ON t CASCADE"; + 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, 11), Location::new(1, 17)), + "{dialect:?} {sql}" + ); + } +} + #[test] fn test_alter_policy() { match verified_stmt("ALTER POLICY old_policy ON my_table RENAME TO new_policy") { @@ -21735,90 +21819,6 @@ fn display_create_rule_propagates_write_errors() { } } -#[test] -fn parse_drop_rule() { - let sql = "DROP RULE r ON t"; - match all_dialects().verified_stmt(sql) { - Statement::DropRule(DropRule { - if_exists, - name, - table_name, - drop_behavior, - }) => { - assert!(!if_exists); - assert_eq!(name.to_string(), "r"); - assert_eq!(table_name.to_string(), "t"); - assert_eq!(drop_behavior, None); - } - _ => unreachable!("Expected DROP RULE"), - } - - all_dialects().verified_stmt("DROP RULE IF EXISTS r ON s1.t CASCADE"); - all_dialects().verified_stmt("DROP RULE r ON t RESTRICT"); - - // the table name is mandatory - assert_eq!( - all_dialects() - .parse_sql_statements("DROP RULE r") - .unwrap_err() - .to_string(), - "sql parser error: Expected: ON, found: EOF" - ); -} - -#[test] -fn parse_drop_rule_rejects_missing_names() { - for (sql, expected) in [ - ("DROP RULE", "Expected: identifier, found: EOF"), - ("DROP RULE r ON", "Expected: identifier, found: EOF"), - ("DROP 1", "Expected: COLLATION, CONNECTOR, DATABASE, EXTENSION, FUNCTION, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, RULE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP, found: 1"), - ] { - assert_eq!( - all_dialects().parse_sql_statements(sql).unwrap_err(), - ParserError::ParserError(expected.to_string()), - ); - } -} - -#[test] -fn display_drop_rule_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 = all_dialects().verified_stmt("DROP RULE r ON t"); - let mut writer = RejectingWriter("DROP"); - assert_eq!(write!(writer, "{statement}"), Err(fmt::Error)); - - let statement = all_dialects().verified_stmt("DROP RULE r ON t CASCADE"); - let mut writer = RejectingWriter("CASCADE"); - assert_eq!(write!(writer, "{statement}"), Err(fmt::Error)); -} - -#[test] -fn parse_drop_rule_spans() { - let dialects = all_dialects(); - let sql = "DROP RULE r ON t CASCADE"; - 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, 11), Location::new(1, 17)), - "{dialect:?} {sql}" - ); - } -} - #[test] fn parse_alter_table_column_position() { let dialects = all_dialects_where(|d| d.supports_alter_column_position()); From b769fe875c9b22550c983943812c7271c35327db Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 13:43:14 +0200 Subject: [PATCH 4/5] Add fuzz seeds for pg drop rule --- fuzz/fuzz_seeds/postgres_2434_drop_rule.sql | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 fuzz/fuzz_seeds/postgres_2434_drop_rule.sql diff --git a/fuzz/fuzz_seeds/postgres_2434_drop_rule.sql b/fuzz/fuzz_seeds/postgres_2434_drop_rule.sql new file mode 100644 index 0000000000..a5dcf3e987 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_2434_drop_rule.sql @@ -0,0 +1,3 @@ +DROP RULE r ON t; +DROP RULE IF EXISTS r ON s1.t CASCADE; +DROP RULE r ON t RESTRICT From 5c50f9a830729158b61ffc374c68f729cde5b265 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 30 Oct 2026 21:52:13 +0100 Subject: [PATCH 5/5] Test DROP RULE rejection of name lists, qualified names and extra options --- tests/sqlparser_common.rs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e72e013f0a..7627f39185 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15885,6 +15885,45 @@ fn parse_drop_rule_rejects_missing_names() { } } +#[test] +fn parse_drop_rule_rejects_lists_and_extra_options() { + for (sql, expected) in [ + ("DROP RULE r1, r2 ON t", "Expected: ON, found: ,"), + ("DROP RULE s.r ON t", "Expected: ON, found: ."), + ("DROP RULE 1 ON t", "Expected: identifier, found: 1"), + ("DROP RULE IF EXISTS", "Expected: identifier, found: EOF"), + ("DROP RULE IF NOT EXISTS r ON t", "Expected: ON, found: NOT"), + ("DROP RULE r t", "Expected: ON, found: t"), + ("DROP RULE r ON (t)", "Expected: identifier, found: ("), + ( + "DROP RULE r ON t1, t2", + "Expected: end of statement, found: ,", + ), + ( + "DROP RULE r ON t ON t", + "Expected: end of statement, found: ON", + ), + ( + "DROP RULE r ON t CASCADE RESTRICT", + "Expected: end of statement, found: RESTRICT", + ), + ( + "DROP RULE r ON t RESTRICT CASCADE", + "Expected: end of statement, found: CASCADE", + ), + ( + "DROP RULE r ON t PURGE", + "Expected: end of statement, found: PURGE", + ), + ] { + assert_eq!( + all_dialects().parse_sql_statements(sql), + Err(ParserError::ParserError(expected.to_string())), + "{sql}" + ); + } +} + #[test] fn display_drop_rule_propagates_write_errors() { use core::fmt::{self, Write};