Skip to content

Commit d7f2372

Browse files
PostgreSQL: parse CREATE GROUP and DROP GROUP
1 parent 30d0836 commit d7f2372

6 files changed

Lines changed: 149 additions & 10 deletions

File tree

‎src/ast/dcl.rs‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,32 @@ use crate::ast::{
3535
};
3636
use crate::tokenizer::Span;
3737

38+
/// The keyword naming the object in a `CREATE`, `ALTER` or `DROP` role statement.
39+
///
40+
/// PostgreSQL accepts `GROUP` as an obsolete spelling of `ROLE`, and Amazon Redshift has user
41+
/// groups as objects distinct from roles. The keyword is preserved either way.
42+
///
43+
/// <https://www.postgresql.org/docs/current/sql-creategroup.html>
44+
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
45+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
46+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
47+
pub enum RoleKeyword {
48+
/// `ROLE`, the general role object in both PostgreSQL and Redshift.
49+
Role,
50+
/// `GROUP`, an obsolete spelling of `ROLE` in PostgreSQL, and a user group
51+
/// distinct from a role in Redshift.
52+
Group,
53+
}
54+
55+
impl fmt::Display for RoleKeyword {
56+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
57+
f.write_str(match self {
58+
RoleKeyword::Role => "ROLE",
59+
RoleKeyword::Group => "GROUP",
60+
})
61+
}
62+
}
63+
3864
/// An option in `ROLE` statement.
3965
///
4066
/// <https://www.postgresql.org/docs/current/sql-createrole.html>
@@ -309,6 +335,8 @@ impl fmt::Display for SecondaryRoles {
309335
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
310336
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
311337
pub struct CreateRole {
338+
/// Whether the statement was spelled `CREATE ROLE` or `CREATE GROUP`.
339+
pub keyword: RoleKeyword,
312340
/// Role names to create.
313341
pub names: Vec<ObjectName>,
314342
/// Whether `IF NOT EXISTS` was specified.
@@ -353,7 +381,8 @@ impl fmt::Display for CreateRole {
353381
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
354382
write!(
355383
f,
356-
"CREATE ROLE {if_not_exists}{names}{superuser}{create_db}{create_role}{inherit}{login}{replication}{bypassrls}",
384+
"CREATE {keyword} {if_not_exists}{names}{superuser}{create_db}{create_role}{inherit}{login}{replication}{bypassrls}",
385+
keyword = self.keyword,
357386
if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" },
358387
names = display_separated(&self.names, ", "),
359388
superuser = match self.superuser {

‎src/ast/mod.rs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ pub use self::data_type::{
5858
ExactNumberInfo, IntervalFields, MapBracketKind, StructBracketKind, TimezoneInfo,
5959
};
6060
pub use self::dcl::{
61-
AlterRoleOperation, CreateRole, Grant, ResetConfig, Revoke, RoleOption, SecondaryRoles,
62-
SetConfigValue, Use,
61+
AlterRoleOperation, CreateRole, Grant, ResetConfig, Revoke, RoleKeyword, RoleOption,
62+
SecondaryRoles, SetConfigValue, Use,
6363
};
6464
pub use self::ddl::{
6565
Alignment, AlterCollation, AlterCollationOperation, AlterColumnOperation, AlterConnectorOwner,
@@ -8656,6 +8656,8 @@ pub enum ObjectType {
86568656
Database,
86578657
/// A role.
86588658
Role,
8659+
/// A user group.
8660+
Group,
86598661
/// A sequence.
86608662
Sequence,
86618663
/// A stage.
@@ -8681,6 +8683,7 @@ impl fmt::Display for ObjectType {
86818683
ObjectType::Schema => "SCHEMA",
86828684
ObjectType::Database => "DATABASE",
86838685
ObjectType::Role => "ROLE",
8686+
ObjectType::Group => "GROUP",
86848687
ObjectType::Sequence => "SEQUENCE",
86858688
ObjectType::Stage => "STAGE",
86868689
ObjectType::Type => "TYPE",

‎src/dialect/mod.rs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,20 @@ pub trait Dialect: Debug + Any {
378378
false
379379
}
380380

381+
/// Returns true if the dialect has `CREATE GROUP` and `DROP GROUP` statements.
382+
///
383+
/// PostgreSQL accepts `GROUP` as an obsolete spelling of `ROLE`, and Amazon Redshift has
384+
/// user groups as objects distinct from roles. The keyword is preserved either way, as
385+
/// [`RoleKeyword::Group`] and [`ObjectType::Group`].
386+
///
387+
/// <https://www.postgresql.org/docs/current/sql-creategroup.html>
388+
///
389+
/// [`RoleKeyword::Group`]: crate::ast::RoleKeyword::Group
390+
/// [`ObjectType::Group`]: crate::ast::ObjectType::Group
391+
fn supports_user_group_statements(&self) -> bool {
392+
false
393+
}
394+
381395
/// Returns true if the dialects supports `group sets, roll up, or cube` expressions.
382396
fn supports_group_by_expr(&self) -> bool {
383397
false

‎src/dialect/postgresql.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,10 @@ impl Dialect for PostgreSqlDialect {
165165
true
166166
}
167167

168+
fn supports_user_group_statements(&self) -> bool {
169+
true
170+
}
171+
168172
fn prec_value(&self, prec: Precedence) -> u8 {
169173
match prec {
170174
Precedence::Period => PERIOD_PREC,

‎src/parser/mod.rs‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5304,7 +5304,11 @@ impl<'a> Parser<'a> {
53045304
} else if self.parse_keyword(Keyword::DATABASE) {
53055305
self.parse_create_database()
53065306
} else if self.parse_keyword(Keyword::ROLE) {
5307-
self.parse_create_role().map(Into::into)
5307+
self.parse_create_role(RoleKeyword::Role).map(Into::into)
5308+
} else if self.dialect.supports_user_group_statements()
5309+
&& self.parse_keyword(Keyword::GROUP)
5310+
{
5311+
self.parse_create_role(RoleKeyword::Group).map(Into::into)
53085312
} else if self.parse_keyword(Keyword::SEQUENCE) {
53095313
self.parse_create_sequence(temporary)
53105314
} else if self.parse_keyword(Keyword::COLLATION) {
@@ -6884,8 +6888,11 @@ impl<'a> Parser<'a> {
68846888
}
68856889
}
68866890

6887-
/// Parse a `CREATE ROLE` statement.
6888-
pub fn parse_create_role(&mut self) -> Result<CreateRole, ParserError> {
6891+
/// Parse a `CREATE ROLE` or `CREATE GROUP` statement, after the keyword has been consumed.
6892+
pub fn parse_create_role(
6893+
&mut self,
6894+
role_keyword: RoleKeyword,
6895+
) -> Result<CreateRole, ParserError> {
68896896
let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
68906897
let names = self.parse_comma_separated(|p| p.parse_object_name(false))?;
68916898

@@ -7088,6 +7095,7 @@ impl<'a> Parser<'a> {
70887095
}
70897096

70907097
Ok(CreateRole {
7098+
keyword: role_keyword,
70917099
names,
70927100
if_not_exists,
70937101
login,
@@ -7587,6 +7595,10 @@ impl<'a> Parser<'a> {
75877595
ObjectType::Index
75887596
} else if self.parse_keyword(Keyword::ROLE) {
75897597
ObjectType::Role
7598+
} else if self.dialect.supports_user_group_statements()
7599+
&& self.parse_keyword(Keyword::GROUP)
7600+
{
7601+
ObjectType::Group
75907602
} else if self.parse_keyword(Keyword::SCHEMA) {
75917603
ObjectType::Schema
75927604
} else if self.parse_keyword(Keyword::DATABASE) {
@@ -7630,7 +7642,7 @@ impl<'a> Parser<'a> {
76307642
};
76317643
} else {
76327644
return self.expected_ref(
7633-
"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",
7645+
"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",
76347646
self.peek_token_ref(),
76357647
);
76367648
};
@@ -7646,9 +7658,11 @@ impl<'a> Parser<'a> {
76467658
if cascade && restrict {
76477659
return parser_err!("Cannot specify both CASCADE and RESTRICT in DROP", loc);
76487660
}
7649-
if object_type == ObjectType::Role && (cascade || restrict || purge) {
7661+
if matches!(object_type, ObjectType::Role | ObjectType::Group)
7662+
&& (cascade || restrict || purge)
7663+
{
76507664
return parser_err!(
7651-
"Cannot specify CASCADE, RESTRICT, or PURGE in DROP ROLE",
7665+
format!("Cannot specify CASCADE, RESTRICT, or PURGE in DROP {object_type}"),
76527666
loc
76537667
);
76547668
}

‎tests/sqlparser_postgres.rs‎

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ mod test_utils;
2424

2525
use helpers::attached_token::AttachedToken;
2626
use sqlparser::ast::*;
27-
use sqlparser::dialect::{Dialect, GenericDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect};
27+
use sqlparser::dialect::{
28+
Dialect, GenericDialect, MsSqlDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect,
29+
};
2830
use sqlparser::parser::ParserError;
2931
use sqlparser::tokenizer::Span;
3032
use test_utils::*;
@@ -9663,3 +9665,76 @@ fn parse_right_deep_join_chain() {
96639665
// NATURAL JOIN followed by a constrained join must stay left-associative.
96649666
pg().verified_stmt("SELECT * FROM t0 NATURAL JOIN t1 INNER JOIN t2 ON true");
96659667
}
9668+
9669+
#[test]
9670+
fn parse_create_group() {
9671+
// `GROUP` names the same object as `ROLE` in PostgreSQL, but the keyword is preserved.
9672+
pg().verified_stmt("CREATE GROUP g");
9673+
pg().verified_stmt("CREATE GROUP staff SUPERUSER LOGIN CONNECTION LIMIT 5 USER karl, john");
9674+
pg().one_statement_parses_to(
9675+
"CREATE GROUP staff WITH SUPERUSER",
9676+
"CREATE GROUP staff SUPERUSER",
9677+
);
9678+
9679+
match pg().verified_stmt("CREATE GROUP staff") {
9680+
Statement::CreateRole(create_role) => {
9681+
assert_eq!(create_role.keyword, RoleKeyword::Group);
9682+
assert_eq_vec(&["staff"], &create_role.names);
9683+
}
9684+
other => panic!("expected CREATE ROLE statement, got {other:?}"),
9685+
}
9686+
9687+
// The `ROLE` spelling is unaffected.
9688+
match pg().verified_stmt("CREATE ROLE staff") {
9689+
Statement::CreateRole(create_role) => {
9690+
assert_eq!(create_role.keyword, RoleKeyword::Role)
9691+
}
9692+
other => panic!("expected CREATE ROLE statement, got {other:?}"),
9693+
}
9694+
}
9695+
9696+
#[test]
9697+
fn parse_drop_group() {
9698+
pg().verified_stmt("DROP GROUP IF EXISTS staff, workers");
9699+
9700+
assert_eq!(
9701+
pg().verified_stmt("DROP GROUP staff"),
9702+
Statement::Drop {
9703+
object_type: ObjectType::Group,
9704+
if_exists: false,
9705+
names: vec![ObjectName::from(vec![Ident::new("staff")])],
9706+
cascade: false,
9707+
restrict: false,
9708+
purge: false,
9709+
temporary: false,
9710+
table: None,
9711+
}
9712+
);
9713+
9714+
// PostgreSQL rejects a drop behavior after `DROP GROUP`, exactly as after `DROP ROLE`.
9715+
assert_eq!(
9716+
pg().parse_sql_statements("DROP GROUP staff CASCADE")
9717+
.unwrap_err()
9718+
.to_string(),
9719+
"sql parser error: Cannot specify CASCADE, RESTRICT, or PURGE in DROP GROUP"
9720+
);
9721+
9722+
pg().verified_stmt("DROP ROLE staff");
9723+
}
9724+
9725+
#[test]
9726+
fn parse_user_group_statements_is_dialect_gated() {
9727+
// Only PostgreSQL opts in. Redshift documents the same statements and can turn the hook
9728+
// on, but its narrower `CREATE GROUP` option list is a separate question.
9729+
let others = TestedDialects::new(vec![
9730+
Box::new(GenericDialect {}),
9731+
Box::new(MySqlDialect {}),
9732+
Box::new(MsSqlDialect {}),
9733+
]);
9734+
for sql in ["CREATE GROUP staff", "DROP GROUP staff"] {
9735+
assert!(
9736+
others.parse_sql_statements(sql).is_err(),
9737+
"{sql} should not parse in a dialect without GROUP statements"
9738+
);
9739+
}
9740+
}

0 commit comments

Comments
 (0)