diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index 079894075..f0200201d 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -31,6 +31,26 @@ use sqlparser_derive::{Visit, VisitMut}; use super::{display_comma_separated, Expr, Ident, Password}; use crate::ast::{display_separated, ObjectName}; +/// Represents whether ROLE or USER keyword was used in CREATE/ALTER statements. +/// In PostgreSQL, CREATE USER and ALTER USER are equivalent to CREATE ROLE and ALTER ROLE, +/// with CREATE USER having LOGIN enabled by default. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum RoleKeyword { + Role, + User, +} + +impl fmt::Display for RoleKeyword { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + RoleKeyword::Role => write!(f, "ROLE"), + RoleKeyword::User => write!(f, "USER"), + } + } +} + /// An option in `ROLE` statement. /// /// diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 71cb6c5ee..6e5050204 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -55,7 +55,7 @@ pub use self::data_type::{ ExactNumberInfo, IntervalFields, StructBracketKind, TimezoneInfo, }; pub use self::dcl::{ - AlterRoleOperation, ResetConfig, RoleOption, SecondaryRoles, SetConfigValue, Use, + AlterRoleOperation, ResetConfig, RoleKeyword, RoleOption, SecondaryRoles, SetConfigValue, Use, }; pub use self::ddl::{ AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, AlterPolicyOperation, @@ -3314,6 +3314,8 @@ pub enum Statement { CreateRole { names: Vec, if_not_exists: bool, + /// Whether ROLE or USER keyword was used + keyword: RoleKeyword, // Postgres login: Option, inherit: Option, @@ -3421,6 +3423,8 @@ pub enum Statement { /// ``` AlterRole { name: Ident, + /// Whether ROLE or USER keyword was used + keyword: RoleKeyword, operation: AlterRoleOperation, }, /// ```sql @@ -5279,6 +5283,7 @@ impl fmt::Display for Statement { Statement::CreateRole { names, if_not_exists, + keyword, inherit, login, bypassrls, @@ -5298,7 +5303,7 @@ impl fmt::Display for Statement { } => { write!( f, - "CREATE ROLE {if_not_exists}{names}{superuser}{create_db}{create_role}{inherit}{login}{replication}{bypassrls}", + "CREATE {keyword} {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 { @@ -5337,6 +5342,7 @@ impl fmt::Display for Statement { None => "" } )?; + if let Some(limit) = connection_limit { write!(f, " CONNECTION LIMIT {limit}")?; } @@ -5506,8 +5512,12 @@ impl fmt::Display for Statement { Statement::AlterType(AlterType { name, operation }) => { write!(f, "ALTER TYPE {name} {operation}") } - Statement::AlterRole { name, operation } => { - write!(f, "ALTER ROLE {name} {operation}") + Statement::AlterRole { + name, + keyword, + operation, + } => { + write!(f, "ALTER {keyword} {name} {operation}") } Statement::AlterPolicy { name, diff --git a/src/parser/alter.rs b/src/parser/alter.rs index bff462ee0..f47d574bb 100644 --- a/src/parser/alter.rs +++ b/src/parser/alter.rs @@ -19,7 +19,7 @@ use super::{Parser, ParserError}; use crate::{ ast::{ AlterConnectorOwner, AlterPolicyOperation, AlterRoleOperation, Expr, Password, ResetConfig, - RoleOption, SetConfigValue, Statement, + RoleKeyword, RoleOption, SetConfigValue, Statement, }, dialect::{MsSqlDialect, PostgreSqlDialect}, keywords::Keyword, @@ -39,6 +39,10 @@ impl Parser<'_> { )) } + pub fn parse_alter_user(&mut self) -> Result { + self.parse_pg_alter_user() + } + /// Parse ALTER POLICY statement /// ```sql /// ALTER POLICY policy_name ON table_name [ RENAME TO new_name ] @@ -162,11 +166,23 @@ impl Parser<'_> { Ok(Statement::AlterRole { name: role_name, + keyword: RoleKeyword::Role, operation, }) } fn parse_pg_alter_role(&mut self) -> Result { + self.parse_pg_alter_role_or_user(RoleKeyword::Role) + } + + fn parse_pg_alter_user(&mut self) -> Result { + self.parse_pg_alter_role_or_user(RoleKeyword::User) + } + + fn parse_pg_alter_role_or_user( + &mut self, + keyword: RoleKeyword, + ) -> Result { let role_name = self.parse_identifier()?; // [ IN DATABASE _`database_name`_ ] @@ -246,6 +262,7 @@ impl Parser<'_> { Ok(Statement::AlterRole { name: role_name, + keyword, operation, }) } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6c559eed4..d90756136 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4788,6 +4788,10 @@ impl<'a> Parser<'a> { } fn parse_create_user(&mut self, or_replace: bool) -> Result { + if dialect_of!(self is PostgreSqlDialect) { + return self.parse_pg_create_user(); + } + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let name = self.parse_identifier()?; let options = self.parse_key_value_options(false, &[Keyword::WITH, Keyword::TAG])?; @@ -5995,6 +5999,17 @@ impl<'a> Parser<'a> { } pub fn parse_create_role(&mut self) -> Result { + self.parse_pg_create_role_or_user(RoleKeyword::Role) + } + + pub fn parse_pg_create_user(&mut self) -> Result { + self.parse_pg_create_role_or_user(RoleKeyword::User) + } + + fn parse_pg_create_role_or_user( + &mut self, + keyword: RoleKeyword, + ) -> Result { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let names = self.parse_comma_separated(|p| p.parse_object_name(false))?; @@ -6196,9 +6211,15 @@ impl<'a> Parser<'a> { }? } + // In PostgreSQL, CREATE USER defaults to LOGIN=true, CREATE ROLE defaults to LOGIN=false + if login.is_none() && keyword == RoleKeyword::User { + login = Some(true); + } + Ok(Statement::CreateRole { names, if_not_exists, + keyword, login, inherit, bypassrls, @@ -9263,6 +9284,7 @@ impl<'a> Parser<'a> { Keyword::TABLE, Keyword::INDEX, Keyword::ROLE, + Keyword::USER, Keyword::POLICY, Keyword::CONNECTOR, Keyword::ICEBERG, @@ -9300,6 +9322,7 @@ impl<'a> Parser<'a> { }) } Keyword::ROLE => self.parse_alter_role(), + Keyword::USER => self.parse_alter_user(), Keyword::POLICY => self.parse_alter_policy(), Keyword::CONNECTOR => self.parse_alter_connector(), // unreachable because expect_one_of_keywords used above diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 8b99bb1dc..b73464454 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -16600,21 +16600,25 @@ fn parse_odbc_time_date_timestamp() { #[test] fn parse_create_user() { - let create = verified_stmt("CREATE USER u1"); + // PostgreSQL has different CREATE USER semantics (CREATE ROLE with LOGIN=true), + // so we exclude it from this test which is for Snowflake-style CREATE USER + let dialects = all_dialects_except(|d| d.is::()); + + let create = dialects.verified_stmt("CREATE USER u1"); match create { Statement::CreateUser(stmt) => { assert_eq!(stmt.name, Ident::new("u1")); } _ => unreachable!(), } - verified_stmt("CREATE OR REPLACE USER u1"); - verified_stmt("CREATE OR REPLACE USER IF NOT EXISTS u1"); - verified_stmt("CREATE OR REPLACE USER IF NOT EXISTS u1 PASSWORD='secret'"); - verified_stmt( + dialects.verified_stmt("CREATE OR REPLACE USER u1"); + dialects.verified_stmt("CREATE OR REPLACE USER IF NOT EXISTS u1"); + dialects.verified_stmt("CREATE OR REPLACE USER IF NOT EXISTS u1 PASSWORD='secret'"); + dialects.verified_stmt( "CREATE OR REPLACE USER IF NOT EXISTS u1 PASSWORD='secret' MUST_CHANGE_PASSWORD=TRUE", ); - verified_stmt("CREATE OR REPLACE USER IF NOT EXISTS u1 PASSWORD='secret' MUST_CHANGE_PASSWORD=TRUE TYPE=SERVICE TAG (t1='v1')"); - let create = verified_stmt("CREATE OR REPLACE USER IF NOT EXISTS u1 PASSWORD='secret' MUST_CHANGE_PASSWORD=TRUE TYPE=SERVICE WITH TAG (t1='v1', t2='v2')"); + dialects.verified_stmt("CREATE OR REPLACE USER IF NOT EXISTS u1 PASSWORD='secret' MUST_CHANGE_PASSWORD=TRUE TYPE=SERVICE TAG (t1='v1')"); + let create = dialects.verified_stmt("CREATE OR REPLACE USER IF NOT EXISTS u1 PASSWORD='secret' MUST_CHANGE_PASSWORD=TRUE TYPE=SERVICE WITH TAG (t1='v1', t2='v2')"); match create { Statement::CreateUser(stmt) => { assert_eq!(stmt.name, Ident::new("u1")); @@ -16982,7 +16986,7 @@ fn test_parse_semantic_view_table_factor() { } let ast_sql = r#"SELECT * FROM SEMANTIC_VIEW( - my_model + my_model DIMENSIONS DATE_PART('year', date_col), region_name METRICS orders.revenue, orders.count WHERE active = true diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index a1e05d030..a88ae7a5e 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -802,6 +802,7 @@ fn parse_alter_role() { quote_style: None, span: Span::empty(), }, + keyword: RoleKeyword::Role, operation: AlterRoleOperation::RenameRole { role_name: Ident { value: "new_name".into(), @@ -821,6 +822,7 @@ fn parse_alter_role() { quote_style: None, span: Span::empty(), }, + keyword: RoleKeyword::Role, operation: AlterRoleOperation::AddMember { member_name: Ident { value: "new_member".into(), @@ -840,6 +842,7 @@ fn parse_alter_role() { quote_style: None, span: Span::empty(), }, + keyword: RoleKeyword::Role, operation: AlterRoleOperation::DropMember { member_name: Ident { value: "old_member".into(), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 43b30aade..92725269e 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3819,6 +3819,7 @@ fn parse_create_role() { user: _, admin, authorization_owner, + .. }], ) => { assert_eq_vec(&["magician"], names); @@ -3895,6 +3896,7 @@ fn parse_alter_role() { quote_style: None, span: Span::empty(), }, + keyword: RoleKeyword::Role, operation: AlterRoleOperation::RenameRole { role_name: Ident { value: "new_name".into(), @@ -3914,6 +3916,7 @@ fn parse_alter_role() { quote_style: None, span: Span::empty(), }, + keyword: RoleKeyword::Role, operation: AlterRoleOperation::WithOptions { options: vec![ RoleOption::SuperUser(true), @@ -3946,6 +3949,7 @@ fn parse_alter_role() { quote_style: None, span: Span::empty(), }, + keyword: RoleKeyword::Role, operation: AlterRoleOperation::WithOptions { options: vec![ RoleOption::SuperUser(false), @@ -3970,6 +3974,7 @@ fn parse_alter_role() { quote_style: None, span: Span::empty(), }, + keyword: RoleKeyword::Role, operation: AlterRoleOperation::Set { config_name: ObjectName::from(vec![Ident { value: "maintenance_work_mem".into(), @@ -3991,6 +3996,7 @@ fn parse_alter_role() { quote_style: None, span: Span::empty(), }, + keyword: RoleKeyword::Role, operation: AlterRoleOperation::Set { config_name: ObjectName::from(vec![Ident { value: "maintenance_work_mem".into(), @@ -4018,6 +4024,7 @@ fn parse_alter_role() { quote_style: None, span: Span::empty(), }, + keyword: RoleKeyword::Role, operation: AlterRoleOperation::Set { config_name: ObjectName::from(vec![Ident { value: "maintenance_work_mem".into(), @@ -4045,6 +4052,7 @@ fn parse_alter_role() { quote_style: None, span: Span::empty(), }, + keyword: RoleKeyword::Role, operation: AlterRoleOperation::Set { config_name: ObjectName::from(vec![Ident { value: "maintenance_work_mem".into(), @@ -4070,6 +4078,7 @@ fn parse_alter_role() { quote_style: None, span: Span::empty(), }, + keyword: RoleKeyword::Role, operation: AlterRoleOperation::Reset { config_name: ResetConfig::ALL, in_database: None @@ -4086,6 +4095,7 @@ fn parse_alter_role() { quote_style: None, span: Span::empty(), }, + keyword: RoleKeyword::Role, operation: AlterRoleOperation::Reset { config_name: ResetConfig::ConfigName(ObjectName::from(vec![Ident { value: "maintenance_work_mem".into(), @@ -6495,3 +6505,125 @@ fn parse_create_server() { assert_eq!(stmt, expected); } } + +#[test] +fn parse_create_user() { + let sql = "CREATE USER testuser"; + match pg().parse_sql_statements(sql).as_deref() { + Ok( + [Statement::CreateRole { + names, + keyword, + login, + .. + }], + ) => { + assert_eq_vec(&["testuser"], names); + assert_eq!(*keyword, RoleKeyword::User); + assert_eq!(*login, Some(true)); + } + _ => unreachable!(), + } + let stmt = pg().parse_sql_statements(sql).unwrap().pop().unwrap(); + assert_eq!(stmt.to_string(), "CREATE USER testuser LOGIN"); +} + +#[test] +fn parse_create_user_with_password() { + let sql = "CREATE USER testuser PASSWORD 'secret'"; + match pg().parse_sql_statements(sql).as_deref() { + Ok( + [Statement::CreateRole { + names, + keyword, + login, + password, + .. + }], + ) => { + assert_eq_vec(&["testuser"], names); + assert_eq!(*keyword, RoleKeyword::User); + assert_eq!(*login, Some(true)); + assert_eq!( + *password, + Some(Password::Password(Expr::Value( + Value::SingleQuotedString("secret".to_string()).with_empty_span() + ))) + ); + } + _ => unreachable!(), + } + let stmt = pg().parse_sql_statements(sql).unwrap().pop().unwrap(); + assert_eq!( + stmt.to_string(), + "CREATE USER testuser LOGIN PASSWORD 'secret'" + ); +} + +#[test] +fn parse_create_user_nologin() { + let sql = "CREATE USER testuser NOLOGIN"; + match pg().parse_sql_statements(sql).as_deref() { + Ok([Statement::CreateRole { keyword, login, .. }]) => { + assert_eq!(*keyword, RoleKeyword::User); + assert_eq!(*login, Some(false)); + } + _ => unreachable!(), + } + let stmt = pg().parse_sql_statements(sql).unwrap().pop().unwrap(); + assert_eq!(stmt.to_string(), sql); +} + +#[test] +fn parse_alter_user() { + let sql = "ALTER USER testuser WITH PASSWORD 'newsecret'"; + match pg().parse_sql_statements(sql).as_deref() { + Ok( + [Statement::AlterRole { + name, + keyword, + operation, + }], + ) => { + assert_eq!(*name, Ident::new("testuser")); + assert_eq!(*keyword, RoleKeyword::User); + match operation { + AlterRoleOperation::WithOptions { options } => { + assert_eq!(options.len(), 1); + assert_eq!( + options[0], + RoleOption::Password(Password::Password(Expr::Value( + Value::SingleQuotedString("newsecret".to_string()).with_empty_span() + ))) + ); + } + _ => unreachable!(), + } + } + _ => unreachable!(), + } +} + +#[test] +fn parse_alter_user_rename() { + let sql = "ALTER USER oldname RENAME TO newname"; + match pg().parse_sql_statements(sql).as_deref() { + Ok( + [Statement::AlterRole { + name, + keyword, + operation, + }], + ) => { + assert_eq!(*name, Ident::new("oldname")); + assert_eq!(*keyword, RoleKeyword::User); + match operation { + AlterRoleOperation::RenameRole { role_name } => { + assert_eq!(*role_name, Ident::new("newname")); + } + _ => unreachable!(), + } + } + _ => unreachable!(), + } +}