diff --git a/fuzz/fuzz_seeds/postgres_2712_drop_group.sql b/fuzz/fuzz_seeds/postgres_2712_drop_group.sql new file mode 100644 index 0000000000..d40197a6b0 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_2712_drop_group.sql @@ -0,0 +1,3 @@ +DROP GROUP staff; +DROP GROUP IF EXISTS staff, workers; +DROP GROUP "Mixed Name", "GROUP" diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9b5441738a..94963c0374 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8769,6 +8769,8 @@ pub enum ObjectType { Database, /// A role. Role, + /// A user group. + Group, /// A sequence. Sequence, /// A stage. @@ -8794,6 +8796,7 @@ impl fmt::Display for ObjectType { ObjectType::Schema => "SCHEMA", ObjectType::Database => "DATABASE", ObjectType::Role => "ROLE", + ObjectType::Group => "GROUP", ObjectType::Sequence => "SEQUENCE", ObjectType::Stage => "STAGE", ObjectType::Type => "TYPE", diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 5f208f4dd4..cc1b1968b7 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -423,6 +423,13 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports `DROP GROUP`. + /// + /// + fn supports_drop_group(&self) -> bool { + false + } + /// Returns true if the dialect treats `ALTER USER` as a synonym for `ALTER ROLE`. /// /// In PostgreSQL, `ALTER USER` and `ALTER ROLE` are synonyms that accept the same diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index e5c09725b1..7a99e836e6 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -351,6 +351,10 @@ impl Dialect for PostgreSqlDialect { true } + fn supports_drop_group(&self) -> bool { + true + } + fn supports_alter_user_as_alter_role(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index fb22bada5b..b069c39c2a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8093,6 +8093,8 @@ impl<'a> Parser<'a> { ObjectType::Index } else if self.parse_keyword(Keyword::ROLE) { ObjectType::Role + } else if self.dialect.supports_drop_group() && self.parse_keyword(Keyword::GROUP) { + ObjectType::Group } else if self.parse_keyword(Keyword::SCHEMA) { ObjectType::Schema } else if self.parse_keyword(Keyword::DATABASE) { @@ -8136,7 +8138,7 @@ impl<'a> Parser<'a> { }; } else { return self.expected_ref( - "COLLATION, CONNECTOR, DATABASE, EXTENSION, FUNCTION, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP", + "COLLATION, CONNECTOR, DATABASE, EXTENSION, FUNCTION, GROUP, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP", self.peek_token_ref(), ); }; @@ -8152,9 +8154,11 @@ impl<'a> Parser<'a> { if cascade && restrict { return parser_err!("Cannot specify both CASCADE and RESTRICT in DROP", loc); } - if object_type == ObjectType::Role && (cascade || restrict || purge) { + if matches!(object_type, ObjectType::Role | ObjectType::Group) + && (cascade || restrict || purge) + { return parser_err!( - "Cannot specify CASCADE, RESTRICT, or PURGE in DROP ROLE", + format_args!("Cannot specify CASCADE, RESTRICT, or PURGE in DROP {object_type}"), loc ); } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index fbb5ef6357..c0adcb37bd 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10916,6 +10916,45 @@ fn parse_create_foreign_data_wrapper() { } } +#[test] +fn parse_drop_group() { + for (sql, expected_names, expected_if_exists) in [ + ("DROP GROUP staff", vec!["staff"], false), + ( + "DROP GROUP IF EXISTS staff, workers", + vec!["staff", "workers"], + true, + ), + ] { + match pg().verified_stmt(sql) { + Statement::Drop { + object_type, + if_exists, + names, + .. + } => { + assert_eq!(object_type, ObjectType::Group); + assert_eq_vec(&expected_names, &names); + assert_eq!(if_exists, expected_if_exists); + } + other => panic!("expected DROP statement, got {other:?}"), + } + } + pg().verified_stmt(r#"DROP GROUP "Mixed Name", "GROUP""#); +} + +#[test] +fn parse_drop_group_rejects_drop_behaviors() { + for behavior in ["CASCADE", "RESTRICT", "PURGE"] { + assert_eq!( + pg().parse_sql_statements(&format!("DROP GROUP staff {behavior}")), + Err(ParserError::ParserError( + "Cannot specify CASCADE, RESTRICT, or PURGE in DROP GROUP".to_string() + )) + ); + } +} + #[test] fn foreign_data_wrapper_display_and_span() { let ident = |value: &str, start: u64, end: u64| Ident { @@ -11167,3 +11206,14 @@ fn parse_alter_table_attach_partition_of() { ); } } + +#[test] +fn parse_drop_group_is_dialect_gated() { + assert_eq!( + all_dialects_where(|d| !d.supports_drop_group()) + .parse_sql_statements("DROP GROUP staff"), + Err(ParserError::ParserError( + "Expected: COLLATION, CONNECTOR, DATABASE, EXTENSION, 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() + )) + ); +}