Skip to content

Commit 170e08e

Browse files
Task LAV-1738: ALTER DATABASE ROLE support (RENAME TO / SET COMMENT / UNSET COMMENT) (apache#2190)
* task LAV-1738: WIP — commit stranded agent work * Task LAV-1738: fix SHOW GRANTS TO DATABASE ROLE parity for ALTER rename - synthesize implicit USAGE-on-own-database edge and render grantee_name unqualified in __snowflake$show_grants_to_database_role - emit queryContext:null for 003594 (cross-database rename rejection) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * task LAV-1738: fix CI — cover unqualified SHOW GRANTS TO DATABASE ROLE (patch coverage) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a6eaf8e commit 170e08e

5 files changed

Lines changed: 201 additions & 3 deletions

File tree

src/ast/dcl.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,42 @@ impl fmt::Display for AlterRoleOperation {
249249
}
250250
}
251251

252+
/// An `ALTER DATABASE ROLE` (`Statement::AlterDatabaseRole`) operation
253+
/// (Snowflake). The new name of a rename may itself be database-qualified,
254+
/// which is why this carries an `ObjectName` rather than reusing
255+
/// `AlterRoleOperation`.
256+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
257+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
258+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
259+
pub enum AlterDatabaseRoleOperation {
260+
/// `RENAME TO <new_name>`
261+
RenameTo {
262+
/// The (optionally database-qualified) new role name.
263+
new_name: ObjectName,
264+
},
265+
/// `SET COMMENT = '<text>'`
266+
SetComment {
267+
/// The comment text.
268+
comment: String,
269+
},
270+
/// `UNSET COMMENT`
271+
UnsetComment,
272+
}
273+
274+
impl fmt::Display for AlterDatabaseRoleOperation {
275+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
276+
match self {
277+
AlterDatabaseRoleOperation::RenameTo { new_name } => {
278+
write!(f, "RENAME TO {new_name}")
279+
}
280+
AlterDatabaseRoleOperation::SetComment { comment } => {
281+
write!(f, "SET COMMENT = '{}'", comment.replace('\'', "''"))
282+
}
283+
AlterDatabaseRoleOperation::UnsetComment => write!(f, "UNSET COMMENT"),
284+
}
285+
}
286+
}
287+
252288
/// A `USE` (`Statement::Use`) operation
253289
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
254290
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]

src/ast/mod.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ pub use self::data_type::{
5858
ExactNumberInfo, IntervalFields, StructBracketKind, TimezoneInfo,
5959
};
6060
pub use self::dcl::{
61-
AlterRoleOperation, CreateRole, Grant, ResetConfig, Revoke, RoleOption, SecondaryRoles,
62-
SetConfigValue, Use,
61+
AlterDatabaseRoleOperation, AlterRoleOperation, CreateRole, Grant, ResetConfig, Revoke,
62+
RoleOption, SecondaryRoles, SetConfigValue, Use,
6363
};
6464
pub use self::ddl::{
6565
Alignment, AlterCollation, AlterCollationOperation, AlterColumnOperation, AlterConnectorOwner,
@@ -4210,6 +4210,20 @@ pub enum Statement {
42104210
operation: AlterRoleOperation,
42114211
},
42124212
/// ```sql
4213+
/// ALTER DATABASE ROLE [IF EXISTS] <name>
4214+
/// { RENAME TO <new_name> | SET COMMENT = '...' | UNSET COMMENT }
4215+
/// ```
4216+
/// Snowflake database role (scoped to a database, distinct from an
4217+
/// account-level role).
4218+
AlterDatabaseRole {
4219+
/// `true` when `IF EXISTS` was specified.
4220+
if_exists: bool,
4221+
/// The (optionally database-qualified) role name being altered.
4222+
name: ObjectName,
4223+
/// Operation to perform on the database role.
4224+
operation: AlterDatabaseRoleOperation,
4225+
},
4226+
/// ```sql
42134227
/// ALTER POLICY <NAME> ON <TABLE NAME> [<OPERATION>]
42144228
/// ```
42154229
/// (Postgresql-specific)
@@ -7197,6 +7211,17 @@ impl fmt::Display for Statement {
71977211
Statement::AlterRole { name, operation } => {
71987212
write!(f, "ALTER ROLE {name} {operation}")
71997213
}
7214+
Statement::AlterDatabaseRole {
7215+
if_exists,
7216+
name,
7217+
operation,
7218+
} => {
7219+
write!(
7220+
f,
7221+
"ALTER DATABASE ROLE {if_exists}{name} {operation}",
7222+
if_exists = if *if_exists { "IF EXISTS " } else { "" },
7223+
)
7224+
}
72007225
Statement::AlterPolicy(alter_policy) => write!(f, "{alter_policy}"),
72017226
Statement::AlterConnector {
72027227
name,

src/ast/spans.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ impl Spanned for Statement {
435435
Statement::AlterOperatorFamily { .. } => Span::empty(),
436436
Statement::AlterOperatorClass { .. } => Span::empty(),
437437
Statement::AlterRole { .. } => Span::empty(),
438+
Statement::AlterDatabaseRole { .. } => Span::empty(),
438439
Statement::AlterSession { .. } => Span::empty(),
439440
Statement::AttachDatabase { .. } => Span::empty(),
440441
Statement::AttachDuckDBDatabase { .. } => Span::empty(),

src/dialect/snowflake.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ use crate::ast::helpers::stmt_data_loading::{
2828
};
2929
use crate::ast::{
3030
AlterExternalVolumeOperation, AlterFileFormatOperation, AlterMaskingPolicyOperation,
31-
AlterNetworkRuleOperation, AlterRoleOperation, AlterSnowflakeSecretOperation,
31+
AlterDatabaseRoleOperation, AlterNetworkRuleOperation, AlterRoleOperation,
32+
AlterSnowflakeSecretOperation,
3233
AlterProcedure, AlterProcedureOperation, AlterStageOperation, AlterTable, AlterTableOperation,
3334
AlterTableType, AlterTagOperation, CatalogRestAuthentication, CatalogRestConfig, CatalogSource,
3435
CatalogSyncNamespaceMode, CatalogTableFormat, ColumnOption, ColumnPolicy, ColumnPolicyProperty,
@@ -372,6 +373,16 @@ impl Dialect for SnowflakeDialect {
372373
return Some(parse_alter_tag(parser));
373374
}
374375

376+
// ALTER DATABASE ROLE [IF EXISTS] <name> { RENAME TO <new> |
377+
// SET COMMENT = '…' | UNSET COMMENT } — must win before the ALTER
378+
// DATABASE tag form below, which unconditionally consumes ALTER DATABASE.
379+
if let Ok(Some(stmt)) = parser.maybe_parse(|p| {
380+
p.expect_keywords(&[Keyword::ALTER, Keyword::DATABASE, Keyword::ROLE])?;
381+
parse_alter_database_role(p)
382+
}) {
383+
return Some(Ok(stmt));
384+
}
385+
375386
if parser.parse_keywords(&[Keyword::ALTER, Keyword::DATABASE]) {
376387
// ALTER DATABASE <name> { SET TAG | UNSET TAG }
377388
return Some(parse_alter_object_set_tags(parser, ObjectType::Database));
@@ -3838,6 +3849,38 @@ fn parse_alter_role(parser: &mut Parser) -> Result<Statement, ParserError> {
38383849
Ok(Statement::AlterRole { name, operation })
38393850
}
38403851

3852+
/// Parse the Snowflake `ALTER DATABASE ROLE [IF EXISTS] <name>` forms:
3853+
/// `RENAME TO <new_name>`, `SET COMMENT = '<text>'`, `UNSET COMMENT`. Both the
3854+
/// altered name and a rename target may be database-qualified. The leading
3855+
/// `ALTER DATABASE ROLE` keywords are consumed by the caller. Any other trailing
3856+
/// shape returns an error so the enclosing `maybe_parse` backtracks.
3857+
fn parse_alter_database_role(parser: &mut Parser) -> Result<Statement, ParserError> {
3858+
let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
3859+
let name = parser.parse_object_name(false)?;
3860+
3861+
let operation = if parser.parse_keyword(Keyword::RENAME) {
3862+
parser.expect_keyword_is(Keyword::TO)?;
3863+
let new_name = parser.parse_object_name(false)?;
3864+
AlterDatabaseRoleOperation::RenameTo { new_name }
3865+
} else if parser.parse_keyword(Keyword::SET) {
3866+
parser.expect_keyword_is(Keyword::COMMENT)?;
3867+
parser.expect_token(&Token::Eq)?;
3868+
let comment = parser.parse_literal_string()?;
3869+
AlterDatabaseRoleOperation::SetComment { comment }
3870+
} else if parser.parse_keyword(Keyword::UNSET) {
3871+
parser.expect_keyword_is(Keyword::COMMENT)?;
3872+
AlterDatabaseRoleOperation::UnsetComment
3873+
} else {
3874+
return parser.expected("RENAME, SET COMMENT, or UNSET COMMENT", parser.peek_token());
3875+
};
3876+
3877+
Ok(Statement::AlterDatabaseRole {
3878+
if_exists,
3879+
name,
3880+
operation,
3881+
})
3882+
}
3883+
38413884
/// Parse `SHOW [TERSE] TAGS [ ... ]`
38423885
fn parse_show_tags(terse: bool, parser: &mut Parser) -> Result<Statement, ParserError> {
38433886
let show_options = parser.parse_show_stmt_options()?;

tests/sqlparser_snowflake.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9844,6 +9844,99 @@ fn parse_sf_alter_role_if_exists_is_accepted() {
98449844
);
98459845
}
98469846

9847+
#[test]
9848+
fn parse_sf_alter_database_role_forms() {
9849+
// RENAME TO / SET COMMENT / UNSET COMMENT round-trip through Display, with
9850+
// both the altered name and the rename target optionally database-qualified.
9851+
match snowflake().verified_stmt("ALTER DATABASE ROLE db.r RENAME TO db.r2") {
9852+
Statement::AlterDatabaseRole {
9853+
if_exists,
9854+
name,
9855+
operation,
9856+
} => {
9857+
assert!(!if_exists);
9858+
assert_eq!(name, ObjectName::from(vec![Ident::new("db"), Ident::new("r")]));
9859+
assert_eq!(
9860+
operation,
9861+
AlterDatabaseRoleOperation::RenameTo {
9862+
new_name: ObjectName::from(vec![Ident::new("db"), Ident::new("r2")])
9863+
}
9864+
);
9865+
}
9866+
_ => unreachable!(),
9867+
}
9868+
9869+
match snowflake().verified_stmt("ALTER DATABASE ROLE r RENAME TO r2") {
9870+
Statement::AlterDatabaseRole { name, operation, .. } => {
9871+
assert_eq!(name, ObjectName::from(vec![Ident::new("r")]));
9872+
assert_eq!(
9873+
operation,
9874+
AlterDatabaseRoleOperation::RenameTo {
9875+
new_name: ObjectName::from(vec![Ident::new("r2")])
9876+
}
9877+
);
9878+
}
9879+
_ => unreachable!(),
9880+
}
9881+
9882+
match snowflake().verified_stmt("ALTER DATABASE ROLE db.r SET COMMENT = 'hello'") {
9883+
Statement::AlterDatabaseRole { operation, .. } => {
9884+
assert_eq!(
9885+
operation,
9886+
AlterDatabaseRoleOperation::SetComment {
9887+
comment: "hello".to_string()
9888+
}
9889+
);
9890+
}
9891+
_ => unreachable!(),
9892+
}
9893+
9894+
match snowflake().verified_stmt("ALTER DATABASE ROLE db.r UNSET COMMENT") {
9895+
Statement::AlterDatabaseRole { operation, .. } => {
9896+
assert_eq!(operation, AlterDatabaseRoleOperation::UnsetComment);
9897+
}
9898+
_ => unreachable!(),
9899+
}
9900+
}
9901+
9902+
#[test]
9903+
fn parse_sf_alter_database_role_if_exists() {
9904+
// `IF EXISTS` is carried on the AST node and round-trips through Display.
9905+
match snowflake().verified_stmt("ALTER DATABASE ROLE IF EXISTS db.r RENAME TO db.r2") {
9906+
Statement::AlterDatabaseRole {
9907+
if_exists,
9908+
operation,
9909+
..
9910+
} => {
9911+
assert!(if_exists);
9912+
assert_eq!(
9913+
operation,
9914+
AlterDatabaseRoleOperation::RenameTo {
9915+
new_name: ObjectName::from(vec![Ident::new("db"), Ident::new("r2")])
9916+
}
9917+
);
9918+
}
9919+
_ => unreachable!(),
9920+
}
9921+
}
9922+
9923+
#[test]
9924+
fn parse_sf_alter_database_role_set_tag_still_intercepted() {
9925+
// The ALTER DATABASE ROLE interceptor only claims RENAME/COMMENT; the tag
9926+
// form falls through to the ALTER DATABASE tag interceptor unaffected.
9927+
match snowflake().verified_stmt("ALTER DATABASE db SET TAG t1='v1'") {
9928+
Statement::SetTags {
9929+
object_type,
9930+
unset,
9931+
..
9932+
} => {
9933+
assert_eq!(object_type, ObjectType::Database);
9934+
assert!(!unset);
9935+
}
9936+
_ => unreachable!(),
9937+
}
9938+
}
9939+
98479940
#[test]
98489941
fn parse_sf_alter_role_set_tag_still_intercepted() {
98499942
// The tag interceptor still wins for the SET/UNSET TAG forms — they become

0 commit comments

Comments
 (0)