This is an automated email from the ASF dual-hosted git repository.

github-bot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new a4308389 Snowflake: ALTER USER and KeyValueOptions Refactoring (#2035)
a4308389 is described below

commit a43083897468946d03b219f6bba31389e4bb4543
Author: Yoav Cohen <[email protected]>
AuthorDate: Thu Sep 25 21:59:11 2025 +0200

    Snowflake: ALTER USER and KeyValueOptions Refactoring (#2035)
---
 src/ast/dml.rs                          |   7 +-
 src/ast/helpers/key_value_options.rs    |  45 +++---
 src/ast/helpers/stmt_create_database.rs |   2 +-
 src/ast/mod.rs                          | 217 +++++++++++++++++++++++++++++
 src/ast/spans.rs                        |   1 +
 src/dialect/snowflake.rs                |  25 ++--
 src/keywords.rs                         |  12 ++
 src/parser/alter.rs                     | 192 ++++++++++++++++++++++++-
 src/parser/mod.rs                       |  85 ++++++++----
 tests/sqlparser_common.rs               | 239 ++++++++++++++++++++++++++++++--
 tests/sqlparser_snowflake.rs            | 119 ++++++++--------
 11 files changed, 809 insertions(+), 135 deletions(-)

diff --git a/src/ast/dml.rs b/src/ast/dml.rs
index 63d6b86c..e4d99bcf 100644
--- a/src/ast/dml.rs
+++ b/src/ast/dml.rs
@@ -16,12 +16,7 @@
 // under the License.
 
 #[cfg(not(feature = "std"))]
-use alloc::{
-    boxed::Box,
-    format,
-    string::{String, ToString},
-    vec::Vec,
-};
+use alloc::{boxed::Box, format, string::ToString, vec::Vec};
 
 use core::fmt::{self, Display};
 #[cfg(feature = "serde")]
diff --git a/src/ast/helpers/key_value_options.rs 
b/src/ast/helpers/key_value_options.rs
index 7f1bb0fd..745c3a65 100644
--- a/src/ast/helpers/key_value_options.rs
+++ b/src/ast/helpers/key_value_options.rs
@@ -19,9 +19,7 @@
 //! See [this 
page](https://docs.snowflake.com/en/sql-reference/commands-data-loading) for 
more details.
 
 #[cfg(not(feature = "std"))]
-use alloc::string::String;
-#[cfg(not(feature = "std"))]
-use alloc::vec::Vec;
+use alloc::{boxed::Box, string::String, vec::Vec};
 use core::fmt;
 use core::fmt::Formatter;
 
@@ -31,7 +29,7 @@ use serde::{Deserialize, Serialize};
 #[cfg(feature = "visitor")]
 use sqlparser_derive::{Visit, VisitMut};
 
-use crate::ast::display_separated;
+use crate::ast::{display_comma_separated, display_separated, Value};
 
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@@ -52,20 +50,23 @@ pub enum KeyValueOptionsDelimiter {
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
-pub enum KeyValueOptionType {
-    STRING,
-    BOOLEAN,
-    ENUM,
-    NUMBER,
+pub struct KeyValueOption {
+    pub option_name: String,
+    pub option_value: KeyValueOptionKind,
 }
 
+/// An option can have a single value, multiple values or a nested list of 
values.
+///
+/// A value can be numeric, boolean, etc. Enum-style values are represented
+/// as Value::Placeholder. For example: MFA_METHOD=SMS will be represented as
+/// `Value::Placeholder("SMS".to_string)`.
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
-pub struct KeyValueOption {
-    pub option_name: String,
-    pub option_type: KeyValueOptionType,
-    pub value: String,
+pub enum KeyValueOptionKind {
+    Single(Value),
+    Multi(Vec<Value>),
+    KeyValueOptions(Box<KeyValueOptions>),
 }
 
 impl fmt::Display for KeyValueOptions {
@@ -80,12 +81,20 @@ impl fmt::Display for KeyValueOptions {
 
 impl fmt::Display for KeyValueOption {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self.option_type {
-            KeyValueOptionType::STRING => {
-                write!(f, "{}='{}'", self.option_name, self.value)?;
+        match &self.option_value {
+            KeyValueOptionKind::Single(value) => {
+                write!(f, "{}={value}", self.option_name)?;
+            }
+            KeyValueOptionKind::Multi(values) => {
+                write!(
+                    f,
+                    "{}=({})",
+                    self.option_name,
+                    display_comma_separated(values)
+                )?;
             }
-            KeyValueOptionType::ENUM | KeyValueOptionType::BOOLEAN | 
KeyValueOptionType::NUMBER => {
-                write!(f, "{}={}", self.option_name, self.value)?;
+            KeyValueOptionKind::KeyValueOptions(options) => {
+                write!(f, "{}=({options})", self.option_name)?;
             }
         }
         Ok(())
diff --git a/src/ast/helpers/stmt_create_database.rs 
b/src/ast/helpers/stmt_create_database.rs
index 94997bfa..58a7b090 100644
--- a/src/ast/helpers/stmt_create_database.rs
+++ b/src/ast/helpers/stmt_create_database.rs
@@ -16,7 +16,7 @@
 // under the License.
 
 #[cfg(not(feature = "std"))]
-use alloc::{boxed::Box, format, string::String, vec, vec::Vec};
+use alloc::{format, string::String, vec::Vec};
 
 #[cfg(feature = "serde")]
 use serde::{Deserialize, Serialize};
diff --git a/src/ast/mod.rs b/src/ast/mod.rs
index 8df636f8..4c1743fe 100644
--- a/src/ast/mod.rs
+++ b/src/ast/mod.rs
@@ -4310,6 +4310,11 @@ pub enum Statement {
     /// ```
     /// 
[Snowflake](https://docs.snowflake.com/en/sql-reference/sql/create-user)
     CreateUser(CreateUser),
+    /// ```sql
+    /// ALTER USER \[ IF EXISTS \] \[ <name> \]
+    /// ```
+    /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/alter-user)
+    AlterUser(AlterUser),
     /// Re-sorts rows and reclaims space in either a specified table or all 
tables in the current database
     ///
     /// ```sql
@@ -6183,6 +6188,7 @@ impl fmt::Display for Statement {
             Statement::CreateUser(s) => write!(f, "{s}"),
             Statement::AlterSchema(s) => write!(f, "{s}"),
             Statement::Vacuum(s) => write!(f, "{s}"),
+            Statement::AlterUser(s) => write!(f, "{s}"),
         }
     }
 }
@@ -10558,6 +10564,217 @@ impl fmt::Display for CreateUser {
     }
 }
 
+/// Modifies the properties of a user
+///
+/// Syntax:
+/// ```sql
+/// ALTER USER [ IF EXISTS ] [ <name> ] [ OPTIONS ]
+/// ```
+///
+/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/alter-user)
+#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub struct AlterUser {
+    pub if_exists: bool,
+    pub name: Ident,
+    /// The following fields are Snowflake-specific: 
<https://docs.snowflake.com/en/sql-reference/sql/alter-user#syntax>
+    pub rename_to: Option<Ident>,
+    pub reset_password: bool,
+    pub abort_all_queries: bool,
+    pub add_role_delegation: Option<AlterUserAddRoleDelegation>,
+    pub remove_role_delegation: Option<AlterUserRemoveRoleDelegation>,
+    pub enroll_mfa: bool,
+    pub set_default_mfa_method: Option<MfaMethodKind>,
+    pub remove_mfa_method: Option<MfaMethodKind>,
+    pub modify_mfa_method: Option<AlterUserModifyMfaMethod>,
+    pub add_mfa_method_otp: Option<AlterUserAddMfaMethodOtp>,
+    pub set_policy: Option<AlterUserSetPolicy>,
+    pub unset_policy: Option<UserPolicyKind>,
+    pub set_tag: KeyValueOptions,
+    pub unset_tag: Vec<String>,
+    pub set_props: KeyValueOptions,
+    pub unset_props: Vec<String>,
+}
+
+/// ```sql
+/// ALTER USER [ IF EXISTS ] [ <name> ] ADD DELEGATED AUTHORIZATION OF ROLE 
<role_name> TO SECURITY INTEGRATION <integration_name>
+/// ```
+#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub struct AlterUserAddRoleDelegation {
+    pub role: Ident,
+    pub integration: Ident,
+}
+
+/// ```sql
+/// ALTER USER [ IF EXISTS ] [ <name> ] REMOVE DELEGATED { AUTHORIZATION OF 
ROLE <role_name> | AUTHORIZATIONS } FROM SECURITY INTEGRATION <integration_name>
+/// ```
+#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub struct AlterUserRemoveRoleDelegation {
+    pub role: Option<Ident>,
+    pub integration: Ident,
+}
+
+/// ```sql
+/// ADD MFA METHOD OTP [ COUNT = number ]
+/// ```
+#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub struct AlterUserAddMfaMethodOtp {
+    pub count: Option<Value>,
+}
+
+/// ```sql
+/// ALTER USER [ IF EXISTS ] [ <name> ] MODIFY MFA METHOD <mfa_method> SET 
COMMENT = '<string>'
+/// ```
+#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub struct AlterUserModifyMfaMethod {
+    pub method: MfaMethodKind,
+    pub comment: String,
+}
+
+/// Types of MFA methods
+#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub enum MfaMethodKind {
+    PassKey,
+    Totp,
+    Duo,
+}
+
+impl fmt::Display for MfaMethodKind {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            MfaMethodKind::PassKey => write!(f, "PASSKEY"),
+            MfaMethodKind::Totp => write!(f, "TOTP"),
+            MfaMethodKind::Duo => write!(f, "DUO"),
+        }
+    }
+}
+
+/// ```sql
+/// ALTER USER [ IF EXISTS ] [ <name> ] SET { AUTHENTICATION | PASSWORD | 
SESSION } POLICY <policy_name>
+/// ```
+#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub struct AlterUserSetPolicy {
+    pub policy_kind: UserPolicyKind,
+    pub policy: Ident,
+}
+
+/// Types of user-based policies
+#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub enum UserPolicyKind {
+    Authentication,
+    Password,
+    Session,
+}
+
+impl fmt::Display for UserPolicyKind {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            UserPolicyKind::Authentication => write!(f, "AUTHENTICATION"),
+            UserPolicyKind::Password => write!(f, "PASSWORD"),
+            UserPolicyKind::Session => write!(f, "SESSION"),
+        }
+    }
+}
+
+impl fmt::Display for AlterUser {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "ALTER")?;
+        write!(f, " USER")?;
+        if self.if_exists {
+            write!(f, " IF EXISTS")?;
+        }
+        write!(f, " {}", self.name)?;
+        if let Some(new_name) = &self.rename_to {
+            write!(f, " RENAME TO {new_name}")?;
+        }
+        if self.reset_password {
+            write!(f, " RESET PASSWORD")?;
+        }
+        if self.abort_all_queries {
+            write!(f, " ABORT ALL QUERIES")?;
+        }
+        if let Some(role_delegation) = &self.add_role_delegation {
+            let role = &role_delegation.role;
+            let integration = &role_delegation.integration;
+            write!(
+                f,
+                " ADD DELEGATED AUTHORIZATION OF ROLE {role} TO SECURITY 
INTEGRATION {integration}"
+            )?;
+        }
+        if let Some(role_delegation) = &self.remove_role_delegation {
+            write!(f, " REMOVE DELEGATED")?;
+            match &role_delegation.role {
+                Some(role) => write!(f, " AUTHORIZATION OF ROLE {role}")?,
+                None => write!(f, " AUTHORIZATIONS")?,
+            }
+            let integration = &role_delegation.integration;
+            write!(f, " FROM SECURITY INTEGRATION {integration}")?;
+        }
+        if self.enroll_mfa {
+            write!(f, " ENROLL MFA")?;
+        }
+        if let Some(method) = &self.set_default_mfa_method {
+            write!(f, " SET DEFAULT_MFA_METHOD {method}")?
+        }
+        if let Some(method) = &self.remove_mfa_method {
+            write!(f, " REMOVE MFA METHOD {method}")?;
+        }
+        if let Some(modify) = &self.modify_mfa_method {
+            let method = &modify.method;
+            let comment = &modify.comment;
+            write!(
+                f,
+                " MODIFY MFA METHOD {method} SET COMMENT '{}'",
+                value::escape_single_quote_string(comment)
+            )?;
+        }
+        if let Some(add_mfa_method_otp) = &self.add_mfa_method_otp {
+            write!(f, " ADD MFA METHOD OTP")?;
+            if let Some(count) = &add_mfa_method_otp.count {
+                write!(f, " COUNT = {count}")?;
+            }
+        }
+        if let Some(policy) = &self.set_policy {
+            let policy_kind = &policy.policy_kind;
+            let name = &policy.policy;
+            write!(f, " SET {policy_kind} POLICY {name}")?;
+        }
+        if let Some(policy_kind) = &self.unset_policy {
+            write!(f, " UNSET {policy_kind} POLICY")?;
+        }
+        if !self.set_tag.options.is_empty() {
+            write!(f, " SET TAG {}", self.set_tag)?;
+        }
+        if !self.unset_tag.is_empty() {
+            write!(f, " UNSET TAG {}", 
display_comma_separated(&self.unset_tag))?;
+        }
+        let has_props = !self.set_props.options.is_empty();
+        if has_props {
+            write!(f, " SET")?;
+            write!(f, " {}", &self.set_props)?;
+        }
+        if !self.unset_props.is_empty() {
+            write!(f, " UNSET {}", 
display_comma_separated(&self.unset_props))?;
+        }
+        Ok(())
+    }
+}
+
 /// Specifies how to create a new table based on an existing table's schema.
 /// '''sql
 /// CREATE TABLE new LIKE old ...
diff --git a/src/ast/spans.rs b/src/ast/spans.rs
index 5913fe16..4c53e55c 100644
--- a/src/ast/spans.rs
+++ b/src/ast/spans.rs
@@ -555,6 +555,7 @@ impl Spanned for Statement {
             Statement::CreateUser(..) => Span::empty(),
             Statement::AlterSchema(s) => s.span(),
             Statement::Vacuum(..) => Span::empty(),
+            Statement::AlterUser(..) => Span::empty(),
         }
     }
 }
diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs
index 3bb36010..825fd45f 100644
--- a/src/dialect/snowflake.rs
+++ b/src/dialect/snowflake.rs
@@ -18,7 +18,7 @@
 #[cfg(not(feature = "std"))]
 use crate::alloc::string::ToString;
 use crate::ast::helpers::key_value_options::{
-    KeyValueOption, KeyValueOptionType, KeyValueOptions, 
KeyValueOptionsDelimiter,
+    KeyValueOption, KeyValueOptionKind, KeyValueOptions, 
KeyValueOptionsDelimiter,
 };
 use crate::ast::helpers::stmt_create_database::CreateDatabaseBuilder;
 use crate::ast::helpers::stmt_create_table::CreateTableBuilder;
@@ -30,7 +30,7 @@ use crate::ast::{
     CopyIntoSnowflakeKind, CreateTableLikeKind, DollarQuotedString, Ident, 
IdentityParameters,
     IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, 
IdentityPropertyOrder,
     InitializeKind, ObjectName, ObjectNamePart, RefreshModeKind, 
RowAccessPolicy, ShowObjects,
-    SqlOption, Statement, StorageSerializationPolicy, TagsColumnOption, 
WrappedCollection,
+    SqlOption, Statement, StorageSerializationPolicy, TagsColumnOption, Value, 
WrappedCollection,
 };
 use crate::dialect::{Dialect, Precedence};
 use crate::keywords::Keyword;
@@ -1004,19 +1004,19 @@ pub fn parse_create_stage(
     // [ directoryTableParams ]
     if parser.parse_keyword(Keyword::DIRECTORY) {
         parser.expect_token(&Token::Eq)?;
-        directory_table_params = parser.parse_key_value_options(true, &[])?;
+        directory_table_params = parser.parse_key_value_options(true, 
&[])?.options;
     }
 
     // [ file_format]
     if parser.parse_keyword(Keyword::FILE_FORMAT) {
         parser.expect_token(&Token::Eq)?;
-        file_format = parser.parse_key_value_options(true, &[])?;
+        file_format = parser.parse_key_value_options(true, &[])?.options;
     }
 
     // [ copy_options ]
     if parser.parse_keyword(Keyword::COPY_OPTIONS) {
         parser.expect_token(&Token::Eq)?;
-        copy_options = parser.parse_key_value_options(true, &[])?;
+        copy_options = parser.parse_key_value_options(true, &[])?.options;
     }
 
     // [ comment ]
@@ -1182,7 +1182,7 @@ pub fn parse_copy_into(parser: &mut Parser) -> 
Result<Statement, ParserError> {
         // FILE_FORMAT
         if parser.parse_keyword(Keyword::FILE_FORMAT) {
             parser.expect_token(&Token::Eq)?;
-            file_format = parser.parse_key_value_options(true, &[])?;
+            file_format = parser.parse_key_value_options(true, &[])?.options;
         // PARTITION BY
         } else if parser.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) {
             partition = Some(Box::new(parser.parse_expr()?))
@@ -1220,14 +1220,14 @@ pub fn parse_copy_into(parser: &mut Parser) -> 
Result<Statement, ParserError> {
         // COPY OPTIONS
         } else if parser.parse_keyword(Keyword::COPY_OPTIONS) {
             parser.expect_token(&Token::Eq)?;
-            copy_options = parser.parse_key_value_options(true, &[])?;
+            copy_options = parser.parse_key_value_options(true, &[])?.options;
         } else {
             match parser.next_token().token {
                 Token::SemiColon | Token::EOF => break,
                 Token::Comma => continue,
                 // In `COPY INTO <location>` the copy options do not have a 
shared key
                 // like in `COPY INTO <table>`
-                Token::Word(key) => 
copy_options.push(parser.parse_key_value_option(key)?),
+                Token::Word(key) => 
copy_options.push(parser.parse_key_value_option(&key)?),
                 _ => return parser.expected("another copy option, ; or EOF'", 
parser.peek_token()),
             }
         }
@@ -1387,7 +1387,7 @@ fn parse_stage_params(parser: &mut Parser) -> 
Result<StageParamsObject, ParserEr
     if parser.parse_keyword(Keyword::CREDENTIALS) {
         parser.expect_token(&Token::Eq)?;
         credentials = KeyValueOptions {
-            options: parser.parse_key_value_options(true, &[])?,
+            options: parser.parse_key_value_options(true, &[])?.options,
             delimiter: KeyValueOptionsDelimiter::Space,
         };
     }
@@ -1396,7 +1396,7 @@ fn parse_stage_params(parser: &mut Parser) -> 
Result<StageParamsObject, ParserEr
     if parser.parse_keyword(Keyword::ENCRYPTION) {
         parser.expect_token(&Token::Eq)?;
         encryption = KeyValueOptions {
-            options: parser.parse_key_value_options(true, &[])?,
+            options: parser.parse_key_value_options(true, &[])?.options,
             delimiter: KeyValueOptionsDelimiter::Space,
         };
     }
@@ -1431,13 +1431,12 @@ fn parse_session_options(
             Token::Word(key) => {
                 parser.advance_token();
                 if set {
-                    let option = parser.parse_key_value_option(key)?;
+                    let option = parser.parse_key_value_option(&key)?;
                     options.push(option);
                 } else {
                     options.push(KeyValueOption {
                         option_name: key.value,
-                        option_type: KeyValueOptionType::STRING,
-                        value: empty(),
+                        option_value: 
KeyValueOptionKind::Single(Value::Placeholder(empty())),
                     });
                 }
             }
diff --git a/src/keywords.rs b/src/keywords.rs
index e0590b69..3c985522 100644
--- a/src/keywords.rs
+++ b/src/keywords.rs
@@ -119,6 +119,7 @@ define_keywords!(
     AUDIT,
     AUTHENTICATION,
     AUTHORIZATION,
+    AUTHORIZATIONS,
     AUTO,
     AUTOEXTEND_SIZE,
     AUTOINCREMENT,
@@ -279,6 +280,8 @@ define_keywords!(
     DEFAULT,
     DEFAULTS,
     DEFAULT_DDL_COLLATION,
+    DEFAULT_MFA_METHOD,
+    DEFAULT_SECONDARY_ROLES,
     DEFERRABLE,
     DEFERRED,
     DEFINE,
@@ -286,6 +289,7 @@ define_keywords!(
     DEFINER,
     DELAYED,
     DELAY_KEY_WRITE,
+    DELEGATED,
     DELETE,
     DELIMITED,
     DELIMITER,
@@ -314,6 +318,7 @@ define_keywords!(
     DOY,
     DROP,
     DRY,
+    DUO,
     DUPLICATE,
     DYNAMIC,
     EACH,
@@ -336,6 +341,7 @@ define_keywords!(
     ENFORCED,
     ENGINE,
     ENGINE_ATTRIBUTE,
+    ENROLL,
     ENUM,
     ENUM16,
     ENUM8,
@@ -586,6 +592,7 @@ define_keywords!(
     METHOD,
     METRIC,
     METRICS,
+    MFA,
     MICROSECOND,
     MICROSECONDS,
     MILLENIUM,
@@ -685,6 +692,7 @@ define_keywords!(
     ORDINALITY,
     ORGANIZATION,
     OTHER,
+    OTP,
     OUT,
     OUTER,
     OUTPUT,
@@ -709,6 +717,7 @@ define_keywords!(
     PARTITIONED,
     PARTITIONS,
     PASSING,
+    PASSKEY,
     PASSWORD,
     PAST,
     PATH,
@@ -753,6 +762,7 @@ define_keywords!(
     PURGE,
     QUALIFY,
     QUARTER,
+    QUERIES,
     QUERY,
     QUOTE,
     RAISE,
@@ -969,6 +979,7 @@ define_keywords!(
     TO,
     TOP,
     TOTALS,
+    TOTP,
     TRACE,
     TRAILING,
     TRANSACTION,
@@ -1067,6 +1078,7 @@ define_keywords!(
     WITHOUT,
     WITHOUT_ARRAY_WRAPPER,
     WORK,
+    WORKLOAD_IDENTITY,
     WRAPPER,
     WRITE,
     XML,
diff --git a/src/parser/alter.rs b/src/parser/alter.rs
index bff462ee..b3e3c99e 100644
--- a/src/parser/alter.rs
+++ b/src/parser/alter.rs
@@ -13,13 +13,16 @@
 //! SQL Parser for ALTER
 
 #[cfg(not(feature = "std"))]
-use alloc::vec;
+use alloc::{string::ToString, vec};
 
 use super::{Parser, ParserError};
 use crate::{
     ast::{
-        AlterConnectorOwner, AlterPolicyOperation, AlterRoleOperation, Expr, 
Password, ResetConfig,
-        RoleOption, SetConfigValue, Statement,
+        helpers::key_value_options::{KeyValueOptions, 
KeyValueOptionsDelimiter},
+        AlterConnectorOwner, AlterPolicyOperation, AlterRoleOperation, 
AlterUser,
+        AlterUserAddMfaMethodOtp, AlterUserAddRoleDelegation, 
AlterUserModifyMfaMethod,
+        AlterUserRemoveRoleDelegation, AlterUserSetPolicy, Expr, 
MfaMethodKind, Password,
+        ResetConfig, RoleOption, SetConfigValue, Statement, UserPolicyKind,
     },
     dialect::{MsSqlDialect, PostgreSqlDialect},
     keywords::Keyword,
@@ -140,6 +143,189 @@ impl Parser<'_> {
         })
     }
 
+    /// Parse an `ALTER USER` statement
+    /// ```sql
+    /// ALTER USER [ IF EXISTS ] [ <name> ] [ OPTIONS ]
+    /// ```
+    pub fn parse_alter_user(&mut self) -> Result<Statement, ParserError> {
+        let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
+        let name = self.parse_identifier()?;
+        let rename_to = if self.parse_keywords(&[Keyword::RENAME, 
Keyword::TO]) {
+            Some(self.parse_identifier()?)
+        } else {
+            None
+        };
+        let reset_password = self.parse_keywords(&[Keyword::RESET, 
Keyword::PASSWORD]);
+        let abort_all_queries =
+            self.parse_keywords(&[Keyword::ABORT, Keyword::ALL, 
Keyword::QUERIES]);
+        let add_role_delegation = if self.parse_keywords(&[
+            Keyword::ADD,
+            Keyword::DELEGATED,
+            Keyword::AUTHORIZATION,
+            Keyword::OF,
+            Keyword::ROLE,
+        ]) {
+            let role = self.parse_identifier()?;
+            self.expect_keywords(&[Keyword::TO, Keyword::SECURITY, 
Keyword::INTEGRATION])?;
+            let integration = self.parse_identifier()?;
+            Some(AlterUserAddRoleDelegation { role, integration })
+        } else {
+            None
+        };
+        let remove_role_delegation = if self.parse_keywords(&[Keyword::REMOVE, 
Keyword::DELEGATED])
+        {
+            let role = if self.parse_keywords(&[Keyword::AUTHORIZATION, 
Keyword::OF, Keyword::ROLE])
+            {
+                Some(self.parse_identifier()?)
+            } else if self.parse_keyword(Keyword::AUTHORIZATIONS) {
+                None
+            } else {
+                return self.expected(
+                    "REMOVE DELEGATED AUTHORIZATION OF ROLE | REMOVE DELEGATED 
AUTHORIZATIONS",
+                    self.peek_token(),
+                );
+            };
+            self.expect_keywords(&[Keyword::FROM, Keyword::SECURITY, 
Keyword::INTEGRATION])?;
+            let integration = self.parse_identifier()?;
+            Some(AlterUserRemoveRoleDelegation { role, integration })
+        } else {
+            None
+        };
+        let enroll_mfa = self.parse_keywords(&[Keyword::ENROLL, Keyword::MFA]);
+        let set_default_mfa_method =
+            if self.parse_keywords(&[Keyword::SET, 
Keyword::DEFAULT_MFA_METHOD]) {
+                Some(self.parse_mfa_method()?)
+            } else {
+                None
+            };
+        let remove_mfa_method =
+            if self.parse_keywords(&[Keyword::REMOVE, Keyword::MFA, 
Keyword::METHOD]) {
+                Some(self.parse_mfa_method()?)
+            } else {
+                None
+            };
+        let modify_mfa_method =
+            if self.parse_keywords(&[Keyword::MODIFY, Keyword::MFA, 
Keyword::METHOD]) {
+                let method = self.parse_mfa_method()?;
+                self.expect_keywords(&[Keyword::SET, Keyword::COMMENT])?;
+                let comment = self.parse_literal_string()?;
+                Some(AlterUserModifyMfaMethod { method, comment })
+            } else {
+                None
+            };
+        let add_mfa_method_otp =
+            if self.parse_keywords(&[Keyword::ADD, Keyword::MFA, 
Keyword::METHOD, Keyword::OTP]) {
+                let count = if self.parse_keyword(Keyword::COUNT) {
+                    self.expect_token(&Token::Eq)?;
+                    Some(self.parse_value()?.into())
+                } else {
+                    None
+                };
+                Some(AlterUserAddMfaMethodOtp { count })
+            } else {
+                None
+            };
+        let set_policy =
+            if self.parse_keywords(&[Keyword::SET, Keyword::AUTHENTICATION, 
Keyword::POLICY]) {
+                Some(AlterUserSetPolicy {
+                    policy_kind: UserPolicyKind::Authentication,
+                    policy: self.parse_identifier()?,
+                })
+            } else if self.parse_keywords(&[Keyword::SET, Keyword::PASSWORD, 
Keyword::POLICY]) {
+                Some(AlterUserSetPolicy {
+                    policy_kind: UserPolicyKind::Password,
+                    policy: self.parse_identifier()?,
+                })
+            } else if self.parse_keywords(&[Keyword::SET, Keyword::SESSION, 
Keyword::POLICY]) {
+                Some(AlterUserSetPolicy {
+                    policy_kind: UserPolicyKind::Session,
+                    policy: self.parse_identifier()?,
+                })
+            } else {
+                None
+            };
+
+        let unset_policy =
+            if self.parse_keywords(&[Keyword::UNSET, Keyword::AUTHENTICATION, 
Keyword::POLICY]) {
+                Some(UserPolicyKind::Authentication)
+            } else if self.parse_keywords(&[Keyword::UNSET, Keyword::PASSWORD, 
Keyword::POLICY]) {
+                Some(UserPolicyKind::Password)
+            } else if self.parse_keywords(&[Keyword::UNSET, Keyword::SESSION, 
Keyword::POLICY]) {
+                Some(UserPolicyKind::Session)
+            } else {
+                None
+            };
+
+        let set_tag = if self.parse_keywords(&[Keyword::SET, Keyword::TAG]) {
+            self.parse_key_value_options(false, &[])?
+        } else {
+            KeyValueOptions {
+                delimiter: KeyValueOptionsDelimiter::Comma,
+                options: vec![],
+            }
+        };
+
+        let unset_tag = if self.parse_keywords(&[Keyword::UNSET, 
Keyword::TAG]) {
+            self.parse_comma_separated(Parser::parse_identifier)?
+                .iter()
+                .map(|i| i.to_string())
+                .collect()
+        } else {
+            vec![]
+        };
+
+        let set_props = if self.parse_keyword(Keyword::SET) {
+            self.parse_key_value_options(false, &[])?
+        } else {
+            KeyValueOptions {
+                delimiter: KeyValueOptionsDelimiter::Comma,
+                options: vec![],
+            }
+        };
+
+        let unset_props = if self.parse_keyword(Keyword::UNSET) {
+            self.parse_comma_separated(Parser::parse_identifier)?
+                .iter()
+                .map(|i| i.to_string())
+                .collect()
+        } else {
+            vec![]
+        };
+
+        Ok(Statement::AlterUser(AlterUser {
+            if_exists,
+            name,
+            rename_to,
+            reset_password,
+            abort_all_queries,
+            add_role_delegation,
+            remove_role_delegation,
+            enroll_mfa,
+            set_default_mfa_method,
+            remove_mfa_method,
+            modify_mfa_method,
+            add_mfa_method_otp,
+            set_policy,
+            unset_policy,
+            set_tag,
+            unset_tag,
+            set_props,
+            unset_props,
+        }))
+    }
+
+    fn parse_mfa_method(&mut self) -> Result<MfaMethodKind, ParserError> {
+        if self.parse_keyword(Keyword::PASSKEY) {
+            Ok(MfaMethodKind::PassKey)
+        } else if self.parse_keyword(Keyword::TOTP) {
+            Ok(MfaMethodKind::Totp)
+        } else if self.parse_keyword(Keyword::DUO) {
+            Ok(MfaMethodKind::Duo)
+        } else {
+            self.expected("PASSKEY, TOTP or DUO", self.peek_token())
+        }
+    }
+
     fn parse_mssql_alter_role(&mut self) -> Result<Statement, ParserError> {
         let role_name = self.parse_identifier()?;
 
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index f47d4b9b..66089be7 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -34,7 +34,7 @@ use IsOptional::*;
 
 use crate::ast::helpers::{
     key_value_options::{
-        KeyValueOption, KeyValueOptionType, KeyValueOptions, 
KeyValueOptionsDelimiter,
+        KeyValueOption, KeyValueOptionKind, KeyValueOptions, 
KeyValueOptionsDelimiter,
     },
     stmt_create_table::{CreateTableBuilder, CreateTableConfiguration},
 };
@@ -4796,10 +4796,12 @@ impl<'a> Parser<'a> {
     fn parse_create_user(&mut self, or_replace: bool) -> Result<Statement, 
ParserError> {
         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])?;
+        let options = self
+            .parse_key_value_options(false, &[Keyword::WITH, Keyword::TAG])?
+            .options;
         let with_tags = self.parse_keyword(Keyword::WITH);
         let tags = if self.parse_keyword(Keyword::TAG) {
-            self.parse_key_value_options(true, &[])?
+            self.parse_key_value_options(true, &[])?.options
         } else {
             vec![]
         };
@@ -9277,6 +9279,7 @@ impl<'a> Parser<'a> {
             Keyword::CONNECTOR,
             Keyword::ICEBERG,
             Keyword::SCHEMA,
+            Keyword::USER,
         ])?;
         match object_type {
             Keyword::SCHEMA => {
@@ -9312,6 +9315,7 @@ impl<'a> Parser<'a> {
             Keyword::ROLE => self.parse_alter_role(),
             Keyword::POLICY => self.parse_alter_policy(),
             Keyword::CONNECTOR => self.parse_alter_connector(),
+            Keyword::USER => self.parse_alter_user(),
             // unreachable because expect_one_of_keywords used above
             _ => unreachable!(),
         }
@@ -17488,8 +17492,9 @@ impl<'a> Parser<'a> {
         &mut self,
         parenthesized: bool,
         end_words: &[Keyword],
-    ) -> Result<Vec<KeyValueOption>, ParserError> {
+    ) -> Result<KeyValueOptions, ParserError> {
         let mut options: Vec<KeyValueOption> = Vec::new();
+        let mut delimiter = KeyValueOptionsDelimiter::Space;
         if parenthesized {
             self.expect_token(&Token::LParen)?;
         }
@@ -17503,9 +17508,12 @@ impl<'a> Parser<'a> {
                     }
                 }
                 Token::EOF => break,
-                Token::Comma => continue,
+                Token::Comma => {
+                    delimiter = KeyValueOptionsDelimiter::Comma;
+                    continue;
+                }
                 Token::Word(w) if !end_words.contains(&w.keyword) => {
-                    options.push(self.parse_key_value_option(w)?)
+                    options.push(self.parse_key_value_option(&w)?)
                 }
                 Token::Word(w) if end_words.contains(&w.keyword) => {
                     self.prev_token();
@@ -17514,40 +17522,67 @@ impl<'a> Parser<'a> {
                 _ => return self.expected("another option, EOF, Comma or ')'", 
self.peek_token()),
             };
         }
-        Ok(options)
+
+        Ok(KeyValueOptions { delimiter, options })
     }
 
     /// Parses a `KEY = VALUE` construct based on the specified key
     pub(crate) fn parse_key_value_option(
         &mut self,
-        key: Word,
+        key: &Word,
     ) -> Result<KeyValueOption, ParserError> {
         self.expect_token(&Token::Eq)?;
-        match self.next_token().token {
-            Token::SingleQuotedString(value) => Ok(KeyValueOption {
-                option_name: key.value,
-                option_type: KeyValueOptionType::STRING,
-                value,
+        match self.peek_token().token {
+            Token::SingleQuotedString(_) => Ok(KeyValueOption {
+                option_name: key.value.clone(),
+                option_value: 
KeyValueOptionKind::Single(self.parse_value()?.into()),
             }),
             Token::Word(word)
                 if word.keyword == Keyword::TRUE || word.keyword == 
Keyword::FALSE =>
             {
                 Ok(KeyValueOption {
-                    option_name: key.value,
-                    option_type: KeyValueOptionType::BOOLEAN,
-                    value: word.value.to_uppercase(),
+                    option_name: key.value.clone(),
+                    option_value: 
KeyValueOptionKind::Single(self.parse_value()?.into()),
                 })
             }
-            Token::Word(word) => Ok(KeyValueOption {
-                option_name: key.value,
-                option_type: KeyValueOptionType::ENUM,
-                value: word.value,
-            }),
-            Token::Number(n, _) => Ok(KeyValueOption {
-                option_name: key.value,
-                option_type: KeyValueOptionType::NUMBER,
-                value: n,
+            Token::Number(..) => Ok(KeyValueOption {
+                option_name: key.value.clone(),
+                option_value: 
KeyValueOptionKind::Single(self.parse_value()?.into()),
             }),
+            Token::Word(word) => {
+                self.next_token();
+                Ok(KeyValueOption {
+                    option_name: key.value.clone(),
+                    option_value: 
KeyValueOptionKind::Single(Value::Placeholder(
+                        word.value.clone(),
+                    )),
+                })
+            }
+            Token::LParen => {
+                // Can be a list of values or a list of key value properties.
+                // Try to parse a list of values and if that fails, try to 
parse
+                // a list of key-value properties.
+                match self.maybe_parse(|parser| {
+                    parser.expect_token(&Token::LParen)?;
+                    let values = parser.parse_comma_separated0(|p| 
p.parse_value(), Token::RParen);
+                    parser.expect_token(&Token::RParen)?;
+                    values
+                })? {
+                    Some(values) => {
+                        let values = values.into_iter().map(|v| 
v.value).collect();
+                        Ok(KeyValueOption {
+                            option_name: key.value.clone(),
+                            option_value: KeyValueOptionKind::Multi(values),
+                        })
+                    }
+                    None => Ok(KeyValueOption {
+                        option_name: key.value.clone(),
+                        option_value: 
KeyValueOptionKind::KeyValueOptions(Box::new(
+                            self.parse_key_value_options(true, &[])?,
+                        )),
+                    }),
+                }
+            }
             _ => self.expected("expected option value", self.peek_token()),
         }
     }
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index f46abc7d..b9434581 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -16761,11 +16761,13 @@ fn parse_create_user() {
     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(
+    let dialects = all_dialects_where(|d| d.supports_boolean_literals());
+    dialects.one_statement_parses_to(
         "CREATE OR REPLACE USER IF NOT EXISTS u1 PASSWORD='secret' 
MUST_CHANGE_PASSWORD=TRUE",
+        "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=false TYPE=SERVICE WITH TAG (t1='v1', 
t2='v2')");
     match create {
         Statement::CreateUser(stmt) => {
             assert_eq!(stmt.name, Ident::new("u1"));
@@ -16778,18 +16780,19 @@ fn parse_create_user() {
                     options: vec![
                         KeyValueOption {
                             option_name: "PASSWORD".to_string(),
-                            value: "secret".to_string(),
-                            option_type: KeyValueOptionType::STRING
+                            option_value: 
KeyValueOptionKind::Single(Value::SingleQuotedString(
+                                "secret".to_string()
+                            )),
                         },
                         KeyValueOption {
                             option_name: "MUST_CHANGE_PASSWORD".to_string(),
-                            value: "TRUE".to_string(),
-                            option_type: KeyValueOptionType::BOOLEAN
+                            option_value: 
KeyValueOptionKind::Single(Value::Boolean(false)),
                         },
                         KeyValueOption {
                             option_name: "TYPE".to_string(),
-                            value: "SERVICE".to_string(),
-                            option_type: KeyValueOptionType::ENUM
+                            option_value: 
KeyValueOptionKind::Single(Value::Placeholder(
+                                "SERVICE".to_string()
+                            )),
                         },
                     ],
                 },
@@ -16802,13 +16805,15 @@ fn parse_create_user() {
                     options: vec![
                         KeyValueOption {
                             option_name: "t1".to_string(),
-                            value: "v1".to_string(),
-                            option_type: KeyValueOptionType::STRING
+                            option_value: 
KeyValueOptionKind::Single(Value::SingleQuotedString(
+                                "v1".to_string()
+                            )),
                         },
                         KeyValueOption {
                             option_name: "t2".to_string(),
-                            value: "v2".to_string(),
-                            option_type: KeyValueOptionType::STRING
+                            option_value: 
KeyValueOptionKind::Single(Value::SingleQuotedString(
+                                "v2".to_string()
+                            )),
                         },
                     ]
                 }
@@ -17246,3 +17251,211 @@ fn parse_invisible_column() {
         _ => panic!("Unexpected statement {stmt}"),
     }
 }
+
+#[test]
+fn test_parse_alter_user() {
+    verified_stmt("ALTER USER u1");
+    verified_stmt("ALTER USER IF EXISTS u1");
+    let stmt = verified_stmt("ALTER USER IF EXISTS u1 RENAME TO u2");
+    match stmt {
+        Statement::AlterUser(alter) => {
+            assert!(alter.if_exists);
+            assert_eq!(alter.name, Ident::new("u1"));
+            assert_eq!(alter.rename_to, Some(Ident::new("u2")));
+        }
+        _ => unreachable!(),
+    }
+    verified_stmt("ALTER USER IF EXISTS u1 RESET PASSWORD");
+    verified_stmt("ALTER USER IF EXISTS u1 ABORT ALL QUERIES");
+    verified_stmt(
+        "ALTER USER IF EXISTS u1 ADD DELEGATED AUTHORIZATION OF ROLE r1 TO 
SECURITY INTEGRATION i1",
+    );
+    verified_stmt("ALTER USER IF EXISTS u1 REMOVE DELEGATED AUTHORIZATION OF 
ROLE r1 FROM SECURITY INTEGRATION i1");
+    verified_stmt(
+        "ALTER USER IF EXISTS u1 REMOVE DELEGATED AUTHORIZATIONS FROM SECURITY 
INTEGRATION i1",
+    );
+    verified_stmt("ALTER USER IF EXISTS u1 ENROLL MFA");
+    let stmt = verified_stmt("ALTER USER u1 SET DEFAULT_MFA_METHOD PASSKEY");
+    match stmt {
+        Statement::AlterUser(alter) => {
+            assert_eq!(alter.set_default_mfa_method, 
Some(MfaMethodKind::PassKey))
+        }
+        _ => unreachable!(),
+    }
+    verified_stmt("ALTER USER u1 SET DEFAULT_MFA_METHOD TOTP");
+    verified_stmt("ALTER USER u1 SET DEFAULT_MFA_METHOD DUO");
+    let stmt = verified_stmt("ALTER USER u1 REMOVE MFA METHOD PASSKEY");
+    match stmt {
+        Statement::AlterUser(alter) => {
+            assert_eq!(alter.remove_mfa_method, Some(MfaMethodKind::PassKey))
+        }
+        _ => unreachable!(),
+    }
+    verified_stmt("ALTER USER u1 REMOVE MFA METHOD TOTP");
+    verified_stmt("ALTER USER u1 REMOVE MFA METHOD DUO");
+    let stmt = verified_stmt("ALTER USER u1 MODIFY MFA METHOD PASSKEY SET 
COMMENT 'abc'");
+    match stmt {
+        Statement::AlterUser(alter) => {
+            assert_eq!(
+                alter.modify_mfa_method,
+                Some(AlterUserModifyMfaMethod {
+                    method: MfaMethodKind::PassKey,
+                    comment: "abc".to_string()
+                })
+            );
+        }
+        _ => unreachable!(),
+    }
+    verified_stmt("ALTER USER u1 ADD MFA METHOD OTP");
+    verified_stmt("ALTER USER u1 ADD MFA METHOD OTP COUNT = 8");
+
+    let stmt = verified_stmt("ALTER USER u1 SET AUTHENTICATION POLICY p1");
+    match stmt {
+        Statement::AlterUser(alter) => {
+            assert_eq!(
+                alter.set_policy,
+                Some(AlterUserSetPolicy {
+                    policy_kind: UserPolicyKind::Authentication,
+                    policy: Ident::new("p1")
+                })
+            );
+        }
+        _ => unreachable!(),
+    }
+    verified_stmt("ALTER USER u1 SET PASSWORD POLICY p1");
+    verified_stmt("ALTER USER u1 SET SESSION POLICY p1");
+    let stmt = verified_stmt("ALTER USER u1 UNSET AUTHENTICATION POLICY");
+    match stmt {
+        Statement::AlterUser(alter) => {
+            assert_eq!(alter.unset_policy, 
Some(UserPolicyKind::Authentication));
+        }
+        _ => unreachable!(),
+    }
+    verified_stmt("ALTER USER u1 UNSET PASSWORD POLICY");
+    verified_stmt("ALTER USER u1 UNSET SESSION POLICY");
+
+    let stmt = verified_stmt("ALTER USER u1 SET TAG k1='v1'");
+    match stmt {
+        Statement::AlterUser(alter) => {
+            assert_eq!(
+                alter.set_tag.options,
+                vec![KeyValueOption {
+                    option_name: "k1".to_string(),
+                    option_value: 
KeyValueOptionKind::Single(Value::SingleQuotedString(
+                        "v1".to_string()
+                    )),
+                },]
+            );
+        }
+        _ => unreachable!(),
+    }
+    verified_stmt("ALTER USER u1 SET TAG k1='v1', k2='v2'");
+    let stmt = verified_stmt("ALTER USER u1 UNSET TAG k1");
+    match stmt {
+        Statement::AlterUser(alter) => {
+            assert_eq!(alter.unset_tag, vec!["k1".to_string()]);
+        }
+        _ => unreachable!(),
+    }
+    verified_stmt("ALTER USER u1 UNSET TAG k1, k2, k3");
+
+    let dialects = all_dialects_where(|d| d.supports_boolean_literals());
+    dialects.one_statement_parses_to(
+        "ALTER USER u1 SET PASSWORD='secret', MUST_CHANGE_PASSWORD=TRUE, 
MINS_TO_UNLOCK=10",
+        "ALTER USER u1 SET PASSWORD='secret', MUST_CHANGE_PASSWORD=true, 
MINS_TO_UNLOCK=10",
+    );
+
+    let stmt = dialects.verified_stmt(
+        "ALTER USER u1 SET PASSWORD='secret', MUST_CHANGE_PASSWORD=true, 
MINS_TO_UNLOCK=10",
+    );
+    match stmt {
+        Statement::AlterUser(alter) => {
+            assert_eq!(
+                alter.set_props,
+                KeyValueOptions {
+                    delimiter: KeyValueOptionsDelimiter::Comma,
+                    options: vec![
+                        KeyValueOption {
+                            option_name: "PASSWORD".to_string(),
+                            option_value: 
KeyValueOptionKind::Single(Value::SingleQuotedString(
+                                "secret".to_string()
+                            )),
+                        },
+                        KeyValueOption {
+                            option_name: "MUST_CHANGE_PASSWORD".to_string(),
+                            option_value: 
KeyValueOptionKind::Single(Value::Boolean(true)),
+                        },
+                        KeyValueOption {
+                            option_name: "MINS_TO_UNLOCK".to_string(),
+                            option_value: 
KeyValueOptionKind::Single(number("10")),
+                        },
+                    ]
+                }
+            );
+        }
+        _ => unreachable!(),
+    }
+
+    let stmt = verified_stmt("ALTER USER u1 UNSET PASSWORD");
+    match stmt {
+        Statement::AlterUser(alter) => {
+            assert_eq!(alter.unset_props, vec!["PASSWORD".to_string()]);
+        }
+        _ => unreachable!(),
+    }
+    verified_stmt("ALTER USER u1 UNSET PASSWORD, MUST_CHANGE_PASSWORD, 
MINS_TO_UNLOCK");
+
+    let stmt = verified_stmt("ALTER USER u1 SET 
DEFAULT_SECONDARY_ROLES=('ALL')");
+    match stmt {
+        Statement::AlterUser(alter) => {
+            assert_eq!(
+                alter.set_props.options,
+                vec![KeyValueOption {
+                    option_name: "DEFAULT_SECONDARY_ROLES".to_string(),
+                    option_value: 
KeyValueOptionKind::Multi(vec![Value::SingleQuotedString(
+                        "ALL".to_string()
+                    )])
+                }]
+            );
+        }
+        _ => unreachable!(),
+    }
+    verified_stmt("ALTER USER u1 SET DEFAULT_SECONDARY_ROLES=()");
+    verified_stmt("ALTER USER u1 SET DEFAULT_SECONDARY_ROLES=('R1', 'R2', 
'R3')");
+    verified_stmt("ALTER USER u1 SET PASSWORD='secret', 
DEFAULT_SECONDARY_ROLES=('ALL')");
+    verified_stmt("ALTER USER u1 SET DEFAULT_SECONDARY_ROLES=('ALL'), 
PASSWORD='secret'");
+    let stmt = verified_stmt(
+        "ALTER USER u1 SET WORKLOAD_IDENTITY=(TYPE=AWS, 
ARN='arn:aws:iam::123456789:r1/')",
+    );
+    match stmt {
+        Statement::AlterUser(alter) => {
+            assert_eq!(
+                alter.set_props.options,
+                vec![KeyValueOption {
+                    option_name: "WORKLOAD_IDENTITY".to_string(),
+                    option_value: 
KeyValueOptionKind::KeyValueOptions(Box::new(KeyValueOptions {
+                        delimiter: KeyValueOptionsDelimiter::Comma,
+                        options: vec![
+                            KeyValueOption {
+                                option_name: "TYPE".to_string(),
+                                option_value: 
KeyValueOptionKind::Single(Value::Placeholder(
+                                    "AWS".to_string()
+                                )),
+                            },
+                            KeyValueOption {
+                                option_name: "ARN".to_string(),
+                                option_value: KeyValueOptionKind::Single(
+                                    Value::SingleQuotedString(
+                                        
"arn:aws:iam::123456789:r1/".to_string()
+                                    )
+                                ),
+                            },
+                        ]
+                    }))
+                }]
+            )
+        }
+        _ => unreachable!(),
+    }
+    verified_stmt("ALTER USER u1 SET DEFAULT_SECONDARY_ROLES=('ALL'), 
PASSWORD='secret', WORKLOAD_IDENTITY=(TYPE=AWS, 
ARN='arn:aws:iam::123456789:r1/')");
+}
diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs
index 7c9e5261..e04bfaf5 100644
--- a/tests/sqlparser_snowflake.rs
+++ b/tests/sqlparser_snowflake.rs
@@ -19,7 +19,7 @@
 //! Test SQL syntax specific to Snowflake. The parser based on the
 //! generic dialect is also tested (on the inputs it can handle).
 
-use sqlparser::ast::helpers::key_value_options::{KeyValueOption, 
KeyValueOptionType};
+use sqlparser::ast::helpers::key_value_options::{KeyValueOption, 
KeyValueOptionKind};
 use sqlparser::ast::helpers::stmt_data_loading::{StageLoadSelectItem, 
StageLoadSelectItemKind};
 use sqlparser::ast::*;
 use sqlparser::dialect::{Dialect, GenericDialect, SnowflakeDialect};
@@ -2116,23 +2116,27 @@ fn test_create_stage_with_stage_params() {
             );
             assert!(stage_params.credentials.options.contains(&KeyValueOption {
                 option_name: "AWS_KEY_ID".to_string(),
-                option_type: KeyValueOptionType::STRING,
-                value: "1a2b3c".to_string()
+                option_value: 
KeyValueOptionKind::Single(Value::SingleQuotedString(
+                    "1a2b3c".to_string()
+                )),
             }));
             assert!(stage_params.credentials.options.contains(&KeyValueOption {
                 option_name: "AWS_SECRET_KEY".to_string(),
-                option_type: KeyValueOptionType::STRING,
-                value: "4x5y6z".to_string()
+                option_value: 
KeyValueOptionKind::Single(Value::SingleQuotedString(
+                    "4x5y6z".to_string()
+                )),
             }));
             assert!(stage_params.encryption.options.contains(&KeyValueOption {
                 option_name: "MASTER_KEY".to_string(),
-                option_type: KeyValueOptionType::STRING,
-                value: "key".to_string()
+                option_value: 
KeyValueOptionKind::Single(Value::SingleQuotedString(
+                    "key".to_string()
+                )),
             }));
             assert!(stage_params.encryption.options.contains(&KeyValueOption {
                 option_name: "TYPE".to_string(),
-                option_type: KeyValueOptionType::STRING,
-                value: "AWS_SSE_KMS".to_string()
+                option_value: 
KeyValueOptionKind::Single(Value::SingleQuotedString(
+                    "AWS_SSE_KMS".to_string()
+                )),
             }));
         }
         _ => unreachable!(),
@@ -2146,7 +2150,7 @@ fn test_create_stage_with_directory_table_params() {
     let sql = concat!(
         "CREATE OR REPLACE STAGE my_ext_stage ",
         "URL='s3://load/files/' ",
-        "DIRECTORY=(ENABLE=TRUE REFRESH_ON_CREATE=FALSE 
NOTIFICATION_INTEGRATION='some-string')"
+        "DIRECTORY=(ENABLE=true REFRESH_ON_CREATE=false 
NOTIFICATION_INTEGRATION='some-string')"
     );
 
     match snowflake().verified_stmt(sql) {
@@ -2156,18 +2160,17 @@ fn test_create_stage_with_directory_table_params() {
         } => {
             assert!(directory_table_params.options.contains(&KeyValueOption {
                 option_name: "ENABLE".to_string(),
-                option_type: KeyValueOptionType::BOOLEAN,
-                value: "TRUE".to_string()
+                option_value: KeyValueOptionKind::Single(Value::Boolean(true)),
             }));
             assert!(directory_table_params.options.contains(&KeyValueOption {
                 option_name: "REFRESH_ON_CREATE".to_string(),
-                option_type: KeyValueOptionType::BOOLEAN,
-                value: "FALSE".to_string()
+                option_value: 
KeyValueOptionKind::Single(Value::Boolean(false)),
             }));
             assert!(directory_table_params.options.contains(&KeyValueOption {
                 option_name: "NOTIFICATION_INTEGRATION".to_string(),
-                option_type: KeyValueOptionType::STRING,
-                value: "some-string".to_string()
+                option_value: 
KeyValueOptionKind::Single(Value::SingleQuotedString(
+                    "some-string".to_string()
+                )),
             }));
         }
         _ => unreachable!(),
@@ -2187,18 +2190,17 @@ fn test_create_stage_with_file_format() {
         Statement::CreateStage { file_format, .. } => {
             assert!(file_format.options.contains(&KeyValueOption {
                 option_name: "COMPRESSION".to_string(),
-                option_type: KeyValueOptionType::ENUM,
-                value: "AUTO".to_string()
+                option_value: 
KeyValueOptionKind::Single(Value::Placeholder("AUTO".to_string())),
             }));
             assert!(file_format.options.contains(&KeyValueOption {
                 option_name: "BINARY_FORMAT".to_string(),
-                option_type: KeyValueOptionType::ENUM,
-                value: "HEX".to_string()
+                option_value: 
KeyValueOptionKind::Single(Value::Placeholder("HEX".to_string())),
             }));
             assert!(file_format.options.contains(&KeyValueOption {
                 option_name: "ESCAPE".to_string(),
-                option_type: KeyValueOptionType::STRING,
-                value: r#"\\"#.to_string()
+                option_value: 
KeyValueOptionKind::Single(Value::SingleQuotedString(
+                    r#"\\"#.to_string()
+                )),
             }));
         }
         _ => unreachable!(),
@@ -2214,19 +2216,19 @@ fn test_create_stage_with_copy_options() {
     let sql = concat!(
         "CREATE OR REPLACE STAGE my_ext_stage ",
         "URL='s3://load/files/' ",
-        "COPY_OPTIONS=(ON_ERROR=CONTINUE FORCE=TRUE)"
+        "COPY_OPTIONS=(ON_ERROR=CONTINUE FORCE=true)"
     );
     match snowflake().verified_stmt(sql) {
         Statement::CreateStage { copy_options, .. } => {
             assert!(copy_options.options.contains(&KeyValueOption {
                 option_name: "ON_ERROR".to_string(),
-                option_type: KeyValueOptionType::ENUM,
-                value: "CONTINUE".to_string()
+                option_value: KeyValueOptionKind::Single(Value::Placeholder(
+                    "CONTINUE".to_string()
+                )),
             }));
             assert!(copy_options.options.contains(&KeyValueOption {
                 option_name: "FORCE".to_string(),
-                option_type: KeyValueOptionType::BOOLEAN,
-                value: "TRUE".to_string()
+                option_value: KeyValueOptionKind::Single(Value::Boolean(true)),
             }));
         }
         _ => unreachable!(),
@@ -2357,23 +2359,27 @@ fn test_copy_into_with_stage_params() {
             );
             assert!(stage_params.credentials.options.contains(&KeyValueOption {
                 option_name: "AWS_KEY_ID".to_string(),
-                option_type: KeyValueOptionType::STRING,
-                value: "1a2b3c".to_string()
+                option_value: 
KeyValueOptionKind::Single(Value::SingleQuotedString(
+                    "1a2b3c".to_string()
+                )),
             }));
             assert!(stage_params.credentials.options.contains(&KeyValueOption {
                 option_name: "AWS_SECRET_KEY".to_string(),
-                option_type: KeyValueOptionType::STRING,
-                value: "4x5y6z".to_string()
+                option_value: 
KeyValueOptionKind::Single(Value::SingleQuotedString(
+                    "4x5y6z".to_string()
+                )),
             }));
             assert!(stage_params.encryption.options.contains(&KeyValueOption {
                 option_name: "MASTER_KEY".to_string(),
-                option_type: KeyValueOptionType::STRING,
-                value: "key".to_string()
+                option_value: 
KeyValueOptionKind::Single(Value::SingleQuotedString(
+                    "key".to_string()
+                )),
             }));
             assert!(stage_params.encryption.options.contains(&KeyValueOption {
                 option_name: "TYPE".to_string(),
-                option_type: KeyValueOptionType::STRING,
-                value: "AWS_SSE_KMS".to_string()
+                option_value: 
KeyValueOptionKind::Single(Value::SingleQuotedString(
+                    "AWS_SSE_KMS".to_string()
+                )),
             }));
         }
         _ => unreachable!(),
@@ -2524,18 +2530,17 @@ fn test_copy_into_file_format() {
         Statement::CopyIntoSnowflake { file_format, .. } => {
             assert!(file_format.options.contains(&KeyValueOption {
                 option_name: "COMPRESSION".to_string(),
-                option_type: KeyValueOptionType::ENUM,
-                value: "AUTO".to_string()
+                option_value: 
KeyValueOptionKind::Single(Value::Placeholder("AUTO".to_string())),
             }));
             assert!(file_format.options.contains(&KeyValueOption {
                 option_name: "BINARY_FORMAT".to_string(),
-                option_type: KeyValueOptionType::ENUM,
-                value: "HEX".to_string()
+                option_value: 
KeyValueOptionKind::Single(Value::Placeholder("HEX".to_string())),
             }));
             assert!(file_format.options.contains(&KeyValueOption {
                 option_name: "ESCAPE".to_string(),
-                option_type: KeyValueOptionType::STRING,
-                value: r#"\\"#.to_string()
+                option_value: 
KeyValueOptionKind::Single(Value::SingleQuotedString(
+                    r#"\\"#.to_string()
+                )),
             }));
         }
         _ => unreachable!(),
@@ -2563,18 +2568,17 @@ fn test_copy_into_file_format() {
         Statement::CopyIntoSnowflake { file_format, .. } => {
             assert!(file_format.options.contains(&KeyValueOption {
                 option_name: "COMPRESSION".to_string(),
-                option_type: KeyValueOptionType::ENUM,
-                value: "AUTO".to_string()
+                option_value: 
KeyValueOptionKind::Single(Value::Placeholder("AUTO".to_string())),
             }));
             assert!(file_format.options.contains(&KeyValueOption {
                 option_name: "BINARY_FORMAT".to_string(),
-                option_type: KeyValueOptionType::ENUM,
-                value: "HEX".to_string()
+                option_value: 
KeyValueOptionKind::Single(Value::Placeholder("HEX".to_string())),
             }));
             assert!(file_format.options.contains(&KeyValueOption {
                 option_name: "ESCAPE".to_string(),
-                option_type: KeyValueOptionType::STRING,
-                value: r#"\\"#.to_string()
+                option_value: 
KeyValueOptionKind::Single(Value::SingleQuotedString(
+                    r#"\\"#.to_string()
+                )),
             }));
         }
         _ => unreachable!(),
@@ -2588,20 +2592,20 @@ fn test_copy_into_copy_options() {
         "FROM 'gcs://mybucket/./../a.csv' ",
         "FILES = ('file1.json', 'file2.json') ",
         "PATTERN = '.*employees0[1-5].csv.gz' ",
-        "COPY_OPTIONS=(ON_ERROR=CONTINUE FORCE=TRUE)"
+        "COPY_OPTIONS=(ON_ERROR=CONTINUE FORCE=true)"
     );
 
     match snowflake().verified_stmt(sql) {
         Statement::CopyIntoSnowflake { copy_options, .. } => {
             assert!(copy_options.options.contains(&KeyValueOption {
                 option_name: "ON_ERROR".to_string(),
-                option_type: KeyValueOptionType::ENUM,
-                value: "CONTINUE".to_string()
+                option_value: KeyValueOptionKind::Single(Value::Placeholder(
+                    "CONTINUE".to_string()
+                )),
             }));
             assert!(copy_options.options.contains(&KeyValueOption {
                 option_name: "FORCE".to_string(),
-                option_type: KeyValueOptionType::BOOLEAN,
-                value: "TRUE".to_string()
+                option_value: KeyValueOptionKind::Single(Value::Boolean(true)),
             }));
         }
         _ => unreachable!(),
@@ -3863,17 +3867,20 @@ fn test_alter_session() {
         "sql parser error: expected at least one option"
     );
 
-    snowflake().verified_stmt("ALTER SESSION SET AUTOCOMMIT=TRUE");
-    snowflake().verified_stmt("ALTER SESSION SET AUTOCOMMIT=FALSE 
QUERY_TAG='tag'");
+    snowflake().one_statement_parses_to(
+        "ALTER SESSION SET AUTOCOMMIT=TRUE",
+        "ALTER SESSION SET AUTOCOMMIT=true",
+    );
+    snowflake().verified_stmt("ALTER SESSION SET AUTOCOMMIT=false 
QUERY_TAG='tag'");
     snowflake().verified_stmt("ALTER SESSION UNSET AUTOCOMMIT");
     snowflake().verified_stmt("ALTER SESSION UNSET AUTOCOMMIT, QUERY_TAG");
     snowflake().one_statement_parses_to(
         "ALTER SESSION SET A=false, B='tag';",
-        "ALTER SESSION SET A=FALSE B='tag'",
+        "ALTER SESSION SET A=false B='tag'",
     );
     snowflake().one_statement_parses_to(
         "ALTER SESSION SET A=true \nB='tag'",
-        "ALTER SESSION SET A=TRUE B='tag'",
+        "ALTER SESSION SET A=true B='tag'",
     );
     snowflake().one_statement_parses_to("ALTER SESSION UNSET a\nB", "ALTER 
SESSION UNSET a, B");
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to