Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions fuzz/fuzz_seeds/postgres_2429_create_group.sql
Original file line number Diff line number Diff line change
@@ -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"
26 changes: 25 additions & 1 deletion src/ast/dcl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
/// <https://www.postgresql.org/docs/current/sql-createrole.html>
Expand Down Expand Up @@ -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<ObjectName>,
/// Whether `IF NOT EXISTS` was specified.
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,18 @@ pub trait Dialect: Debug + Any {
false
}

/// Returns true if the dialect supports `CREATE GROUP`.
///
/// <https://www.postgresql.org/docs/current/sql-creategroup.html>
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
Expand Down
8 changes: 8 additions & 0 deletions src/dialect/postgresql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
25 changes: 21 additions & 4 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -7088,9 +7090,23 @@ impl<'a> Parser<'a> {
}
}

/// Parse a `CREATE ROLE` statement.
pub fn parse_create_role(&mut self) -> Result<CreateRole, ParserError> {
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<CreateRole, ParserError> {
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 ]
Expand Down Expand Up @@ -7292,6 +7308,7 @@ impl<'a> Parser<'a> {
}

Ok(CreateRole {
keyword: role_keyword,
names,
if_not_exists,
login,
Expand Down
2 changes: 1 addition & 1 deletion tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
68 changes: 65 additions & 3 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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!(),
}
Expand Down Expand Up @@ -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";
Expand Down
Loading