From 87b9e03de9a5afc97ac9b88305f27404d1eb46ef Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Thu, 1 May 2025 17:59:47 -0400 Subject: [PATCH 1/5] Add support for `DENY` statements --- src/ast/mod.rs | 36 +++++++++++++++++++++++++++++++++ src/ast/spans.rs | 1 + src/keywords.rs | 1 + src/parser/mod.rs | 42 +++++++++++++++++++++++++++++++++++---- tests/sqlparser_common.rs | 24 ++++++++++++++++++++++ 5 files changed, 100 insertions(+), 4 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d65889810..78c72307a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3794,6 +3794,10 @@ pub enum Statement { granted_by: Option, }, /// ```sql + /// DENY privileges ON object TO grantees + /// ``` + Deny(DenyStatement), + /// ```sql /// REVOKE privileges ON objects FROM grantees /// ``` Revoke { @@ -5424,6 +5428,7 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::Deny(s) => write!(f, "{s}"), Statement::Revoke { privileges, objects, @@ -6674,6 +6679,37 @@ impl fmt::Display for GrantObjects { } } +/// A `DENY` statement +/// +/// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/deny-transact-sql) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct DenyStatement { + pub privileges: Privileges, + pub objects: GrantObjects, + pub grantees: Vec, + pub granted_by: Option, + pub cascade: Option, +} + +impl fmt::Display for DenyStatement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "DENY {}", self.privileges)?; + write!(f, " ON {}", self.objects)?; + if !self.grantees.is_empty() { + write!(f, " TO {}", display_comma_separated(&self.grantees))?; + } + if let Some(cascade) = &self.cascade { + write!(f, " {cascade}")?; + } + if let Some(granted_by) = &self.granted_by { + write!(f, " AS {granted_by}")?; + } + Ok(()) + } +} + /// SQL assignment `foo = expr` as used in SQLUpdate #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 28d479f30..ab27b30d9 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -486,6 +486,7 @@ impl Spanned for Statement { Statement::CreateStage { .. } => Span::empty(), Statement::Assert { .. } => Span::empty(), Statement::Grant { .. } => Span::empty(), + Statement::Deny { .. } => Span::empty(), Statement::Revoke { .. } => Span::empty(), Statement::Deallocate { .. } => Span::empty(), Statement::Execute { .. } => Span::empty(), diff --git a/src/keywords.rs b/src/keywords.rs index 15a6f91ad..950a89a4c 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -273,6 +273,7 @@ define_keywords!( DELIMITER, DELTA, DENSE_RANK, + DENY, DEREF, DESC, DESCRIBE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 03ea91faf..580d725db 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -575,6 +575,10 @@ impl<'a> Parser<'a> { Keyword::SHOW => self.parse_show(), Keyword::USE => self.parse_use(), Keyword::GRANT => self.parse_grant(), + Keyword::DENY => { + self.prev_token(); + self.parse_deny() + } Keyword::REVOKE => self.parse_revoke(), Keyword::START => self.parse_start_transaction(), Keyword::BEGIN => self.parse_begin(), @@ -12987,7 +12991,7 @@ impl<'a> Parser<'a> { /// Parse a GRANT statement. pub fn parse_grant(&mut self) -> Result { - let (privileges, objects) = self.parse_grant_revoke_privileges_objects()?; + let (privileges, objects) = self.parse_grant_deny_revoke_privileges_objects()?; self.expect_keyword_is(Keyword::TO)?; let grantees = self.parse_grantees()?; @@ -13066,7 +13070,7 @@ impl<'a> Parser<'a> { Ok(values) } - pub fn parse_grant_revoke_privileges_objects( + pub fn parse_grant_deny_revoke_privileges_objects( &mut self, ) -> Result<(Privileges, Option), ParserError> { let privileges = if self.parse_keyword(Keyword::ALL) { @@ -13116,7 +13120,6 @@ impl<'a> Parser<'a> { let object_type = self.parse_one_of_keywords(&[ Keyword::SEQUENCE, Keyword::DATABASE, - Keyword::DATABASE, Keyword::SCHEMA, Keyword::TABLE, Keyword::VIEW, @@ -13409,9 +13412,40 @@ impl<'a> Parser<'a> { } } + /// Parse [`Statement::Deny`] + pub fn parse_deny(&mut self) -> Result { + self.expect_keyword(Keyword::DENY)?; + + let (privileges, objects) = self.parse_grant_deny_revoke_privileges_objects()?; + let objects = match objects { + Some(o) => o, + None => { + return parser_err!( + "DENY statements must specify an object", + self.peek_token().span.start + ) + } + }; + + self.expect_keyword_is(Keyword::TO)?; + let grantees = self.parse_grantees()?; + let cascade = self.parse_cascade_option(); + let granted_by = self + .parse_keywords(&[Keyword::AS]) + .then(|| self.parse_identifier().unwrap()); + + Ok(Statement::Deny(DenyStatement { + privileges, + objects, + grantees, + cascade, + granted_by, + })) + } + /// Parse a REVOKE statement pub fn parse_revoke(&mut self) -> Result { - let (privileges, objects) = self.parse_grant_revoke_privileges_objects()?; + let (privileges, objects) = self.parse_grant_deny_revoke_privileges_objects()?; self.expect_keyword_is(Keyword::FROM)?; let grantees = self.parse_grantees()?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index fa2346c2c..30ed00d49 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9325,6 +9325,30 @@ fn parse_grant() { verified_stmt("GRANT SELECT ON VIEW view1 TO ROLE role1"); } +#[test] +fn parse_deny() { + let sql = "DENY INSERT, DELETE ON users TO analyst CASCADE AS admin"; + match verified_stmt(sql) { + Statement::Deny(deny) => { + assert_eq!( + Privileges::Actions(vec![Action::Insert { columns: None }, Action::Delete]), + deny.privileges + ); + assert_eq!( + &GrantObjects::Tables(vec![ObjectName::from(vec![Ident::new("users")])]), + &deny.objects + ); + assert_eq_vec(&["analyst"], &deny.grantees); + assert_eq!(Some(CascadeOption::Cascade), deny.cascade); + assert_eq!(Some(Ident::from("admin")), deny.granted_by); + } + _ => unreachable!(), + } + + verified_stmt("DENY SELECT, INSERT, UPDATE, DELETE ON db1.sc1 TO role1, role2"); + verified_stmt("DENY ALL ON db1.sc1 TO role1"); +} + #[test] fn test_revoke() { let sql = "REVOKE ALL PRIVILEGES ON users, auth FROM analyst"; From 0eaee16f90f09bf033bf29edc7b777eb3738a131 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Thu, 1 May 2025 18:24:34 -0400 Subject: [PATCH 2/5] Define identifier quote style for SQL Server - this is required to have common tests with identifiers with that quote style --- src/dialect/mssql.rs | 4 ++++ tests/sqlparser_common.rs | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 31e324f06..7c181e50e 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -51,6 +51,10 @@ impl Dialect for MsSqlDialect { || ch == '_' } + fn identifier_quote_style(&self, _identifier: &str) -> Option { + Some('[') + } + /// SQL Server has `CONVERT(type, value)` instead of `CONVERT(value, type)` /// fn convert_type_before_value(&self) -> bool { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 30ed00d49..65b5d9025 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9323,6 +9323,9 @@ fn parse_grant() { verified_stmt("GRANT USAGE ON WAREHOUSE wh1 TO ROLE role1"); verified_stmt("GRANT OWNERSHIP ON INTEGRATION int1 TO ROLE role1"); verified_stmt("GRANT SELECT ON VIEW view1 TO ROLE role1"); + + all_dialects_where(|d| d.identifier_quote_style("none") == Some('[')) + .verified_stmt("GRANT SELECT ON [my_table] TO [public]"); } #[test] @@ -9347,6 +9350,9 @@ fn parse_deny() { verified_stmt("DENY SELECT, INSERT, UPDATE, DELETE ON db1.sc1 TO role1, role2"); verified_stmt("DENY ALL ON db1.sc1 TO role1"); + + all_dialects_where(|d| d.identifier_quote_style("none") == Some('[')) + .verified_stmt("DENY SELECT ON [my_table] TO [public]"); } #[test] From 279732ecfd8c4718314a250e45d4228ad7913014 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Thu, 1 May 2025 18:27:58 -0400 Subject: [PATCH 3/5] Add support for `EXEC` privilege type --- src/ast/mod.rs | 9 +++++++++ src/parser/mod.rs | 3 +++ tests/sqlparser_common.rs | 2 ++ 3 files changed, 14 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 78c72307a..badcf528a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6192,6 +6192,9 @@ pub enum Action { }, Delete, EvolveSchema, + Exec { + obj_type: Option, + }, Execute { obj_type: Option, }, @@ -6258,6 +6261,12 @@ impl fmt::Display for Action { Action::DatabaseRole { role } => write!(f, "DATABASE ROLE {role}")?, Action::Delete => f.write_str("DELETE")?, Action::EvolveSchema => f.write_str("EVOLVE SCHEMA")?, + Action::Exec { obj_type } => { + f.write_str("EXEC")?; + if let Some(obj_type) = obj_type { + write!(f, " {obj_type}")? + } + } Action::Execute { obj_type } => { f.write_str("EXECUTE")?; if let Some(obj_type) = obj_type { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 580d725db..e5eb0def3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13214,6 +13214,9 @@ impl<'a> Parser<'a> { Ok(Action::Create { obj_type }) } else if self.parse_keyword(Keyword::DELETE) { Ok(Action::Delete) + } else if self.parse_keyword(Keyword::EXEC) { + let obj_type = self.maybe_parse_action_execute_obj_type(); + Ok(Action::Exec { obj_type }) } else if self.parse_keyword(Keyword::EXECUTE) { let obj_type = self.maybe_parse_action_execute_obj_type(); Ok(Action::Execute { obj_type }) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 65b5d9025..d77aafc02 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9323,6 +9323,7 @@ fn parse_grant() { verified_stmt("GRANT USAGE ON WAREHOUSE wh1 TO ROLE role1"); verified_stmt("GRANT OWNERSHIP ON INTEGRATION int1 TO ROLE role1"); verified_stmt("GRANT SELECT ON VIEW view1 TO ROLE role1"); + verified_stmt("GRANT EXEC ON my_sp TO runner"); all_dialects_where(|d| d.identifier_quote_style("none") == Some('[')) .verified_stmt("GRANT SELECT ON [my_table] TO [public]"); @@ -9350,6 +9351,7 @@ fn parse_deny() { verified_stmt("DENY SELECT, INSERT, UPDATE, DELETE ON db1.sc1 TO role1, role2"); verified_stmt("DENY ALL ON db1.sc1 TO role1"); + verified_stmt("DENY EXEC ON my_sp TO runner"); all_dialects_where(|d| d.identifier_quote_style("none") == Some('[')) .verified_stmt("DENY SELECT ON [my_table] TO [public]"); From 681cfe1c070d3cd0788ce9af754854d81c76677c Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Fri, 2 May 2025 10:48:41 -0400 Subject: [PATCH 4/5] Allow `GRANT`/`DENY` for public roles for SQL Server --- src/parser/mod.rs | 8 ++++++-- tests/sqlparser_mssql.rs | 10 ++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e5eb0def3..79e8e16bd 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13024,14 +13024,18 @@ impl<'a> Parser<'a> { GranteesType::Share } else if self.parse_keyword(Keyword::GROUP) { GranteesType::Group - } else if self.parse_keyword(Keyword::PUBLIC) { - GranteesType::Public } else if self.parse_keywords(&[Keyword::DATABASE, Keyword::ROLE]) { GranteesType::DatabaseRole } else if self.parse_keywords(&[Keyword::APPLICATION, Keyword::ROLE]) { GranteesType::ApplicationRole } else if self.parse_keyword(Keyword::APPLICATION) { GranteesType::Application + } else if self.peek_keyword(Keyword::PUBLIC) { + if dialect_of!(self is MsSqlDialect) { + grantee_type + } else { + GranteesType::Public + } } else { grantee_type // keep from previous iteraton, if not specified }; diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index ef6103474..f1a3c1d87 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2160,3 +2160,13 @@ fn parse_print() { let _ = ms().verified_stmt("PRINT N'Hello, ⛄️!'"); let _ = ms().verified_stmt("PRINT @my_variable"); } + +#[test] +fn parse_mssql_grant() { + ms().verified_stmt("GRANT SELECT ON my_table TO public, db_admin"); +} + +#[test] +fn parse_mssql_deny() { + ms().verified_stmt("DENY SELECT ON my_table TO public, db_admin"); +} From e46727970a20eb3b12b80c6bf84cc96f024be80f Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Fri, 2 May 2025 13:37:20 -0400 Subject: [PATCH 5/5] Add support for `GRANT .. AS role` syntax --- src/ast/mod.rs | 5 +++++ src/parser/mod.rs | 17 ++++++++++++++--- tests/sqlparser_common.rs | 1 + tests/sqlparser_mysql.rs | 1 + 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index badcf528a..00267ed67 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3791,6 +3791,7 @@ pub enum Statement { objects: Option, grantees: Vec, with_grant_option: bool, + as_grantor: Option, granted_by: Option, }, /// ```sql @@ -5413,6 +5414,7 @@ impl fmt::Display for Statement { objects, grantees, with_grant_option, + as_grantor, granted_by, } => { write!(f, "GRANT {privileges} ")?; @@ -5423,6 +5425,9 @@ impl fmt::Display for Statement { if *with_grant_option { write!(f, " WITH GRANT OPTION")?; } + if let Some(grantor) = as_grantor { + write!(f, " AS {grantor}")?; + } if let Some(grantor) = granted_by { write!(f, " GRANTED BY {grantor}")?; } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 79e8e16bd..886af020e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12999,15 +12999,26 @@ impl<'a> Parser<'a> { let with_grant_option = self.parse_keywords(&[Keyword::WITH, Keyword::GRANT, Keyword::OPTION]); - let granted_by = self - .parse_keywords(&[Keyword::GRANTED, Keyword::BY]) - .then(|| self.parse_identifier().unwrap()); + let as_grantor = if self.peek_keyword(Keyword::AS) { + self.parse_keywords(&[Keyword::AS]) + .then(|| self.parse_identifier().unwrap()) + } else { + None + }; + + let granted_by = if self.peek_keywords(&[Keyword::GRANTED, Keyword::BY]) { + self.parse_keywords(&[Keyword::GRANTED, Keyword::BY]) + .then(|| self.parse_identifier().unwrap()) + } else { + None + }; Ok(Statement::Grant { privileges, objects, grantees, with_grant_option, + as_grantor, granted_by, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index d77aafc02..13bea0817 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9324,6 +9324,7 @@ fn parse_grant() { verified_stmt("GRANT OWNERSHIP ON INTEGRATION int1 TO ROLE role1"); verified_stmt("GRANT SELECT ON VIEW view1 TO ROLE role1"); verified_stmt("GRANT EXEC ON my_sp TO runner"); + verified_stmt("GRANT UPDATE ON my_table TO updater_role AS dbo"); all_dialects_where(|d| d.identifier_quote_style("none") == Some('[')) .verified_stmt("GRANT SELECT ON [my_table] TO [public]"); diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index f74248b86..c7619b5e5 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3283,6 +3283,7 @@ fn parse_grant() { objects, grantees, with_grant_option, + as_grantor: _, granted_by, } = stmt {