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
31 changes: 30 additions & 1 deletion src/ast/dcl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,32 @@ use crate::ast::{
};
use crate::tokenizer::Span;

/// The keyword naming the object in a `CREATE`, `ALTER` or `DROP` role statement.
///
/// PostgreSQL accepts `GROUP` as an obsolete spelling of `ROLE`, and Amazon Redshift has user
/// groups as objects distinct from roles. The keyword is preserved either way.
///
/// <https://www.postgresql.org/docs/current/sql-creategroup.html>
#[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 +335,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 +381,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
7 changes: 5 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,
};
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::{
Alignment, AlterCollation, AlterCollationOperation, AlterColumnOperation, AlterConnectorOwner,
Expand Down Expand Up @@ -8656,6 +8656,8 @@ pub enum ObjectType {
Database,
/// A role.
Role,
/// A user group.
Group,
/// A sequence.
Sequence,
/// A stage.
Expand All @@ -8681,6 +8683,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",
Expand Down
14 changes: 14 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,20 @@ pub trait Dialect: Debug + Any {
false
}

/// Returns true if the dialect has `CREATE GROUP` and `DROP GROUP` statements.
///
/// PostgreSQL accepts `GROUP` as an obsolete spelling of `ROLE`, and Amazon Redshift has
/// user groups as objects distinct from roles. The keyword is preserved either way, as
/// [`RoleKeyword::Group`] and [`ObjectType::Group`].
///
/// <https://www.postgresql.org/docs/current/sql-creategroup.html>
///
/// [`RoleKeyword::Group`]: crate::ast::RoleKeyword::Group
/// [`ObjectType::Group`]: crate::ast::ObjectType::Group
fn supports_user_group_statements(&self) -> bool {
false
}

/// Returns true if the dialects supports `group sets, roll up, or cube` expressions.
fn supports_group_by_expr(&self) -> bool {
false
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/postgresql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ impl Dialect for PostgreSqlDialect {
true
}

fn supports_user_group_statements(&self) -> bool {
true
}

fn prec_value(&self, prec: Precedence) -> u8 {
match prec {
Precedence::Period => PERIOD_PREC,
Expand Down
26 changes: 20 additions & 6 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5304,7 +5304,11 @@ 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_user_group_statements()
&& 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 @@ -6884,8 +6888,11 @@ impl<'a> Parser<'a> {
}
}

/// Parse a `CREATE ROLE` statement.
pub fn parse_create_role(&mut self) -> Result<CreateRole, ParserError> {
/// 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 if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
let names = self.parse_comma_separated(|p| p.parse_object_name(false))?;

Expand Down Expand Up @@ -7088,6 +7095,7 @@ impl<'a> Parser<'a> {
}

Ok(CreateRole {
keyword: role_keyword,
names,
if_not_exists,
login,
Expand Down Expand Up @@ -7587,6 +7595,10 @@ impl<'a> Parser<'a> {
ObjectType::Index
} else if self.parse_keyword(Keyword::ROLE) {
ObjectType::Role
} else if self.dialect.supports_user_group_statements()
&& self.parse_keyword(Keyword::GROUP)
{
ObjectType::Group
} else if self.parse_keyword(Keyword::SCHEMA) {
ObjectType::Schema
} else if self.parse_keyword(Keyword::DATABASE) {
Expand Down Expand Up @@ -7630,7 +7642,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(),
);
};
Expand All @@ -7646,9 +7658,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!("Cannot specify CASCADE, RESTRICT, or PURGE in DROP {object_type}"),
loc
);
}
Expand Down
77 changes: 76 additions & 1 deletion tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ mod test_utils;

use helpers::attached_token::AttachedToken;
use sqlparser::ast::*;
use sqlparser::dialect::{Dialect, GenericDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect};
use sqlparser::dialect::{
Dialect, GenericDialect, MsSqlDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect,
};
use sqlparser::parser::ParserError;
use sqlparser::tokenizer::Span;
use test_utils::*;
Expand Down Expand Up @@ -9663,3 +9665,76 @@ fn parse_right_deep_join_chain() {
// NATURAL JOIN followed by a constrained join must stay left-associative.
pg().verified_stmt("SELECT * FROM t0 NATURAL JOIN t1 INNER JOIN t2 ON true");
}

#[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:?}"),
}
}

#[test]
fn parse_drop_group() {
pg().verified_stmt("DROP GROUP IF EXISTS staff, workers");

assert_eq!(
pg().verified_stmt("DROP GROUP staff"),
Statement::Drop {
object_type: ObjectType::Group,
if_exists: false,
names: vec![ObjectName::from(vec![Ident::new("staff")])],
cascade: false,
restrict: false,
purge: false,
temporary: false,
table: None,
}
);

// PostgreSQL rejects a drop behavior after `DROP GROUP`, exactly as after `DROP ROLE`.
assert_eq!(
pg().parse_sql_statements("DROP GROUP staff CASCADE")
.unwrap_err()
.to_string(),
"sql parser error: Cannot specify CASCADE, RESTRICT, or PURGE in DROP GROUP"
);

pg().verified_stmt("DROP ROLE staff");
}

#[test]
fn parse_user_group_statements_is_dialect_gated() {
// Only PostgreSQL opts in. Redshift documents the same statements and can turn the hook
// on, but its narrower `CREATE GROUP` option list is a separate question.
let others = TestedDialects::new(vec![
Box::new(GenericDialect {}),
Box::new(MySqlDialect {}),
Box::new(MsSqlDialect {}),
]);
for sql in ["CREATE GROUP staff", "DROP GROUP staff"] {
assert!(
others.parse_sql_statements(sql).is_err(),
"{sql} should not parse in a dialect without GROUP statements"
);
}
}
Loading