diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4f5fdb2eb..4efc797d1 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -892,6 +892,13 @@ impl fmt::Display for CommentObject { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Password { + Password(Expr), + NullPassword, +} + /// A top-level statement (SELECT, INSERT, CREATE, etc.) #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -1047,6 +1054,27 @@ pub enum Statement { unique: bool, if_not_exists: bool, }, + /// CREATE ROLE + CreateRole { + names: Vec, + if_not_exists: bool, + // Postgres + login: Option, + inherit: Option, + bypassrls: Option, + password: Option, + superuser: Option, + create_db: Option, + create_role: Option, + replication: Option, + connection_limit: Option, + valid_until: Option, + in_role: Vec, + role: Vec, + admin: Vec, + // MSSQL + authorization_owner: Option, + }, /// ALTER TABLE AlterTable { /// Table name @@ -1890,6 +1918,90 @@ impl fmt::Display for Statement { table_name = table_name, columns = display_separated(columns, ",") ), + Statement::CreateRole { + names, + if_not_exists, + inherit, + login, + bypassrls, + password, + create_db, + create_role, + superuser, + replication, + connection_limit, + valid_until, + in_role, + role, + admin, + authorization_owner, + } => { + write!( + f, + "CREATE ROLE {if_not_exists}{names}{superuser}{create_db}{create_role}{inherit}{login}{replication}{bypassrls}", + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + names = display_separated(names, ", "), + superuser = match *superuser { + Some(true) => " SUPERUSER", + Some(false) => " NOSUPERUSER", + None => "" + }, + create_db = match *create_db { + Some(true) => " CREATEDB", + Some(false) => " NOCREATEDB", + None => "" + }, + create_role = match *create_role { + Some(true) => " CREATEROLE", + Some(false) => " NOCREATEROLE", + None => "" + }, + inherit = match *inherit { + Some(true) => " INHERIT", + Some(false) => " NOINHERIT", + None => "" + }, + login = match *login { + Some(true) => " LOGIN", + Some(false) => " NOLOGIN", + None => "" + }, + replication = match *replication { + Some(true) => " REPLICATION", + Some(false) => " NOREPLICATION", + None => "" + }, + bypassrls = match *bypassrls { + Some(true) => " BYPASSRLS", + Some(false) => " NOBYPASSRLS", + None => "" + } + )?; + if let Some(limit) = connection_limit { + write!(f, " CONNECTION LIMIT {}", limit)?; + } + match password { + Some(Password::Password(pass)) => write!(f, " PASSWORD {}", pass), + Some(Password::NullPassword) => write!(f, " PASSWORD NULL"), + None => Ok(()), + }?; + if let Some(until) = valid_until { + write!(f, " VALID UNTIL {}", until)?; + } + if !in_role.is_empty() { + write!(f, " IN ROLE {}", display_comma_separated(in_role))?; + } + if !role.is_empty() { + write!(f, " ROLE {}", display_comma_separated(role))?; + } + if !admin.is_empty() { + write!(f, " ADMIN {}", display_comma_separated(admin))?; + } + if let Some(owner) = authorization_owner { + write!(f, " AUTHORIZATION {}", owner)?; + } + Ok(()) + } Statement::AlterTable { name, operation } => { write!(f, "ALTER TABLE {} {}", name, operation) } @@ -2603,6 +2715,7 @@ pub enum ObjectType { View, Index, Schema, + Role, } impl fmt::Display for ObjectType { @@ -2612,6 +2725,7 @@ impl fmt::Display for ObjectType { ObjectType::View => "VIEW", ObjectType::Index => "INDEX", ObjectType::Schema => "SCHEMA", + ObjectType::Role => "ROLE", }) } } diff --git a/src/keywords.rs b/src/keywords.rs index 30ec735f7..e888dc7c7 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -70,6 +70,7 @@ define_keywords!( ABSOLUTE, ACTION, ADD, + ADMIN, ALL, ALLOCATE, ALTER, @@ -105,6 +106,7 @@ define_keywords!( BOOLEAN, BOTH, BY, + BYPASSRLS, BYTEA, CACHE, CALL, @@ -152,6 +154,8 @@ define_keywords!( COVAR_POP, COVAR_SAMP, CREATE, + CREATEDB, + CREATEROLE, CROSS, CSV, CUBE, @@ -270,6 +274,7 @@ define_keywords!( IN, INDEX, INDICATOR, + INHERIT, INNER, INOUT, INPUTFORMAT, @@ -310,6 +315,7 @@ define_keywords!( LOCALTIME, LOCALTIMESTAMP, LOCATION, + LOGIN, LOWER, MANAGEDLOCATION, MATCH, @@ -339,9 +345,16 @@ define_keywords!( NEW, NEXT, NO, + NOBYPASSRLS, + NOCREATEDB, + NOCREATEROLE, + NOINHERIT, + NOLOGIN, NONE, + NOREPLICATION, NORMALIZE, NOSCAN, + NOSUPERUSER, NOT, NTH_VALUE, NTILE, @@ -377,6 +390,7 @@ define_keywords!( PARTITION, PARTITIONED, PARTITIONS, + PASSWORD, PERCENT, PERCENTILE_CONT, PERCENTILE_DISC, @@ -429,6 +443,7 @@ define_keywords!( REPAIR, REPEATABLE, REPLACE, + REPLICATION, RESTRICT, RESULT, RETURN, @@ -489,6 +504,7 @@ define_keywords!( SUCCEEDS, SUM, SUPER, + SUPERUSER, SYMMETRIC, SYNC, SYSTEM, @@ -535,6 +551,7 @@ define_keywords!( UNLOGGED, UNNEST, UNSIGNED, + UNTIL, UPDATE, UPPER, USAGE, @@ -542,6 +559,7 @@ define_keywords!( USER, USING, UUID, + VALID, VALUE, VALUES, VALUE_OF, diff --git a/src/parser.rs b/src/parser.rs index 877c47303..6c362b3e3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1866,6 +1866,8 @@ impl<'a> Parser<'a> { self.parse_create_database() } else if dialect_of!(self is HiveDialect) && self.parse_keyword(Keyword::FUNCTION) { self.parse_create_function(temporary) + } else if self.parse_keyword(Keyword::ROLE) { + self.parse_create_role() } else { self.expected("an object type after CREATE", self.peek_token()) } @@ -2044,6 +2046,207 @@ impl<'a> Parser<'a> { }) } + pub fn parse_create_role(&mut self) -> Result { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let names = self.parse_comma_separated(Parser::parse_object_name)?; + + let _ = self.parse_keyword(Keyword::WITH); + + let optional_keywords = if dialect_of!(self is MsSqlDialect) { + vec![Keyword::AUTHORIZATION] + } else if dialect_of!(self is PostgreSqlDialect) { + vec![ + Keyword::LOGIN, + Keyword::NOLOGIN, + Keyword::INHERIT, + Keyword::NOINHERIT, + Keyword::BYPASSRLS, + Keyword::NOBYPASSRLS, + Keyword::PASSWORD, + Keyword::CREATEDB, + Keyword::NOCREATEDB, + Keyword::CREATEROLE, + Keyword::NOCREATEROLE, + Keyword::SUPERUSER, + Keyword::NOSUPERUSER, + Keyword::REPLICATION, + Keyword::NOREPLICATION, + Keyword::CONNECTION, + Keyword::VALID, + Keyword::IN, + Keyword::ROLE, + Keyword::ADMIN, + Keyword::USER, + ] + } else { + vec![] + }; + + // MSSQL + let mut authorization_owner = None; + // Postgres + let mut login = None; + let mut inherit = None; + let mut bypassrls = None; + let mut password = None; + let mut create_db = None; + let mut create_role = None; + let mut superuser = None; + let mut replication = None; + let mut connection_limit = None; + let mut valid_until = None; + let mut in_role = vec![]; + let mut roles = vec![]; + let mut admin = vec![]; + + while let Some(keyword) = self.parse_one_of_keywords(&optional_keywords) { + match keyword { + Keyword::AUTHORIZATION => { + if authorization_owner.is_some() { + parser_err!("Found multiple AUTHORIZATION") + } else { + authorization_owner = Some(self.parse_object_name()?); + Ok(()) + } + } + Keyword::LOGIN | Keyword::NOLOGIN => { + if login.is_some() { + parser_err!("Found multiple LOGIN or NOLOGIN") + } else { + login = Some(keyword == Keyword::LOGIN); + Ok(()) + } + } + Keyword::INHERIT | Keyword::NOINHERIT => { + if inherit.is_some() { + parser_err!("Found multiple INHERIT or NOINHERIT") + } else { + inherit = Some(keyword == Keyword::INHERIT); + Ok(()) + } + } + Keyword::BYPASSRLS | Keyword::NOBYPASSRLS => { + if bypassrls.is_some() { + parser_err!("Found multiple BYPASSRLS or NOBYPASSRLS") + } else { + bypassrls = Some(keyword == Keyword::BYPASSRLS); + Ok(()) + } + } + Keyword::CREATEDB | Keyword::NOCREATEDB => { + if create_db.is_some() { + parser_err!("Found multiple CREATEDB or NOCREATEDB") + } else { + create_db = Some(keyword == Keyword::CREATEDB); + Ok(()) + } + } + Keyword::CREATEROLE | Keyword::NOCREATEROLE => { + if create_role.is_some() { + parser_err!("Found multiple CREATEROLE or NOCREATEROLE") + } else { + create_role = Some(keyword == Keyword::CREATEROLE); + Ok(()) + } + } + Keyword::SUPERUSER | Keyword::NOSUPERUSER => { + if superuser.is_some() { + parser_err!("Found multiple SUPERUSER or NOSUPERUSER") + } else { + superuser = Some(keyword == Keyword::SUPERUSER); + Ok(()) + } + } + Keyword::REPLICATION | Keyword::NOREPLICATION => { + if replication.is_some() { + parser_err!("Found multiple REPLICATION or NOREPLICATION") + } else { + replication = Some(keyword == Keyword::REPLICATION); + Ok(()) + } + } + Keyword::PASSWORD => { + if password.is_some() { + parser_err!("Found multiple PASSWORD") + } else { + password = if self.parse_keyword(Keyword::NULL) { + Some(Password::NullPassword) + } else { + Some(Password::Password(Expr::Value(self.parse_value()?))) + }; + Ok(()) + } + } + Keyword::CONNECTION => { + self.expect_keyword(Keyword::LIMIT)?; + if connection_limit.is_some() { + parser_err!("Found multiple CONNECTION LIMIT") + } else { + connection_limit = Some(Expr::Value(self.parse_number_value()?)); + Ok(()) + } + } + Keyword::VALID => { + self.expect_keyword(Keyword::UNTIL)?; + if valid_until.is_some() { + parser_err!("Found multiple VALID UNTIL") + } else { + valid_until = Some(Expr::Value(self.parse_value()?)); + Ok(()) + } + } + Keyword::IN => { + if self.parse_keyword(Keyword::ROLE) || self.parse_keyword(Keyword::GROUP) { + if !in_role.is_empty() { + parser_err!("Found multiple IN ROLE or IN GROUP") + } else { + in_role = self.parse_comma_separated(Parser::parse_identifier)?; + Ok(()) + } + } else { + self.expected("ROLE or GROUP after IN", self.peek_token()) + } + } + Keyword::ROLE | Keyword::USER => { + if !roles.is_empty() { + parser_err!("Found multiple ROLE or USER") + } else { + roles = self.parse_comma_separated(Parser::parse_identifier)?; + Ok(()) + } + } + Keyword::ADMIN => { + if !admin.is_empty() { + parser_err!("Found multiple ADMIN") + } else { + admin = self.parse_comma_separated(Parser::parse_identifier)?; + Ok(()) + } + } + _ => break, + }? + } + + Ok(Statement::CreateRole { + names, + if_not_exists, + login, + inherit, + bypassrls, + password, + create_db, + create_role, + replication, + superuser, + connection_limit, + valid_until, + in_role, + role: roles, + admin, + authorization_owner, + }) + } + pub fn parse_drop(&mut self) -> Result { let object_type = if self.parse_keyword(Keyword::TABLE) { ObjectType::Table @@ -2051,10 +2254,15 @@ impl<'a> Parser<'a> { ObjectType::View } else if self.parse_keyword(Keyword::INDEX) { ObjectType::Index + } else if self.parse_keyword(Keyword::ROLE) { + ObjectType::Role } else if self.parse_keyword(Keyword::SCHEMA) { ObjectType::Schema } else { - return self.expected("TABLE, VIEW, INDEX or SCHEMA after DROP", self.peek_token()); + return self.expected( + "TABLE, VIEW, INDEX, ROLE, or SCHEMA after DROP", + self.peek_token(), + ); }; // Many dialects support the non standard `IF EXISTS` clause and allow // specifying multiple objects to delete in a single statement @@ -2066,6 +2274,9 @@ impl<'a> Parser<'a> { if cascade && restrict { return parser_err!("Cannot specify both CASCADE and RESTRICT in DROP"); } + if object_type == ObjectType::Role && (cascade || restrict || purge) { + return parser_err!("Cannot specify CASCADE, RESTRICT, or PURGE in DROP ROLE"); + } Ok(Statement::Drop { object_type, if_exists, diff --git a/src/test_utils.rs b/src/test_utils.rs index c5ea62085..c3d60ee62 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -146,6 +146,13 @@ pub fn all_dialects() -> TestedDialects { } } +pub fn assert_eq_vec(expected: &[&str], actual: &[T]) { + assert_eq!( + expected, + actual.iter().map(ToString::to_string).collect::>() + ); +} + pub fn only(v: impl IntoIterator) -> T { let mut iter = v.into_iter(); if let (Some(item), None) = (iter.next(), iter.next()) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 6fee5e88b..87a089c1b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -31,7 +31,8 @@ use sqlparser::keywords::ALL_KEYWORDS; use sqlparser::parser::{Parser, ParserError}; use test_utils::{ - all_dialects, expr_from_projection, join, number, only, table, table_alias, TestedDialects, + all_dialects, assert_eq_vec, expr_from_projection, join, number, only, table, table_alias, + TestedDialects, }; #[test] @@ -4763,6 +4764,63 @@ fn parse_drop_index() { } } +#[test] +fn parse_create_role() { + let sql = "CREATE ROLE consultant"; + match verified_stmt(sql) { + Statement::CreateRole { names, .. } => { + assert_eq_vec(&["consultant"], &names); + } + _ => unreachable!(), + } + + let sql = "CREATE ROLE IF NOT EXISTS mysql_a, mysql_b"; + match verified_stmt(sql) { + Statement::CreateRole { + names, + if_not_exists, + .. + } => { + assert_eq_vec(&["mysql_a", "mysql_b"], &names); + assert!(if_not_exists); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_drop_role() { + let sql = "DROP ROLE abc"; + match verified_stmt(sql) { + Statement::Drop { + names, + object_type, + if_exists, + .. + } => { + assert_eq_vec(&["abc"], &names); + assert_eq!(ObjectType::Role, object_type); + assert!(!if_exists); + } + _ => unreachable!(), + }; + + let sql = "DROP ROLE IF EXISTS def, magician, quaternion"; + match verified_stmt(sql) { + Statement::Drop { + names, + object_type, + if_exists, + .. + } => { + assert_eq_vec(&["def", "magician", "quaternion"], &names); + assert_eq!(ObjectType::Role, object_type); + assert!(if_exists); + } + _ => unreachable!(), + } +} + #[test] fn parse_grant() { let sql = "GRANT SELECT, INSERT, UPDATE (shape, size), USAGE, DELETE, TRUNCATE, REFERENCES, TRIGGER, CONNECT, CREATE, EXECUTE, TEMPORARY ON abc, def TO xyz, m WITH GRANT OPTION GRANTED BY jj"; @@ -4804,14 +4862,8 @@ fn parse_grant() { ], actions ); - assert_eq!( - vec!["abc", "def"], - objects.iter().map(ToString::to_string).collect::>() - ); - assert_eq!( - vec!["xyz", "m"], - grantees.iter().map(ToString::to_string).collect::>() - ); + assert_eq_vec(&["abc", "def"], &objects); + assert_eq_vec(&["xyz", "m"], &grantees); assert!(with_grant_option); assert_eq!("jj", granted_by.unwrap().to_string()); } @@ -4831,14 +4883,8 @@ fn parse_grant() { } => match (privileges, objects) { (Privileges::Actions(actions), GrantObjects::AllTablesInSchema { schemas }) => { assert_eq!(vec![Action::Insert { columns: None }], actions); - assert_eq!( - vec!["public"], - schemas.iter().map(ToString::to_string).collect::>() - ); - assert_eq!( - vec!["browser"], - grantees.iter().map(ToString::to_string).collect::>() - ); + assert_eq_vec(&["public"], &schemas); + assert_eq_vec(&["browser"], &grantees); assert!(!with_grant_option); } _ => unreachable!(), @@ -4860,14 +4906,8 @@ fn parse_grant() { vec![Action::Usage, Action::Select { columns: None }], actions ); - assert_eq!( - vec!["p"], - objects.iter().map(ToString::to_string).collect::>() - ); - assert_eq!( - vec!["u"], - grantees.iter().map(ToString::to_string).collect::>() - ); + assert_eq_vec(&["p"], &objects); + assert_eq_vec(&["u"], &grantees); } _ => unreachable!(), }, @@ -4901,10 +4941,7 @@ fn parse_grant() { GrantObjects::Schemas(schemas), ) => { assert!(!with_privileges_keyword); - assert_eq!( - vec!["aa", "b"], - schemas.iter().map(ToString::to_string).collect::>() - ); + assert_eq_vec(&["aa", "b"], &schemas); } _ => unreachable!(), }, @@ -4920,10 +4957,7 @@ fn parse_grant() { } => match (privileges, objects) { (Privileges::Actions(actions), GrantObjects::AllSequencesInSchema { schemas }) => { assert_eq!(vec![Action::Usage], actions); - assert_eq!( - vec!["bus"], - schemas.iter().map(ToString::to_string).collect::>() - ); + assert_eq_vec(&["bus"], &schemas); } _ => unreachable!(), }, @@ -4948,14 +4982,8 @@ fn test_revoke() { }, privileges ); - assert_eq!( - vec!["users", "auth"], - tables.iter().map(ToString::to_string).collect::>() - ); - assert_eq!( - vec!["analyst"], - grantees.iter().map(ToString::to_string).collect::>() - ); + assert_eq_vec(&["users", "auth"], &tables); + assert_eq_vec(&["analyst"], &grantees); assert!(cascade); assert_eq!(None, granted_by); } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index c613b8b16..de2376f92 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -118,6 +118,28 @@ fn parse_mssql_bin_literal() { let _ = ms_and_generic().one_statement_parses_to("SELECT 0xdeadBEEF", "SELECT X'deadBEEF'"); } +#[test] +fn parse_mssql_create_role() { + let sql = "CREATE ROLE mssql AUTHORIZATION helena"; + match ms().verified_stmt(sql) { + Statement::CreateRole { + names, + authorization_owner, + .. + } => { + assert_eq_vec(&["mssql"], &names); + assert_eq!( + authorization_owner, + Some(ObjectName(vec![Ident { + value: "helena".into(), + quote_style: None + }])) + ); + } + _ => unreachable!(), + } +} + fn ms() -> TestedDialects { TestedDialects { dialects: vec![Box::new(MsSqlDialect {})], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 3aaabc9e3..ecc41a4d3 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1739,3 +1739,104 @@ fn parse_custom_operator() { }) ); } + +#[test] +fn parse_create_role() { + let sql = "CREATE ROLE IF NOT EXISTS mysql_a, mysql_b"; + match pg().verified_stmt(sql) { + Statement::CreateRole { + names, + if_not_exists, + .. + } => { + assert_eq_vec(&["mysql_a", "mysql_b"], &names); + assert!(if_not_exists); + } + _ => unreachable!(), + } + + let sql = "CREATE ROLE abc LOGIN PASSWORD NULL"; + match pg().parse_sql_statements(sql).as_deref() { + Ok( + [Statement::CreateRole { + names, + login, + password, + .. + }], + ) => { + assert_eq_vec(&["abc"], names); + assert_eq!(*login, Some(true)); + assert_eq!(*password, Some(Password::NullPassword)); + } + err => panic!("Failed to parse CREATE ROLE test case: {:?}", err), + } + + let sql = "CREATE ROLE magician WITH SUPERUSER CREATEROLE NOCREATEDB BYPASSRLS INHERIT PASSWORD 'abcdef' LOGIN VALID UNTIL '2025-01-01' IN ROLE role1, role2 ROLE role3 ADMIN role4, role5 REPLICATION"; + // Roundtrip order of optional parameters is not preserved + match pg().parse_sql_statements(sql).as_deref() { + Ok( + [Statement::CreateRole { + names, + if_not_exists, + bypassrls, + login, + inherit, + password, + superuser, + create_db, + create_role, + replication, + connection_limit, + valid_until, + in_role, + role, + admin, + authorization_owner, + }], + ) => { + assert_eq_vec(&["magician"], names); + assert!(!*if_not_exists); + assert_eq!(*login, Some(true)); + assert_eq!(*inherit, Some(true)); + assert_eq!(*bypassrls, Some(true)); + assert_eq!( + *password, + Some(Password::Password(Expr::Value(Value::SingleQuotedString( + "abcdef".into() + )))) + ); + assert_eq!(*superuser, Some(true)); + assert_eq!(*create_db, Some(false)); + assert_eq!(*create_role, Some(true)); + assert_eq!(*replication, Some(true)); + assert_eq!(*connection_limit, None); + assert_eq!( + *valid_until, + Some(Expr::Value(Value::SingleQuotedString("2025-01-01".into()))) + ); + assert_eq_vec(&["role1", "role2"], in_role); + assert_eq_vec(&["role3"], role); + assert_eq_vec(&["role4", "role5"], admin); + assert_eq!(*authorization_owner, None); + } + err => panic!("Failed to parse CREATE ROLE test case: {:?}", err), + } + + let negatables = vec![ + "BYPASSRLS", + "CREATEDB", + "CREATEROLE", + "INHERIT", + "LOGIN", + "REPLICATION", + "SUPERUSER", + ]; + + for negatable_kw in negatables.iter() { + let sql = format!("CREATE ROLE abc {kw} NO{kw}", kw = negatable_kw); + if pg().parse_sql_statements(&sql).is_ok() { + panic!("Should not be able to parse CREATE ROLE containing both negated and non-negated versions of the same keyword: {}", negatable_kw) + } + } +}