diff --git a/fuzz/fuzz_seeds/postgres_2429_create_group.sql b/fuzz/fuzz_seeds/postgres_2429_create_group.sql new file mode 100644 index 0000000000..b243741374 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_2429_create_group.sql @@ -0,0 +1,4 @@ +CREATE GROUP g; +CREATE GROUP staff SUPERUSER LOGIN CONNECTION LIMIT 5 USER karl, john; +CREATE GROUP staff WITH SUPERUSER; +CREATE GROUP "IF" diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index 96b340714c..c716a80224 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -35,6 +35,27 @@ use crate::ast::{ }; use crate::tokenizer::Span; +/// The object keyword in a `CREATE ROLE` or `CREATE GROUP` statement. +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum RoleKeyword { + /// `ROLE`, the general role object in both PostgreSQL and Redshift. + Role, + /// `GROUP`, an obsolete spelling of `ROLE` in PostgreSQL, and a user group + /// distinct from a role in Redshift. + Group, +} + +impl fmt::Display for RoleKeyword { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + RoleKeyword::Role => "ROLE", + RoleKeyword::Group => "GROUP", + }) + } +} + /// An option in `ROLE` statement. /// /// @@ -309,6 +330,8 @@ impl fmt::Display for SecondaryRoles { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct CreateRole { + /// Whether the statement was spelled `CREATE ROLE` or `CREATE GROUP`. + pub keyword: RoleKeyword, /// Role names to create. pub names: Vec, /// Whether `IF NOT EXISTS` was specified. @@ -353,7 +376,8 @@ impl fmt::Display for CreateRole { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "CREATE ROLE {if_not_exists}{names}{superuser}{create_db}{create_role}{inherit}{login}{replication}{bypassrls}", + "CREATE {keyword} {if_not_exists}{names}{superuser}{create_db}{create_role}{inherit}{login}{replication}{bypassrls}", + keyword = self.keyword, if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" }, names = display_separated(&self.names, ", "), superuser = match self.superuser { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8e798524c9..373afeb5b7 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -58,8 +58,8 @@ pub use self::data_type::{ ExactNumberInfo, IntervalFields, MapBracketKind, StructBracketKind, TimezoneInfo, TypeModifier, }; pub use self::dcl::{ - AlterRoleOperation, CreateRole, Grant, ResetConfig, Revoke, RoleOption, SecondaryRoles, - SetConfigValue, Use, + AlterRoleOperation, CreateRole, Grant, ResetConfig, Revoke, RoleKeyword, RoleOption, + SecondaryRoles, SetConfigValue, Use, }; pub use self::ddl::{ AggregateModifyKind, Alignment, AlterCollation, AlterCollationOperation, AlterColumnOperation, diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index c867b76f61..a9cb4de557 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -443,6 +443,18 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports `CREATE GROUP`. + /// + /// + fn supports_create_group(&self) -> bool { + false + } + + /// Returns true if `CREATE ROLE` accepts `IF NOT EXISTS`. + fn supports_create_role_if_not_exists(&self) -> bool { + true + } + /// Returns true if the dialects supports `group sets, roll up, or cube` expressions. fn supports_group_by_expr(&self) -> bool { false diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 9e5257b30f..76da98a9f7 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -355,6 +355,14 @@ impl Dialect for PostgreSqlDialect { true } + fn supports_create_group(&self) -> bool { + true + } + + fn supports_create_role_if_not_exists(&self) -> bool { + false + } + fn prec_value(&self, prec: Precedence) -> u8 { match prec { Precedence::Period => PERIOD_PREC, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3efb22a349..e8a8ed5af4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5497,7 +5497,9 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::DATABASE) { self.parse_create_database() } else if self.parse_keyword(Keyword::ROLE) { - self.parse_create_role().map(Into::into) + self.parse_create_role(RoleKeyword::Role).map(Into::into) + } else if self.dialect.supports_create_group() && self.parse_keyword(Keyword::GROUP) { + self.parse_create_role(RoleKeyword::Group).map(Into::into) } else if self.parse_keyword(Keyword::SEQUENCE) { self.parse_create_sequence(temporary) } else if self.parse_keyword(Keyword::COLLATION) { @@ -7088,9 +7090,23 @@ impl<'a> Parser<'a> { } } - /// Parse a `CREATE ROLE` statement. - pub fn parse_create_role(&mut self) -> Result { - let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + /// Parse a `CREATE ROLE` or `CREATE GROUP` statement, after the keyword has been consumed. + pub fn parse_create_role( + &mut self, + role_keyword: RoleKeyword, + ) -> Result { + let supports_if_not_exists = matches!(role_keyword, RoleKeyword::Role) + && self.dialect.supports_create_role_if_not_exists(); + if !supports_if_not_exists + && self.peek_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]) + { + return parser_err!( + format!("IF NOT EXISTS is not supported for CREATE {role_keyword}"), + self.peek_token_ref().span.start + ); + } + let if_not_exists = supports_if_not_exists + && self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let names = self.parse_comma_separated(|p| p.parse_object_name(false))?; let _ = self.parse_keyword(Keyword::WITH); // [ WITH ] @@ -7292,6 +7308,7 @@ impl<'a> Parser<'a> { } Ok(CreateRole { + keyword: role_keyword, names, if_not_exists, login, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f71869e820..64505c7960 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10416,7 +10416,7 @@ fn parse_create_role() { } let sql = "CREATE ROLE IF NOT EXISTS mysql_a, mysql_b"; - match verified_stmt(sql) { + match all_dialects_where(|d| d.supports_create_role_if_not_exists()).verified_stmt(sql) { Statement::CreateRole(create_role) => { assert_eq_vec(&["mysql_a", "mysql_b"], &create_role.names); assert!(create_role.if_not_exists); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index aa9252125b..ca9a3267d2 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -25,7 +25,8 @@ mod test_utils; use helpers::attached_token::AttachedToken; use sqlparser::ast::*; use sqlparser::dialect::{ - Dialect, GenericDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect, SnowflakeDialect, + Dialect, GenericDialect, MsSqlDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect, + SnowflakeDialect, }; use sqlparser::parser::{Parser, ParserError}; use sqlparser::tokenizer::{Location, Span, Token, Tokenizer}; @@ -4739,11 +4740,11 @@ fn parse_operator_empty_parens_rejected() { #[test] fn parse_create_role() { - let sql = "CREATE ROLE IF NOT EXISTS mysql_a, mysql_b"; + let sql = "CREATE ROLE mysql_a, mysql_b"; match pg().verified_stmt(sql) { Statement::CreateRole(create_role) => { assert_eq_vec(&["mysql_a", "mysql_b"], &create_role.names); - assert!(create_role.if_not_exists); + assert!(!create_role.if_not_exists); } _ => unreachable!(), } @@ -4834,6 +4835,67 @@ fn parse_create_role() { } } +#[test] +fn parse_create_group() { + // `GROUP` names the same object as `ROLE` in PostgreSQL, but the keyword is preserved. + pg().verified_stmt("CREATE GROUP g"); + pg().verified_stmt("CREATE GROUP staff SUPERUSER LOGIN CONNECTION LIMIT 5 USER karl, john"); + pg().one_statement_parses_to( + "CREATE GROUP staff WITH SUPERUSER", + "CREATE GROUP staff SUPERUSER", + ); + + match pg().verified_stmt("CREATE GROUP staff") { + Statement::CreateRole(create_role) => { + assert_eq!(create_role.keyword, RoleKeyword::Group); + assert_eq_vec(&["staff"], &create_role.names); + } + other => panic!("expected CREATE ROLE statement, got {other:?}"), + } + + // The `ROLE` spelling is unaffected. + match pg().verified_stmt("CREATE ROLE staff") { + Statement::CreateRole(create_role) => { + assert_eq!(create_role.keyword, RoleKeyword::Role) + } + other => panic!("expected CREATE ROLE statement, got {other:?}"), + } + + assert_eq!( + pg().parse_sql_statements("CREATE GROUP IF NOT EXISTS g"), + Err(ParserError::ParserError( + "IF NOT EXISTS is not supported for CREATE GROUP".to_string() + )) + ); + pg().verified_stmt(r#"CREATE GROUP "IF""#); +} + +#[test] +fn parse_create_group_is_dialect_gated() { + let others = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(MySqlDialect {}), + Box::new(MsSqlDialect {}), + ]); + assert_eq!( + others.parse_sql_statements("CREATE GROUP staff"), + Err(ParserError::ParserError( + "Expected: an object type after CREATE, found: GROUP".to_string() + )) + ); +} + +#[test] +fn parse_create_role_if_not_exists_rejected() { + assert_eq!( + pg().parse_sql_statements("CREATE ROLE IF NOT EXISTS r"), + Err(ParserError::ParserError( + "IF NOT EXISTS is not supported for CREATE ROLE".to_string() + )) + ); + pg().verified_stmt(r#"CREATE ROLE "IF""#); +} + #[test] fn parse_alter_role() { let sql = "ALTER ROLE old_name RENAME TO new_name";