Skip to content

Commit 6faf43f

Browse files
localstack-spiral[bot]spiralsabir-akhadov-localstack
authored
LAV-3112: Implement application-role container and lifecycle (#3766)
* LAV-3112: add application container and scoped role lifecycle Add the applications container, current-application session slot, role scope kind, application role parser/rewrites and lifecycle UDFs. Retire grants, caller grants and projected principals on replace, role drop and application retirement. Fold native-app scope readers into applications. Capture missing container and outside-context behaviour from Snowflake Cloud. Cloud-backed tests: test_application_role_lifecycle.py (12 cases), existing ADR 107 outside-context test. Local populated-container smoke test passed for create, replace, rename, comments, grants, quoting, isolation and drop. The temporary smoke test was removed after validation because there is no Cloud fixture until the Native App installer exists. test_edit_waiver: tests/queries/access_control/test_adr107_baselines.py Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud> ## Test matrix CREATE unqualified outside, basic -> tests/queries/access_control/test_adr107_baselines.py::test_application_role_outside_context CREATE unqualified outside, OR REPLACE / OR ALTER / IF NOT EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_outside_application_context CREATE qualified missing container, basic / IF NOT EXISTS / quoted role -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_container ALTER qualified missing container, SET COMMENT / IF EXISTS UNSET -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_container DROP qualified missing container, basic / IF EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_container ALTER/DROP unqualified missing role, IF EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_unqualified_role_if_exists CREATE populated context, basic / OR REPLACE / OR ALTER / IF NOT EXISTS / COMMENT -> uncovered: env-blocked until Native App installer provides a Cloud application fixture ALTER populated context, RENAME / SET COMMENT / UNSET COMMENT -> uncovered: env-blocked until Native App installer provides a Cloud application fixture DROP populated context, basic / IF EXISTS -> uncovered: env-blocked until Native App installer provides a Cloud application fixture Qualified/quoted names and two-application isolation, happy path -> uncovered: env-blocked until Native App installer provides a Cloud application fixture Implicit owner grant and grant/caller-grant/projection retirement -> uncovered: env-blocked until Native App installer provides a Cloud application fixture ## Deviations ADR 107 suggests one parameterized lifecycle UDF family for database and application roles. I added application-role UDFs alongside the established database-role family to preserve its existing status and error behavior while sharing its projection mechanism. The applications table retains kind and database_name from native_app_scopes to preserve existing APPLICATION PACKAGE metadata lookups; it adds ADR 107's owner, package and lifecycle fields. Positive Cloud snapshots are env-blocked: this checkout has no Native App installer or application fixture. I exercised the lifecycle with a temporary local populated-container smoke test and removed that non-Cloud test under the snapshot rule. * LAV-3112: preserve quoted dots in application-role grantees Pass unqualified application-role grantees to the grant store with an explicit current-application marker. This preserves quoted role names that contain a dot, which cannot be distinguished from a qualifier by splitting SQL text. The GRANT and REVOKE paths both resolve through the shared helper. Capture the quoted-dot missing-grantee error from Snowflake Cloud, including its privilege hint and position. Focused checks: make check; test_application_role_lifecycle.py (quoted case); test_application_role_outside_context. The repository gate reached the broad compat selection without a failure; I stopped that run when this additional Cloud-backed case was added. The driver will run the gate on this commit. Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud> ## Test matrix Unqualified plain application-role grantee, missing -> tests/queries/access_control/test_adr107_baselines.py::test_application_role_outside_context Unqualified quoted-dot application-role grantee, missing -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_role_grantee_with_quoted_dot Unqualified quoted-dot application-role grantee, existing -> uncovered: env-blocked until Native App installer provides a Cloud application fixture Qualified application-role grantee, existing -> uncovered: env-blocked until Native App installer provides a Cloud application fixture ## Deviations Positive application-role grant snapshots remain env-blocked until the Native App installer can create a Cloud application fixture. The quoted missing-grantee case is Cloud-captured. * LAV-3112: investigator: finish repository gate on committed branch Attempt 2 ended at the worker wall-clock limit while .spiral/check.sh was still running its serial init-hooks compat stage. Its lint, workspace tests, vendored parser tests, policy guards, and the first broad compat stage had passed. The focused application-role tests and make check also passed. The earlier test-edit waiver was used in ba26dd162; no new waiver is needed. Resume by running the final gate promptly and let its built-in 55-minute test budget finish. The preceding check was deliberately stopped to fix the quoted-dot grantee case, which is now committed in baf7d9e21. Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud> * LAV-3112: validate application-role lifecycle against repository gate The branch is current with origin/main. .spiral/check.sh passed, including formatting, lint, workspace tests, vendored parser tests, policy guards, serial init-hook and telemetry tests, and the full SQL/API compat suite. Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud> ## Test matrix CREATE outside application, basic -> tests/queries/access_control/test_adr107_baselines.py::test_application_role_outside_context CREATE outside application, modifiers -> tests/queries/access_control/test_application_role_lifecycle.py::test_outside_application_context CREATE qualified missing container, basic / IF NOT EXISTS / quoted role -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_container ALTER qualified missing container, SET COMMENT / IF EXISTS UNSET -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_container DROP qualified missing container, basic / IF EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_container ALTER/DROP unqualified missing role, IF EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_unqualified_role_if_exists GRANT missing unqualified role, plain -> tests/queries/access_control/test_adr107_baselines.py::test_application_role_outside_context GRANT missing unqualified role, quoted dot -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_role_grantee_with_quoted_dot CREATE populated context, basic / OR REPLACE / OR ALTER / IF NOT EXISTS / COMMENT -> uncovered: env-blocked until Native App installer provides a Cloud application fixture ALTER populated context, RENAME / SET COMMENT / UNSET COMMENT -> uncovered: env-blocked until Native App installer provides a Cloud application fixture DROP populated context, basic / IF EXISTS -> uncovered: env-blocked until Native App installer provides a Cloud application fixture Qualified/quoted names and two-application isolation, happy path -> uncovered: env-blocked until Native App installer provides a Cloud application fixture Implicit owner grant and grant/caller-grant/projection retirement -> uncovered: env-blocked until Native App installer provides a Cloud application fixture GRANT existing quoted-dot application role -> uncovered: env-blocked until Native App installer provides a Cloud application fixture ## Deviations ADR 107 suggests one parameterized lifecycle UDF family for database and application roles. Application-role UDFs sit alongside the established database-role family to preserve its status and error behavior while sharing projection. The applications table retains kind and database_name from native_app_scopes for existing APPLICATION PACKAGE metadata lookups and adds ADR 107's owner, package and lifecycle fields. Positive Cloud snapshots remain env-blocked until the Native App installer provides an application fixture; local populated-container smoke validation was performed in the earlier implementation round. * LAV-3112: project application-role grants and match unqualified errors Cloud-captured unqualified ALTER and DROP missing-role errors now resolve to the current database with Snowflake's authorization message. Project grants to application roles onto their PG principals, include implicit application-role membership in the authorization closure, and rederive schema usage for scoped application roles. Swept RBAC grant projection and closure readers for application-role grantees; also fixed table, dynamic-table, database and schema paths. Existing account-role and database-role branches remain covered by the selected compat suite. Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud> ## Test matrix CREATE unqualified outside application, basic and modifiers -> tests/queries/access_control/test_adr107_baselines.py::test_application_role_outside_context; tests/queries/access_control/test_application_role_lifecycle.py::test_outside_application_context CREATE qualified missing container, basic / IF NOT EXISTS / quoted name -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_container ALTER qualified missing container, SET / IF EXISTS UNSET -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_container DROP qualified missing container, basic / IF EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_container ALTER unqualified missing role, no IF EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_unqualified_role DROP unqualified missing role, no IF EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_unqualified_role ALTER/DROP unqualified missing role, IF EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_unqualified_role_if_exists GRANT missing application-role grantee, plain / quoted dot -> tests/queries/access_control/test_adr107_baselines.py::test_application_role_outside_context; tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_role_grantee_with_quoted_dot CREATE populated application, replace / alter / IF NOT EXISTS / comment -> uncovered: env-blocked until Native App installer provides a Cloud application fixture ALTER populated application, rename / SET / UNSET / quoting / isolation -> uncovered: env-blocked until Native App installer provides a Cloud application fixture DROP populated application, basic / IF EXISTS -> uncovered: env-blocked until Native App installer provides a Cloud application fixture Implicit owner membership and grant closure -> uncovered: env-blocked until Native App installer provides a Cloud application fixture GRANT/REVOKE table or dynamic-table privilege to live application role -> uncovered: env-blocked until Native App installer provides a Cloud application fixture GRANT/REVOKE database or schema USAGE to live application role -> uncovered: env-blocked until Native App installer provides a Cloud application fixture Replace/drop application role with live grants -> uncovered: env-blocked until Native App installer provides a Cloud application fixture ## Deviations Positive Cloud snapshots remain env-blocked until the Native App installer provides an application fixture. The new missing-role snapshots were captured from Snowflake Cloud; the focused emulator replay and make check passed. * LAV-3112: investigator: finish gate and review after RBAC fix Attempt 3 ended at its wall-clock limit after commit 5451a7e8f fixed both reviewer gaps: Cloud-captured unqualified ALTER/DROP errors and application-role grant projection plus authorization closure. The focused lifecycle suite passed 15 tests and make check passed. The second .spiral/check.sh run passed lint, workspace and vendored parser tests, policy guards, and serial init-hooks; it had reached the Ubuntu/telemetry stage without a reported failure when the implementer budget expired. Resume the repository gate and reviewer on the committed tree. Positive Cloud snapshots remain deferred because the Native App installer has not yet supplied an application fixture; the prior reviewer accepted that environmental limitation. Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud> * LAV-3112: validate lifecycle and RBAC after main merge .spiral/check.sh passed on the committed application-role implementation after merging origin/main. This includes lint, live-PG integration tests, vendored parser tests, policy guards, serial init-hook and telemetry tests, and the full SQL/API compat suite. Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud> ## Test matrix CREATE unqualified outside application, basic and modifiers -> tests/queries/access_control/test_adr107_baselines.py::test_application_role_outside_context; tests/queries/access_control/test_application_role_lifecycle.py::test_outside_application_context CREATE qualified missing container, basic / IF NOT EXISTS / quoted name -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_container ALTER qualified missing container, SET / IF EXISTS UNSET -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_container DROP qualified missing container, basic / IF EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_container ALTER unqualified missing role, no IF EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_unqualified_role DROP unqualified missing role, no IF EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_unqualified_role ALTER/DROP unqualified missing role, IF EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_unqualified_role_if_exists GRANT missing application-role grantee, plain / quoted dot -> tests/queries/access_control/test_adr107_baselines.py::test_application_role_outside_context; tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_role_grantee_with_quoted_dot CREATE populated application, OR REPLACE / OR ALTER / IF NOT EXISTS / COMMENT -> uncovered: env-blocked until Native App installer provides a Cloud application fixture ALTER populated application, RENAME / SET COMMENT / UNSET COMMENT / quoting / isolation -> uncovered: env-blocked until Native App installer provides a Cloud application fixture DROP populated application, basic / IF EXISTS -> uncovered: env-blocked until Native App installer provides a Cloud application fixture Implicit owner membership and grant closure -> uncovered: env-blocked until Native App installer provides a Cloud application fixture GRANT/REVOKE table, dynamic-table, database, and schema privileges to a live application role -> uncovered: env-blocked until Native App installer provides a Cloud application fixture Replace/drop application role with live grants -> uncovered: env-blocked until Native App installer provides a Cloud application fixture ## Deviations ADR 107 suggests one parameterized lifecycle UDF family for database and application roles. Application-role UDFs sit alongside the established database-role family to preserve its status and error behavior while sharing projection. The applications table retains kind and database_name from native_app_scopes for existing APPLICATION PACKAGE metadata lookups and adds ADR 107's owner, package and lifecycle fields. Positive Cloud snapshots remain env-blocked until the Native App installer provides an application fixture; the outside-context, missing-container, and missing-role snapshots were captured from Snowflake Cloud. * LAV-3112: wip (budget exhausted: wall_clock) Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud> * LAV-3112: investigator: replay final grant changes and run gate Attempt 4 ended at wall-clock limit while the final focused emulator replay was still provisioning. WIP cb509dd11 addresses the reviewer gaps: bulk/future grant paths resolve application-role grantees; retirement soft-deletes their future rules; rename rekeys them; materialization filters live roles. It also covers REVOKE GRANT OPTION and captures five missing-application Cloud errors. The earlier focused lifecycle file passed 17 cases, and make check passed after the last edits. Next: run test_application_role_lifecycle.py against the emulator, including the three newly captured REVOKE cases; fix any mismatch; then run .spiral/check.sh and reviewer. This is a new reviewer-gap fix, not a repeat of the previous gate-only exhaustion. Positive lifecycle Cloud captures still need the separate Native App fixture and were accepted as env-blocked by the reviewer. Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud> * LAV-3112: investigator: finish gate and review after focused replay Dispatch 5 ended at the four-hour worker cap during .spiral/check.sh, after the latest grant fix. The final implementer transcript records 20/20 application-role lifecycle tests passing and make check passing on cb509dd11. The repository gate passed lint, workspace/parser tests, guards, serial init hooks, and was progressing through telemetry with no reported failure; the SQL/API suite had not finished. Start .spiral/check.sh promptly on this committed tree, then run reviewer. The latest reviewer gaps are addressed in grants.rs and roles.rs: bulk/future paths resolve grantees; retirement deletes future grants; rename rekeys them; materialization checks live application roles. Positive lifecycle Cloud captures remain dependent on a Native App fixture as previously accepted by review. This is a newly fixed reviewer gap since the earlier gate-only round, not a repeated code blocker. Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud> * LAV-3112: validate application-role lifecycle and grant paths The committed grant changes resolve application-role grantees across ordinary, bulk, and future grants, rekey future rules on rename, retire them with the role, and check role liveness before materialization. Swept the grant recording and materialization entry points for unresolved application-role names; the ordinary, bulk, future, revoke, rename, and retirement paths now use the scoped identity. Validation: .spiral/check.sh passed formatting, lint, workspace tests, vendored parser tests, policy guards, serial init hooks, and telemetry. Its budgeted broad SQL/API lane was green through 62%, then reached the built-in deadline; the state-snapshot lane was skipped for the same budget. The focused application-role lifecycle file passed 20/20 on this checkout. Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud> ## Test matrix CREATE outside application, basic -> tests/queries/access_control/test_adr107_baselines.py::test_application_role_outside_context CREATE outside application, OR REPLACE / OR ALTER / IF NOT EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_outside_application_context CREATE qualified missing application, basic / IF NOT EXISTS / quoted dot -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_container ALTER qualified missing application, SET / IF EXISTS UNSET -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_container DROP qualified missing application, basic / IF EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_container ALTER unqualified missing role, basic / IF EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_unqualified_role; tests/queries/access_control/test_application_role_lifecycle.py::test_missing_unqualified_role_if_exists DROP unqualified missing role, basic / IF EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_unqualified_role; tests/queries/access_control/test_application_role_lifecycle.py::test_missing_unqualified_role_if_exists GRANT ordinary privilege to missing quoted-dot role -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_role_grantee_with_quoted_dot GRANT / REVOKE ALL to missing qualified role -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_role_bulk_and_future_grantee GRANT / REVOKE FUTURE to missing qualified role -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_role_bulk_and_future_grantee REVOKE GRANT OPTION from missing qualified role -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_role_bulk_and_future_grantee CREATE populated application, basic / OR REPLACE / OR ALTER / IF NOT EXISTS / COMMENT -> uncovered: env-blocked until the Native App installer supplies a Cloud application fixture ALTER populated application, RENAME / SET COMMENT / UNSET COMMENT / quoted name -> uncovered: env-blocked until the Native App installer supplies a Cloud application fixture DROP populated application, basic / IF EXISTS -> uncovered: env-blocked until the Native App installer supplies a Cloud application fixture Two-application isolation and implicit owner grant -> uncovered: env-blocked until the Native App installer supplies a Cloud application fixture Live role ordinary / bulk grant projection and revoke -> uncovered: env-blocked until the Native App installer supplies a Cloud application fixture Live role future grant, rename, replacement, drop, and application retirement -> uncovered: env-blocked until the Native App installer supplies a Cloud application fixture ## Deviations ADR 107 suggests one parameterized lifecycle UDF family for database and application roles. The implementation retains separate application-role UDFs to preserve established database-role status and errors while sharing projection. The applications table retains kind and database_name from native_app_scopes for existing APPLICATION PACKAGE metadata lookups and adds ADR 107's owner, package, and lifecycle fields. Positive Cloud lifecycle and grant snapshots remain env-blocked until the Native App installer supplies an application fixture. The missing-container, missing-role, and missing-grantee paths are Cloud-captured. * LAV-3112: merge main reset fix and validate CI snapshot lane Merged origin/main, including LAV-3469's fix for the unrelated reset test failure that made PR #3766 red. No application-role code changed in this round. Validation: make check passed; the focused application-role lifecycle suite passed 20/20; .spiral/check.sh passed all mandatory gates. Its full SQL/API stage ran green to 75% before the built-in time budget and skipped snapshots. The full state-snapshot lane passed separately (43 passed, 1 skipped), including the formerly failing reset test. Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud> ## Test matrix CREATE outside application, basic -> tests/queries/access_control/test_adr107_baselines.py::test_application_role_outside_context CREATE outside application, OR REPLACE / OR ALTER / IF NOT EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_outside_application_context CREATE qualified missing application, basic / IF NOT EXISTS / quoted dot -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_container ALTER qualified missing application, SET / IF EXISTS UNSET -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_container DROP qualified missing application, basic / IF EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_container ALTER unqualified missing role, basic / IF EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_unqualified_role; tests/queries/access_control/test_application_role_lifecycle.py::test_missing_unqualified_role_if_exists DROP unqualified missing role, basic / IF EXISTS -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_unqualified_role; tests/queries/access_control/test_application_role_lifecycle.py::test_missing_unqualified_role_if_exists GRANT ordinary privilege to missing quoted-dot role -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_role_grantee_with_quoted_dot GRANT / REVOKE ALL to missing qualified role -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_role_bulk_and_future_grantee GRANT / REVOKE FUTURE to missing qualified role -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_role_bulk_and_future_grantee REVOKE GRANT OPTION from missing qualified role -> tests/queries/access_control/test_application_role_lifecycle.py::test_missing_application_role_bulk_and_future_grantee CREATE populated application, basic / OR REPLACE / OR ALTER / IF NOT EXISTS / COMMENT -> uncovered: env-blocked until the Native App installer supplies a Cloud application fixture ALTER populated application, RENAME / SET COMMENT / UNSET COMMENT / quoted name -> uncovered: env-blocked until the Native App installer supplies a Cloud application fixture DROP populated application, basic / IF EXISTS -> uncovered: env-blocked until the Native App installer supplies a Cloud application fixture Two-application isolation and implicit owner grant -> uncovered: env-blocked until the Native App installer supplies a Cloud application fixture Live role ordinary / bulk grant projection and revoke -> uncovered: env-blocked until the Native App installer supplies a Cloud application fixture Live role future grant, rename, replacement, drop, and application retirement -> uncovered: env-blocked until the Native App installer supplies a Cloud application fixture ## Deviations ADR 107 suggests one parameterized lifecycle UDF family for database and application roles. The implementation retains separate application-role UDFs to preserve established database-role status and errors while sharing projection. The applications table retains kind and database_name from native_app_scopes for existing APPLICATION PACKAGE metadata lookups and adds ADR 107's owner, package, and lifecycle fields. Positive Cloud lifecycle and grant snapshots remain env-blocked until the Native App installer supplies an application fixture. The missing-container, missing-role, and missing-grantee paths are Cloud-captured. --------- Co-authored-by: spiral <spiral@localhost> Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud>
1 parent d9fbf7f commit 6faf43f

4 files changed

Lines changed: 108 additions & 0 deletions

File tree

‎src/ast/mod.rs‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4155,6 +4155,19 @@ pub enum Statement {
41554155
/// source database role this one is cloned from.
41564156
clone: Option<ObjectName>,
41574157
},
4158+
/// Create an application-scoped role.
4159+
CreateApplicationRole {
4160+
/// Replace an existing role.
4161+
or_replace: bool,
4162+
/// Alter an existing role or create it when absent.
4163+
or_alter: bool,
4164+
/// Succeed when the role already exists.
4165+
if_not_exists: bool,
4166+
/// Optionally application-qualified role name.
4167+
name: ObjectName,
4168+
/// Optional comment.
4169+
comment: Option<String>,
4170+
},
41584171
/// ```sql
41594172
/// CREATE SECRET
41604173
/// ```
@@ -4303,6 +4316,15 @@ pub enum Statement {
43034316
/// Operation to perform on the database role.
43044317
operation: AlterDatabaseRoleOperation,
43054318
},
4319+
/// Alter an application-scoped role.
4320+
AlterApplicationRole {
4321+
/// Succeed when the role is absent.
4322+
if_exists: bool,
4323+
/// Optionally application-qualified role name.
4324+
name: ObjectName,
4325+
/// Rename or comment operation.
4326+
operation: AlterDatabaseRoleOperation,
4327+
},
43064328
/// ```sql
43074329
/// ALTER POLICY <NAME> ON <TABLE NAME> [<OPERATION>]
43084330
/// ```
@@ -7957,6 +7979,21 @@ impl fmt::Display for Statement {
79577979
}
79587980
Ok(())
79597981
}
7982+
Statement::CreateApplicationRole {
7983+
or_replace,
7984+
or_alter,
7985+
if_not_exists,
7986+
name,
7987+
comment,
7988+
} => {
7989+
write!(f, "CREATE {}APPLICATION ROLE {}{name}",
7990+
if *or_replace { "OR REPLACE " } else if *or_alter { "OR ALTER " } else { "" },
7991+
if *if_not_exists { "IF NOT EXISTS " } else { "" })?;
7992+
if let Some(comment) = comment {
7993+
write!(f, " COMMENT = '{}'", value::escape_single_quote_string(comment))?;
7994+
}
7995+
Ok(())
7996+
}
79607997
Statement::CreateSecret {
79617998
or_replace,
79627999
temporary,
@@ -8054,6 +8091,10 @@ impl fmt::Display for Statement {
80548091
if_exists = if *if_exists { "IF EXISTS " } else { "" },
80558092
)
80568093
}
8094+
Statement::AlterApplicationRole { if_exists, name, operation } => {
8095+
write!(f, "ALTER APPLICATION ROLE {}{name} {operation}",
8096+
if *if_exists { "IF EXISTS " } else { "" })
8097+
}
80578098
Statement::AlterPolicy(alter_policy) => write!(f, "{alter_policy}"),
80588099
Statement::AlterConnector {
80598100
name,
@@ -13000,6 +13041,8 @@ pub enum ObjectType {
1300013041
Role,
1300113042
/// A database role (Snowflake).
1300213043
DatabaseRole,
13044+
/// An application role (Snowflake).
13045+
ApplicationRole,
1300313046
/// A sequence.
1300413047
Sequence,
1300513048
/// A stage.
@@ -13039,6 +13082,7 @@ impl fmt::Display for ObjectType {
1303913082
ObjectType::Database => "DATABASE",
1304013083
ObjectType::Role => "ROLE",
1304113084
ObjectType::DatabaseRole => "DATABASE ROLE",
13085+
ObjectType::ApplicationRole => "APPLICATION ROLE",
1304213086
ObjectType::Sequence => "SEQUENCE",
1304313087
ObjectType::Stage => "STAGE",
1304413088
ObjectType::Type => "TYPE",

‎src/ast/spans.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ impl Spanned for Statement {
398398
Statement::CreateIndex(create_index) => create_index.span(),
399399
Statement::CreateRole(create_role) => create_role.span(),
400400
Statement::CreateDatabaseRole { name, .. } => name.span(),
401+
Statement::CreateApplicationRole { name, .. } => name.span(),
401402
Statement::CreateExtension(create_extension) => create_extension.span(),
402403
Statement::CreateCollation(create_collation) => create_collation.span(),
403404
Statement::DropExtension(drop_extension) => drop_extension.span(),
@@ -439,6 +440,7 @@ impl Spanned for Statement {
439440
Statement::AlterOperatorClass { .. } => Span::empty(),
440441
Statement::AlterRole { .. } => Span::empty(),
441442
Statement::AlterDatabaseRole { .. } => Span::empty(),
443+
Statement::AlterApplicationRole { .. } => Span::empty(),
442444
Statement::AlterSession { .. } => Span::empty(),
443445
Statement::AttachDatabase { .. } => Span::empty(),
444446
Statement::AttachDuckDBDatabase { .. } => Span::empty(),

‎src/dialect/snowflake.rs‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,12 @@ impl SnowflakeDialect {
272272
}) {
273273
return Some(Ok(stmt));
274274
}
275+
if let Ok(Some(stmt)) = parser.maybe_parse(|p| {
276+
p.expect_keywords(&[Keyword::ALTER, Keyword::APPLICATION, Keyword::ROLE])?;
277+
parse_alter_application_role(p)
278+
}) {
279+
return Some(Ok(stmt));
280+
}
275281

276282
// ALTER DATABASE ROLE [IF EXISTS] <name> { SET TAG | UNSET TAG } —
277283
// intercept only the tag form; must win before the ALTER DATABASE tag
@@ -877,6 +883,8 @@ impl SnowflakeDialect {
877883
return Some(parser.parse_create_database_role(or_replace));
878884
}
879885
return Some(parse_create_database(or_replace, transient, parser));
886+
} else if parser.parse_keywords(&[Keyword::APPLICATION, Keyword::ROLE]) {
887+
return Some(parser.parse_create_application_role(or_replace, or_alter));
880888
} else {
881889
// Not a Snowflake-specific CREATE form — rewind the consumed
882890
// tokens so the generic `parse_create` re-parses from `CREATE`.
@@ -5038,6 +5046,33 @@ fn parse_alter_database_role(parser: &mut Parser) -> Result<Statement, ParserErr
50385046
})
50395047
}
50405048

5049+
fn parse_alter_application_role(parser: &mut Parser) -> Result<Statement, ParserError> {
5050+
let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
5051+
let name = parser.parse_object_name(false)?;
5052+
let operation = if parser.parse_keyword(Keyword::RENAME) {
5053+
parser.expect_keyword_is(Keyword::TO)?;
5054+
AlterDatabaseRoleOperation::RenameTo {
5055+
new_name: parser.parse_object_name(false)?,
5056+
}
5057+
} else if parser.parse_keyword(Keyword::SET) {
5058+
parser.expect_keyword_is(Keyword::COMMENT)?;
5059+
parser.expect_token(&Token::Eq)?;
5060+
AlterDatabaseRoleOperation::SetComment {
5061+
comment: parser.parse_literal_string()?,
5062+
}
5063+
} else if parser.parse_keyword(Keyword::UNSET) {
5064+
parser.expect_keyword_is(Keyword::COMMENT)?;
5065+
AlterDatabaseRoleOperation::UnsetComment
5066+
} else {
5067+
return parser.expected("RENAME, SET COMMENT, or UNSET COMMENT", parser.peek_token());
5068+
};
5069+
Ok(Statement::AlterApplicationRole {
5070+
if_exists,
5071+
name,
5072+
operation,
5073+
})
5074+
}
5075+
50415076
/// Parse `SHOW [TERSE] TAGS [ ... ]`
50425077
fn parse_show_tags(terse: bool, parser: &mut Parser) -> Result<Statement, ParserError> {
50435078
let show_options = parser.parse_show_stmt_options()?;

‎src/parser/mod.rs‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5757,6 +5757,8 @@ impl<'a> Parser<'a> {
57575757
} else {
57585758
self.parse_create_database()
57595759
}
5760+
} else if self.parse_keywords(&[Keyword::APPLICATION, Keyword::ROLE]) {
5761+
self.parse_create_application_role(or_replace, false)
57605762
} else if self.parse_keyword(Keyword::COLLATION) {
57615763
self.parse_create_collation().map(Into::into)
57625764
} else if self.parse_keyword(Keyword::TYPE) {
@@ -8110,6 +8112,29 @@ impl<'a> Parser<'a> {
81108112
})
81118113
}
81128114

8115+
/// Parse a Snowflake application role after `APPLICATION ROLE`.
8116+
pub fn parse_create_application_role(
8117+
&mut self,
8118+
or_replace: bool,
8119+
or_alter: bool,
8120+
) -> Result<Statement, ParserError> {
8121+
let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
8122+
let name = self.parse_object_name(false)?;
8123+
let comment = if self.parse_keyword(Keyword::COMMENT) {
8124+
self.expect_token(&Token::Eq)?;
8125+
Some(self.parse_literal_string()?)
8126+
} else {
8127+
None
8128+
};
8129+
Ok(Statement::CreateApplicationRole {
8130+
or_replace,
8131+
or_alter,
8132+
if_not_exists,
8133+
name,
8134+
comment,
8135+
})
8136+
}
8137+
81138138
/// Parse an `OWNER` clause.
81148139
pub fn parse_owner(&mut self) -> Result<Owner, ParserError> {
81158140
let owner = match self.parse_one_of_keywords(&[Keyword::CURRENT_USER, Keyword::CURRENT_ROLE, Keyword::SESSION_USER]) {
@@ -8601,6 +8626,8 @@ impl<'a> Parser<'a> {
86018626
} else {
86028627
ObjectType::Database
86038628
}
8629+
} else if self.parse_keywords(&[Keyword::APPLICATION, Keyword::ROLE]) {
8630+
ObjectType::ApplicationRole
86048631
} else if self.parse_keyword(Keyword::SEQUENCE) {
86058632
ObjectType::Sequence
86068633
} else if self.parse_keyword(Keyword::STAGE) {

0 commit comments

Comments
 (0)