Skip to content

Commit eb95863

Browse files
PostgreSQL: parse CREATE GROUP and DROP GROUP
1 parent 777a166 commit eb95863

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,
@@ -8676,6 +8676,8 @@ pub enum ObjectType {
86768676
Database,
86778677
/// A role.
86788678
Role,
8679+
/// A user group.
8680+
Group,
86798681
/// A sequence.
86808682
Sequence,
86818683
/// A stage.
@@ -8701,6 +8703,7 @@ impl fmt::Display for ObjectType {
87018703
ObjectType::Schema => "SCHEMA",
87028704
ObjectType::Database => "DATABASE",
87038705
ObjectType::Role => "ROLE",
8706+
ObjectType::Group => "GROUP",
87048707
ObjectType::Sequence => "SEQUENCE",
87058708
ObjectType::Stage => "STAGE",
87068709
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
@@ -5327,7 +5327,11 @@ impl<'a> Parser<'a> {
53275327
} else if self.parse_keyword(Keyword::DATABASE) {
53285328
self.parse_create_database()
53295329
} else if self.parse_keyword(Keyword::ROLE) {
5330-
self.parse_create_role().map(Into::into)
5330+
self.parse_create_role(RoleKeyword::Role).map(Into::into)
5331+
} else if self.dialect.supports_user_group_statements()
5332+
&& self.parse_keyword(Keyword::GROUP)
5333+
{
5334+
self.parse_create_role(RoleKeyword::Group).map(Into::into)
53315335
} else if self.parse_keyword(Keyword::SEQUENCE) {
53325336
self.parse_create_sequence(temporary)
53335337
} else if self.parse_keyword(Keyword::COLLATION) {
@@ -6907,8 +6911,11 @@ impl<'a> Parser<'a> {
69076911
}
69086912
}
69096913

6910-
/// Parse a `CREATE ROLE` statement.
6911-
pub fn parse_create_role(&mut self) -> Result<CreateRole, ParserError> {
6914+
/// Parse a `CREATE ROLE` or `CREATE GROUP` statement, after the keyword has been consumed.
6915+
pub fn parse_create_role(
6916+
&mut self,
6917+
role_keyword: RoleKeyword,
6918+
) -> Result<CreateRole, ParserError> {
69126919
let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
69136920
let names = self.parse_comma_separated(|p| p.parse_object_name(false))?;
69146921

@@ -7111,6 +7118,7 @@ impl<'a> Parser<'a> {
71117118
}
71127119

71137120
Ok(CreateRole {
7121+
keyword: role_keyword,
71147122
names,
71157123
if_not_exists,
71167124
login,
@@ -7610,6 +7618,10 @@ impl<'a> Parser<'a> {
76107618
ObjectType::Index
76117619
} else if self.parse_keyword(Keyword::ROLE) {
76127620
ObjectType::Role
7621+
} else if self.dialect.supports_user_group_statements()
7622+
&& self.parse_keyword(Keyword::GROUP)
7623+
{
7624+
ObjectType::Group
76137625
} else if self.parse_keyword(Keyword::SCHEMA) {
76147626
ObjectType::Schema
76157627
} else if self.parse_keyword(Keyword::DATABASE) {
@@ -7653,7 +7665,7 @@ impl<'a> Parser<'a> {
76537665
};
76547666
} else {
76557667
return self.expected_ref(
7656-
"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",
7668+
"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",
76577669
self.peek_token_ref(),
76587670
);
76597671
};
@@ -7669,9 +7681,11 @@ impl<'a> Parser<'a> {
76697681
if cascade && restrict {
76707682
return parser_err!("Cannot specify both CASCADE and RESTRICT in DROP", loc);
76717683
}
7672-
if object_type == ObjectType::Role && (cascade || restrict || purge) {
7684+
if matches!(object_type, ObjectType::Role | ObjectType::Group)
7685+
&& (cascade || restrict || purge)
7686+
{
76737687
return parser_err!(
7674-
"Cannot specify CASCADE, RESTRICT, or PURGE in DROP ROLE",
7688+
format!("Cannot specify CASCADE, RESTRICT, or PURGE in DROP {object_type}"),
76757689
loc
76767690
);
76777691
}

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::{Parser, ParserError};
2931
use sqlparser::tokenizer::{Location, Span};
3032
use test_utils::*;
@@ -9852,3 +9854,76 @@ fn parse_alter_table_constraint_check_no_inherit() {
98529854
}
98539855
pg_and_generic().verified_stmt("ALTER TABLE docs ADD CONSTRAINT c CHECK (id > 0) NO INHERIT");
98549856
}
9857+
9858+
#[test]
9859+
fn parse_create_group() {
9860+
// `GROUP` names the same object as `ROLE` in PostgreSQL, but the keyword is preserved.
9861+
pg().verified_stmt("CREATE GROUP g");
9862+
pg().verified_stmt("CREATE GROUP staff SUPERUSER LOGIN CONNECTION LIMIT 5 USER karl, john");
9863+
pg().one_statement_parses_to(
9864+
"CREATE GROUP staff WITH SUPERUSER",
9865+
"CREATE GROUP staff SUPERUSER",
9866+
);
9867+
9868+
match pg().verified_stmt("CREATE GROUP staff") {
9869+
Statement::CreateRole(create_role) => {
9870+
assert_eq!(create_role.keyword, RoleKeyword::Group);
9871+
assert_eq_vec(&["staff"], &create_role.names);
9872+
}
9873+
other => panic!("expected CREATE ROLE statement, got {other:?}"),
9874+
}
9875+
9876+
// The `ROLE` spelling is unaffected.
9877+
match pg().verified_stmt("CREATE ROLE staff") {
9878+
Statement::CreateRole(create_role) => {
9879+
assert_eq!(create_role.keyword, RoleKeyword::Role)
9880+
}
9881+
other => panic!("expected CREATE ROLE statement, got {other:?}"),
9882+
}
9883+
}
9884+
9885+
#[test]
9886+
fn parse_drop_group() {
9887+
pg().verified_stmt("DROP GROUP IF EXISTS staff, workers");
9888+
9889+
assert_eq!(
9890+
pg().verified_stmt("DROP GROUP staff"),
9891+
Statement::Drop {
9892+
object_type: ObjectType::Group,
9893+
if_exists: false,
9894+
names: vec![ObjectName::from(vec![Ident::new("staff")])],
9895+
cascade: false,
9896+
restrict: false,
9897+
purge: false,
9898+
temporary: false,
9899+
table: None,
9900+
}
9901+
);
9902+
9903+
// PostgreSQL rejects a drop behavior after `DROP GROUP`, exactly as after `DROP ROLE`.
9904+
assert_eq!(
9905+
pg().parse_sql_statements("DROP GROUP staff CASCADE")
9906+
.unwrap_err()
9907+
.to_string(),
9908+
"sql parser error: Cannot specify CASCADE, RESTRICT, or PURGE in DROP GROUP"
9909+
);
9910+
9911+
pg().verified_stmt("DROP ROLE staff");
9912+
}
9913+
9914+
#[test]
9915+
fn parse_user_group_statements_is_dialect_gated() {
9916+
// Only PostgreSQL opts in. Redshift documents the same statements and can turn the hook
9917+
// on, but its narrower `CREATE GROUP` option list is a separate question.
9918+
let others = TestedDialects::new(vec![
9919+
Box::new(GenericDialect {}),
9920+
Box::new(MySqlDialect {}),
9921+
Box::new(MsSqlDialect {}),
9922+
]);
9923+
for sql in ["CREATE GROUP staff", "DROP GROUP staff"] {
9924+
assert!(
9925+
others.parse_sql_statements(sql).is_err(),
9926+
"{sql} should not parse in a dialect without GROUP statements"
9927+
);
9928+
}
9929+
}

0 commit comments

Comments
 (0)