Repository: apache/datafusion-sqlparser-rs (Rust)

Issue:
Parse MySQL `SET GLOBAL` variables
In addition to `SESSION` and `LOCAL` qualifiers, MySQL allows `GLOBAL` for modifying system variables:

```
mysql> SET GLOBAL max_connections = 1000;
Query OK, 0 rows affected (0.00 sec)
```

sqlparser currently interprets `global` as a variable name and fails to parse:

```
$ echo -n "SET GLOBAL max_connections = 1000;" | cargo run --example cli - --mysql
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/examples/cli - --mysql`
Parsing from stdin using MySqlDialect
2025-01-31T00:44:22.858Z DEBUG [sqlparser::parser] Parsing sql 'SET GLOBAL max_connections = 1000;'...
Error during parsing: ParserError("Expected: equals sign or TO, found: max_connections at Line: 1, Column: 12")
```


Discussion: As mentioned in #1696, this may be best accomplished in combination with #1697; possibly this issue should just be closed in favor of that one.



Fix (patch):
diff --git a/src/ast/mod.rs b/src/ast/mod.rs
index e5e4aef05..8ab3fc0f1 100644
--- a/src/ast/mod.rs
+++ b/src/ast/mod.rs
@@ -2394,6 +2394,168 @@ pub enum CreatePolicyCommand {
     Delete,
 }
 
+#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub enum Set {
+    /// SQL Standard-style
+    /// SET a = 1;
+    SingleAssignment {
+        local: bool,
+        hivevar: bool,
+        variable: ObjectName,
+        values: Vec<Expr>,
+    },
+    /// Snowflake-style
+    /// SET (a, b, ..) = (1, 2, ..);
+    ParenthesizedAssignments {
+        variables: Vec<ObjectName>,
+        values: Vec<Expr>,
+    },
+    /// MySQL-style
+    /// SET a = 1, b = 2, ..;
+    MultipleAssignments { assignments: Vec<SetAssignment> },
+    /// MS-SQL session
+    ///
+    /// See <https://learn.microsoft.com/en-us/sql/t-sql/statements/set-statements-transact-sql>
+    SetSessionParam(SetSessionParamKind),
+    /// ```sql
+    /// SET [ SESSION | LOCAL ] ROLE role_name
+    /// ```
+    ///
+    /// Sets session state. Examples: [ANSI][1], [Postgresql][2], [MySQL][3], and [Oracle][4]
+    ///
+    /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#set-role-statement
+    /// [2]: https://www.postgresql.org/docs/14/sql-set-role.html
+    /// [3]: https://dev.mysql.com/doc/refman/8.0/en/set-role.html
+    /// [4]: https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_10004.htm
+    SetRole {
+        /// Non-ANSI optional identifier to inform if the role is defined inside the current session (`SESSION`) or transaction (`LOCAL`).
+        context_modifier: ContextModifier,
+        /// Role name. If NONE is specified, then the current role name is removed.
+        role_name: Option<Ident>,
+    },
+    /// ```sql
+    /// SET TIME ZONE <value>
+    /// ```
+    ///
+    /// Note: this is a PostgreSQL-specific statements
+    /// `SET TIME ZONE <value>` is an alias for `SET timezone TO <value>` in PostgreSQL
+    /// However, we allow it for all dialects.
+    SetTimeZone { local: bool, value: Expr },
+    /// ```sql
+    /// SET NAMES 'charset_name' [COLLATE 'collation_name']
+    /// ```
+    SetNames {
+        charset_name: Ident,
+        collation_name: Option<String>,
+    },
+    /// ```sql
+    /// SET NAMES DEFAULT
+    /// ```
+    ///
+    /// Note: this is a MySQL-specific statement.
+    SetNamesDefault {},
+    /// ```sql
+    /// SET TRANSACTION ...
+    /// ```
+    SetTransaction {
+        modes: Vec<TransactionMode>,
+        snapshot: Option<Value>,
+        session: bool,
+    },
+}
+
+impl Display for Set {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::ParenthesizedAssignments { variables, values } => write!(
+                f,
+                "SET ({}) = ({})",
+                display_comma_separated(variables),
+                display_comma_separated(values)
+            ),
+            Self::MultipleAssignments { assignments } => {
+                write!(f, "SET {}", display_comma_separated(assignments))
+            }
+            Self::SetRole {
+                context_modifier,
+                role_name,
+            } => {
+                let role_name = role_name.clone().unwrap_or_else(|| Ident::new("NONE"));
+                write!(f, "SET{context_modifier} ROLE {role_name}")
+            }
+            Self::SetSessionParam(kind) => write!(f, "SET {kind}"),
+            Self::SetTransaction {
+                modes,
+                snapshot,
+                session,
+            } => {
+                if *session {
+                    write!(f, "SET SESSION CHARACTERISTICS AS TRANSACTION")?;
+                } else {
+                    write!(f, "SET TRANSACTION")?;
+                }
+                if !modes.is_empty() {
+                    write!(f, " {}", display_comma_separated(modes))?;
+                }
+                if let Some(snapshot_id) = snapshot {
+                    write!(f, " SNAPSHOT {snapshot_id}")?;
+                }
+                Ok(())
+            }
+            Self::SetTimeZone { local, value } => {
+                f.write_str("SET ")?;
+                if *local {
+                    f.write_str("LOCAL ")?;
+                }
+                write!(f, "TIME ZONE {value}")
+            }
+            Self::SetNames {
+                charset_name,
+                collation_name,
+            } => {
+                write!(f, "SET NAMES {}", charset_name)?;
+
+                if let Some(collation) = collation_name {
+                    f.write_str(" COLLATE ")?;
+                    f.write_str(collation)?;
+                };
+
+                Ok(())
+            }
+            Self::SetNamesDefault {} => {
+                f.write_str("SET NAMES DEFAULT")?;
+
+                Ok(())
+            }
+            Set::SingleAssignment {
+                local,
+                hivevar,
+                variable,
+                values,
+            } => {
+                write!(
+                    f,
+                    "SET {}{}{} = {}",
+                    if *local { "LOCAL " } else { "" },
+                    if *hivevar { "HIVEVAR:" } else { "" },
+                    variable,
+                    display_comma_separated(values)
+                )
+            }
+        }
+    }
+}
+
+/// Convert a `Set` into a `Statement`.
+/// Convenience function, instead of writing `Statement::Set(Set::Set...{...})`
+impl From<Set> for Statement {
+    fn from(set: Set) -> Self {
+        Statement::Set(set)
+    }
+}
+
 /// A top-level statement (SELECT, INSERT, CREATE, etc.)
 #[allow(clippy::large_enum_variant)]
 #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
@@ -2419,6 +2581,7 @@ pub enum Statement {
         compute_statistics: bool,
         has_table_keyword: bool,
     },
+    Set(Set),
     /// ```sql
     /// TRUNCATE
     /// ```
@@ -2846,7 +3009,10 @@ pub enum Statement {
     /// DROP CONNECTOR
     /// ```
     /// See [Hive](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27362034#LanguageManualDDL-DropConnector)
-    DropConnector { if_exists: bool, name: Ident },
+    DropConnector {
+        if_exists: bool,
+        name: Ident,
+    },
     /// ```sql
     /// DECLARE
     /// ```
@@ -2854,7 +3020,9 @@ pub enum Statement {
     ///
     /// Note: this is a PostgreSQL-specific statement,
     /// but may also compatible with other SQL.
-    Declare { stmts: Vec<Declare> },
+    Declare {
+        stmts: Vec<Declare>,
+    },
     /// ```sql
     /// CREATE EXTENSION [ IF NOT EXISTS ] extension_name
     ///     [ WITH ] [ SCHEMA schema_name ]
@@ -2916,67 +3084,23 @@ pub enum Statement {
     ///
     /// Note: this is a PostgreSQL-specific statement,
     /// but may also compatible with other SQL.
-    Discard { object_type: DiscardObject },
-    /// ```sql
-    /// SET [ SESSION | LOCAL ] ROLE role_name
-    /// ```
-    ///
-    /// Sets session state. Examples: [ANSI][1], [Postgresql][2], [MySQL][3], and [Oracle][4]
-    ///
-    /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#set-role-statement
-    /// [2]: https://www.postgresql.org/docs/14/sql-set-role.html
-    /// [3]: https://dev.mysql.com/doc/refman/8.0/en/set-role.html
-    /// [4]: https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_10004.htm
-    SetRole {
-        /// Non-ANSI optional identifier to inform if the role is defined inside the current session (`SESSION`) or transaction (`LOCAL`).
-        context_modifier: ContextModifier,
-        /// Role name. If NONE is specified, then the current role name is removed.
-        role_name: Option<Ident>,
-    },
-    /// ```sql
-    /// SET <variable> = expression;
-    /// SET (variable[, ...]) = (expression[, ...]);
-    /// ```
-    ///
-    /// Note: this is not a standard SQL statement, but it is supported by at
-    /// least MySQL and PostgreSQL. Not all MySQL-specific syntactic forms are
-    /// supported yet.
-    SetVariable {
-        local: bool,
-        hivevar: bool,
-        variables: OneOrManyWithParens<ObjectName>,
-        value: Vec<Expr>,
+    Discard {
+        object_type: DiscardObject,
     },
-    /// ```sql
-    /// SET TIME ZONE <value>
-    /// ```
-    ///
-    /// Note: this is a PostgreSQL-specific statements
-    /// `SET TIME ZONE <value>` is an alias for `SET timezone TO <value>` in PostgreSQL
-    SetTimeZone { local: bool, value: Expr },
-    /// ```sql
-    /// SET NAMES 'charset_name' [COLLATE 'collation_name']
-    /// ```
-    SetNames {
-        charset_name: Ident,
-        collation_name: Option<String>,
-    },
-    /// ```sql
-    /// SET NAMES DEFAULT
-    /// ```
-    ///
-    /// Note: this is a MySQL-specific statement.
-    SetNamesDefault {},
     /// `SHOW FUNCTIONS`
     ///
     /// Note: this is a Presto-specific statement.
-    ShowFunctions { filter: Option<ShowStatementFilter> },
+    ShowFunctions {
+        filter: Option<ShowStatementFilter>,
+    },
     /// ```sql
     /// SHOW <variable>
     /// ```
     ///
     /// Note: this is a PostgreSQL-specific statement.
-    ShowVariable { variable: Vec<Ident> },
+    ShowVariable {
+        variable: Vec<Ident>,
+    },
     /// ```sql
     /// SHOW [GLOBAL | SESSION] STATUS [LIKE 'pattern' | WHERE expr]
     /// ```
@@ -3060,7 +3184,9 @@ pub enum Statement {
     /// ```
     ///
     /// Note: this is a MySQL-specific statement.
-    ShowCollation { filter: Option<ShowStatementFilter> },
+    ShowCollation {
+        filter: Option<ShowStatementFilter>,
+    },
     /// ```sql
     /// `USE ...`
     /// ```
@@ -3103,14 +3229,6 @@ pub enum Statement {
         has_end_keyword: bool,
     },
     /// ```sql
-    /// SET TRANSACTION ...
-    /// ```
-    SetTransaction {
-        modes: Vec<TransactionMode>,
-        snapshot: Option<Value>,
-        session: bool,
-    },
-    /// ```sql
     /// COMMENT ON ...
     /// ```
     ///
@@ -3329,7 +3447,10 @@ pub enum Statement {
     /// ```
     ///
     /// Note: this is a PostgreSQL-specific statement.
-    Deallocate { name: Ident, prepare: bool },
+    Deallocate {
+        name: Ident,
+        prepare: bool,
+    },
     /// ```sql
     /// An `EXECUTE` statement
     /// ```
@@ -3415,11 +3536,15 @@ pub enum Statement {
     /// SAVEPOINT
     /// ```
     /// Define a new savepoint within the current transaction
-    Savepoint { name: Ident },
+    Savepoint {
+        name: Ident,
+    },
     /// ```sql
     /// RELEASE [ SAVEPOINT ] savepoint_name
     /// ```
-    ReleaseSavepoint { name: Ident },
+    ReleaseSavepoint {
+        name: Ident,
+    },
     /// A `MERGE` statement.
     ///
     /// ```sql
@@ -3499,7 +3624,9 @@ pub enum Statement {
     /// LOCK TABLES <table_name> [READ [LOCAL] | [LOW_PRIORITY] WRITE]
     /// ```
     /// Note: this is a MySQL-specific statement. See <https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html>
-    LockTables { tables: Vec<LockTable> },
+    LockTables {
+        tables: Vec<LockTable>,
+    },
     /// ```sql
     /// UNLOCK TABLES
     /// ```
@@ -3533,14 +3660,18 @@ pub enum Statement {
     /// listen for a notification channel
     ///
     /// See Postgres <https://www.postgresql.org/docs/current/sql-listen.html>
-    LISTEN { channel: Ident },
+    LISTEN {
+        channel: Ident,
+    },
     /// ```sql
     /// UNLISTEN
     /// ```
     /// stop listening for a notification
     ///
     /// See Postgres <https://www.postgresql.org/docs/current/sql-unlisten.html>
-    UNLISTEN { channel: Ident },
+    UNLISTEN {
+        channel: Ident,
+    },
     /// ```sql
     /// NOTIFY channel [ , payload ]
     /// ```
@@ -3580,10 +3711,6 @@ pub enum Statement {
     /// Snowflake `REMOVE`
     /// See: <https://docs.snowflake.com/en/sql-reference/sql/remove>
     Remove(FileStagingCommand),
-    /// MS-SQL session
-    ///
-    /// See <https://learn.microsoft.com/en-us/sql/t-sql/statements/set-statements-transact-sql>
-    SetSessionParam(SetSessionParamKind),
     /// RaiseError (MSSQL)
     /// RAISERROR ( { msg_id | msg_str | @local_variable }
     /// { , severity , state }
@@ -4644,59 +4771,7 @@ impl fmt::Display for Statement {
                 write!(f, "DISCARD {object_type}")?;
                 Ok(())
             }
-            Self::SetRole {
-                context_modifier,
-                role_name,
-            } => {
-                let role_name = role_name.clone().unwrap_or_else(|| Ident::new("NONE"));
-                write!(f, "SET{context_modifier} ROLE {role_name}")
-            }
-            Statement::SetVariable {
-                local,
-                variables,
-                hivevar,
-                value,
-            } => {
-                f.write_str("SET ")?;
-                if *local {
-                    f.write_str("LOCAL ")?;
-                }
-                let parenthesized = matches!(variables, OneOrManyWithParens::Many(_));
-                write!(
-                    f,
-                    "{hivevar}{name} = {l_paren}{value}{r_paren}",
-                    hivevar = if *hivevar { "HIVEVAR:" } else { "" },
-                    name = variables,
-                    l_paren = parenthesized.then_some("(").unwrap_or_default(),
-                    value = display_comma_separated(value),
-                    r_paren = parenthesized.then_some(")").unwrap_or_default(),
-                )
-            }
-            Statement::SetTimeZone { local, value } => {
-                f.write_str("SET ")?;
-                if *local {
-                    f.write_str("LOCAL ")?;
-                }
-                write!(f, "TIME ZONE {value}")
-            }
-            Statement::SetNames {
-                charset_name,
-                collation_name,
-            } => {
-                write!(f, "SET NAMES {}", charset_name)?;
-
-                if let Some(collation) = collation_name {
-                    f.write_str(" COLLATE ")?;
-                    f.write_str(collation)?;
-                };
-
-                Ok(())
-            }
-            Statement::SetNamesDefault {} => {
-                f.write_str("SET NAMES DEFAULT")?;
-
-                Ok(())
-            }
+            Self::Set(set) => write!(f, "{set}"),
             Statement::ShowVariable { variable } => {
                 write!(f, "SHOW")?;
                 if !variable.is_empty() {
@@ -4885,24 +4960,6 @@ impl fmt::Display for Statement {
                 }
                 Ok(())
             }
-            Statement::SetTransaction {
-                modes,
-                snapshot,
-                session,
-            } => {
-                if *session {
-                    write!(f, "SET SESSION CHARACTERISTICS AS TRANSACTION")?;
-                } else {
-                    write!(f, "SET TRANSACTION")?;
-                }
-                if !modes.is_empty() {
-                    write!(f, " {}", display_comma_separated(modes))?;
-                }
-                if let Some(snapshot_id) = snapshot {
-                    write!(f, " SNAPSHOT {snapshot_id}")?;
-                }
-                Ok(())
-            }
             Statement::Commit {
                 chain,
                 end: end_syntax,
@@ -5333,7 +5390,6 @@ impl fmt::Display for Statement {
 
             Statement::List(command) => write!(f, "LIST {command}"),
             Statement::Remove(command) => write!(f, "REMOVE {command}"),
-            Statement::SetSessionParam(kind) => write!(f, "SET {kind}"),
         }
     }
 }
@@ -5397,6 +5453,21 @@ impl fmt::Display for SequenceOptions {
     }
 }
 
+/// Assignment for a `SET` statement (name [=|TO] value)
+#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub struct SetAssignment {
+    pub name: ObjectName,
+    pub value: Expr,
+}
+
+impl fmt::Display for SetAssignment {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{} = {}", self.name, self.value)
+    }
+}
+
 /// Target of a `TRUNCATE TABLE` command
 ///
 /// Note this is its own struct because `visit_relation` requires an `ObjectName` (not a `Vec<ObjectName>`)
diff --git a/src/ast/spans.rs b/src/ast/spans.rs
index 0a64fb8ea..da5ee2f02 100644
--- a/src/ast/spans.rs
+++ b/src/ast/spans.rs
@@ -229,11 +229,7 @@ impl Spanned for Values {
 /// - [Statement::Fetch]
 /// - [Statement::Flush]
 /// - [Statement::Discard]
-/// - [Statement::SetRole]
-/// - [Statement::SetVariable]
-/// - [Statement::SetTimeZone]
-/// - [Statement::SetNames]
-/// - [Statement::SetNamesDefault]
+/// - [Statement::Set]
 /// - [Statement::ShowFunctions]
 /// - [Statement::ShowVariable]
 /// - [Statement::ShowStatus]
@@ -243,7 +239,6 @@ impl Spanned for Values {
 /// - [Statement::ShowTables]
 /// - [Statement::ShowCollation]
 /// - [Statement::StartTransaction]
-/// - [Statement::SetTransaction]
 /// - [Statement::Comment]
 /// - [Statement::Commit]
 /// - [Statement::Rollback]
@@ -444,11 +439,7 @@ impl Spanned for Statement {
             Statement::Fetch { .. } => Span::empty(),
             Statement::Flush { .. } => Span::empty(),
             Statement::Discard { .. } => Span::empty(),
-            Statement::SetRole { .. } => Span::empty(),
-            Statement::SetVariable { .. } => Span::empty(),
-            Statement::SetTimeZone { .. } => Span::empty(),
-            Statement::SetNames { .. } => Span::empty(),
-            Statement::SetNamesDefault {} => Span::empty(),
+            Statement::Set(_) => Span::empty(),
             Statement::ShowFunctions { .. } => Span::empty(),
             Statement::ShowVariable { .. } => Span::empty(),
             Statement::ShowStatus { .. } => Span::empty(),
@@ -459,7 +450,6 @@ impl Spanned for Statement {
             Statement::ShowCollation { .. } => Span::empty(),
             Statement::Use(u) => u.span(),
             Statement::StartTransaction { .. } => Span::empty(),
-            Statement::SetTransaction { .. } => Span::empty(),
             Statement::Comment { .. } => Span::empty(),
             Statement::Commit { .. } => Span::empty(),
             Statement::Rollback { .. } => Span::empty(),
@@ -508,7 +498,6 @@ impl Spanned for Statement {
             Statement::RenameTable { .. } => Span::empty(),
             Statement::RaisError { .. } => Span::empty(),
             Statement::List(..) | Statement::Remove(..) => Span::empty(),
-            Statement::SetSessionParam { .. } => Span::empty(),
         }
     }
 }
diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs
index aeb097cfd..8d4557e2f 100644
--- a/src/dialect/mod.rs
+++ b/src/dialect/mod.rs
@@ -399,6 +399,16 @@ pub trait Dialect: Debug + Any {
         false
     }
 
+    /// Returns true if the dialect supports multiple `SET` statements
+    /// in a single statement.
+    ///
+    /// ```sql
+    /// SET variable = expression [, variable = expression];
+    /// ```
+    fn supports_comma_separated_set_assignments(&self) -> bool {
+        false
+    }
+
     /// Returns true if the dialect supports an `EXCEPT` clause following a
     /// wildcard in a select list.
     ///
diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs
index aeed1eb79..3db34748e 100644
--- a/src/dialect/mssql.rs
+++ b/src/dialect/mssql.rs
@@ -82,6 +82,7 @@ impl Dialect for MsSqlDialect {
     fn supports_start_transaction_modifier(&self) -> bool {
         true
     }
+
     fn supports_end_transaction_modifier(&self) -> bool {
         true
     }
diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs
index 0bdfc9bf3..2077ea195 100644
--- a/src/dialect/mysql.rs
+++ b/src/dialect/mysql.rs
@@ -141,6 +141,10 @@ impl Dialect for MySqlDialect {
     fn supports_set_names(&self) -> bool {
         true
     }
+
+    fn supports_comma_separated_set_assignments(&self) -> bool {
+        true
+    }
 }
 
 /// `LOCK TABLES`
diff --git a/src/keywords.rs b/src/keywords.rs
index bda817df9..195bbb172 100644
--- a/src/keywords.rs
+++ b/src/keywords.rs
@@ -173,6 +173,7 @@ define_keywords!(
     CHANNEL,
     CHAR,
     CHARACTER,
+    CHARACTERISTICS,
     CHARACTERS,
     CHARACTER_LENGTH,
     CHARSET,
@@ -557,6 +558,7 @@ define_keywords!(
     MULTISET,
     MUTATION,
     NAME,
+    NAMES,
     NANOSECOND,
     NANOSECONDS,
     NATIONAL,
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index b34415388..8791808e0 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -4314,7 +4314,8 @@ impl<'a> Parser<'a> {
     }
 
     /// Run a parser method `f`, reverting back to the current position if unsuccessful.
-    /// Returns `None` if `f` returns an error
+    /// Returns `ParserError::RecursionLimitExceeded` if `f` returns a `RecursionLimitExceeded`.
+    /// Returns `Ok(None)` if `f` returns any other error.
     pub fn maybe_parse<T, F>(&mut self, f: F) -> Result<Option<T>, ParserError>
     where
         F: FnMut(&mut Parser) -> Result<T, ParserError>,
@@ -10955,47 +10956,108 @@ impl<'a> Parser<'a> {
         } else {
             Some(self.parse_identifier()?)
         };
-        Ok(Statement::SetRole {
+        Ok(Statement::Set(Set::SetRole {
             context_modifier,
             role_name,
-        })
+        }))
     }
 
-    pub fn parse_set(&mut self) -> Result<Statement, ParserError> {
-        let modifier =
-            self.parse_one_of_keywords(&[Keyword::SESSION, Keyword::LOCAL, Keyword::HIVEVAR]);
-        if let Some(Keyword::HIVEVAR) = modifier {
-            self.expect_token(&Token::Colon)?;
-        } else if let Some(set_role_stmt) =
-            self.maybe_parse(|parser| parser.parse_set_role(modifier))?
-        {
-            return Ok(set_role_stmt);
+    fn parse_set_values(
+        &mut self,
+        parenthesized_assignment: bool,
+    ) -> Result<Vec<Expr>, ParserError> {
+        let mut values = vec![];
+
+        if parenthesized_assignment {
+            self.expect_token(&Token::LParen)?;
+        }
+
+        loop {
+            let value = if let Some(expr) = self.try_parse_expr_sub_query()? {
+                expr
+            } else if let Ok(expr) = self.parse_expr() {
+                expr
+            } else {
+                self.expected("variable value", self.peek_token())?
+            };
+
+            values.push(value);
+            if self.consume_token(&Token::Comma) {
+                continue;
+            }
+
+            if parenthesized_assignment {
+                self.expect_token(&Token::RParen)?;
+            }
+            return Ok(values);
         }
+    }
 
-        let variables = if self.parse_keywords(&[Keyword::TIME, Keyword::ZONE]) {
-            OneOrManyWithParens::One(ObjectName::from(vec!["TIMEZONE".into()]))
-        } else if self.dialect.supports_parenthesized_set_variables()
+    fn parse_set_assignment(
+        &mut self,
+    ) -> Result<(OneOrManyWithParens<ObjectName>, Expr), ParserError> {
+        let variables = if self.dialect.supports_parenthesized_set_variables()
             && self.consume_token(&Token::LParen)
         {
-            let variables = OneOrManyWithParens::Many(
+            let vars = OneOrManyWithParens::Many(
                 self.parse_comma_separated(|parser: &mut Parser<'a>| parser.parse_identifier())?
                     .into_iter()
                     .map(|ident| ObjectName::from(vec![ident]))
                     .collect(),
             );
             self.expect_token(&Token::RParen)?;
-            variables
+            vars
         } else {
             OneOrManyWithParens::One(self.parse_object_name(false)?)
         };
 
-        let names = matches!(&variables, OneOrManyWithParens::One(variable) if variable.to_string().eq_ignore_ascii_case("NAMES"));
+        if !(self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO)) {
+            return self.expected("assignment operator", self.peek_token());
+        }
+
+        let values = self.parse_expr()?;
+
+        Ok((variables, values))
+    }
+
+    fn parse_set(&mut self) -> Result<Statement, ParserError> {
+        let modifier =
+            self.parse_one_of_keywords(&[Keyword::SESSION, Keyword::LOCAL, Keyword::HIVEVAR]);
 
-        if names && self.dialect.supports_set_names() {
+        if let Some(Keyword::HIVEVAR) = modifier {
+            self.expect_token(&Token::Colon)?;
+        }
+
+        if let Some(set_role_stmt) = self.maybe_parse(|parser| parser.parse_set_role(modifier))? {
+            return Ok(set_role_stmt);
+        }
+
+        // Handle special cases first
+        if self.parse_keywords(&[Keyword::TIME, Keyword::ZONE])
+            || self.parse_keyword(Keyword::TIMEZONE)
+        {
+            if self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO) {
+                return Ok(Set::SingleAssignment {
+                    local: modifier == Some(Keyword::LOCAL),
+                    hivevar: modifier == Some(Keyword::HIVEVAR),
+                    variable: ObjectName::from(vec!["TIMEZONE".into()]),
+                    values: self.parse_set_values(false)?,
+                }
+                .into());
+            } else {
+                // A shorthand alias for SET TIME ZONE that doesn't require
+                // the assignment operator. It's originally PostgreSQL specific,
+                // but we allow it for all the dialects
+                return Ok(Set::SetTimeZone {
+                    local: modifier == Some(Keyword::LOCAL),
+                    value: self.parse_expr()?,
+                }
+                .into());
+            }
+        } else if self.dialect.supports_set_names() && self.parse_keyword(Keyword::NAMES) {
             if self.parse_keyword(Keyword::DEFAULT) {
-                return Ok(Statement::SetNamesDefault {});
+                return Ok(Set::SetNamesDefault {}.into());
             }
-
             let charset_name = self.parse_identifier()?;
             let collation_name = if self.parse_one_of_keywords(&[Keyword::COLLATE]).is_some() {
                 Some(self.parse_literal_string()?)
@@ -11003,86 +11065,117 @@ impl<'a> Parser<'a> {
                 None
             };
 
-            return Ok(Statement::SetNames {
+            return Ok(Set::SetNames {
                 charset_name,
                 collation_name,
-            });
-        }
-
-        let parenthesized_assignment = matches!(&variables, OneOrManyWithParens::Many(_));
-
-        if self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO) {
-            if parenthesized_assignment {
-                self.expect_token(&Token::LParen)?;
-            }
-
-            let mut values = vec![];
-            loop {
-                let value = if let Some(expr) = self.try_parse_expr_sub_query()? {
-                    expr
-                } else if let Ok(expr) = self.parse_expr() {
-                    expr
-                } else {
-                    self.expected("variable value", self.peek_token())?
-                };
-
-                values.push(value);
-                if self.consume_token(&Token::Comma) {
-                    continue;
-                }
-
-                if parenthesized_assignment {
-                    self.expect_token(&Token::RParen)?;
-                }
-                return Ok(Statement::SetVariable {
-                    local: modifier == Some(Keyword::LOCAL),
-                    hivevar: Some(Keyword::HIVEVAR) == modifier,
-                    variables,
-                    value: values,
-                });
             }
-        }
-
-        let OneOrManyWithParens::One(variable) = variables else {
-            return self.expected("set variable", self.peek_token());
-        };
-
-        if variable.to_string().eq_ignore_ascii_case("TIMEZONE") {
-            // for some db (e.g. postgresql), SET TIME ZONE <value> is an alias for SET TIMEZONE [TO|=] <value>
-            match self.parse_expr() {
-                Ok(expr) => Ok(Statement::SetTimeZone {
-                    local: modifier == Some(Keyword::LOCAL),
-                    value: expr,
-                }),
-                _ => self.expected("timezone value", self.peek_token())?,
-            }
-        } else if variable.to_string() == "CHARACTERISTICS" {
+            .into());
+        } else if self.parse_keyword(Keyword::CHARACTERISTICS) {
             self.expect_keywords(&[Keyword::AS, Keyword::TRANSACTION])?;
-            Ok(Statement::SetTransaction {
+            return Ok(Set::SetTransaction {
                 modes: self.parse_transaction_modes()?,
                 snapshot: None,
                 session: true,
-            })
-        } else if variable.to_string() == "TRANSACTION" && modifier.is_none() {
+            }
+            .into());
+        } else if self.parse_keyword(Keyword::TRANSACTION) {
             if self.parse_keyword(Keyword::SNAPSHOT) {
                 let snapshot_id = self.parse_value()?.value;
-                return Ok(Statement::SetTransaction {
+                return Ok(Set::SetTransaction {
                     modes: vec![],
                     snapshot: Some(snapshot_id),
                     session: false,
-                });
+                }
+                .into());
             }
-            Ok(Statement::SetTransaction {
+            return Ok(Set::SetTransaction {
                 modes: self.parse_transaction_modes()?,
                 snapshot: None,
                 session: false,
-            })
-        } else if self.dialect.supports_set_stmt_without_operator() {
-            self.prev_token();
-            self.parse_set_session_params()
+            }
+            .into());
+        }
+
+        if self.dialect.supports_comma_separated_set_assignments() {
+            if let Some(assignments) = self
+                .maybe_parse(|parser| parser.parse_comma_separated(Parser::parse_set_assignment))?
+            {
+                return if assignments.len() > 1 {
+                    let assignments = assignments
+                        .into_iter()
+                        .map(|(var, val)| match var {
+                            OneOrManyWithParens::One(v) => Ok(SetAssignment {
+                                name: v,
+                                value: val,
+                            }),
+                            OneOrManyWithParens::Many(_) => {
+                                self.expected("List of single identifiers", self.peek_token())
+                            }
+                        })
+                        .collect::<Result<_, _>>()?;
+
+                    Ok(Set::MultipleAssignments { assignments }.into())
+                } else {
+                    let (vars, values): (Vec<_>, Vec<_>) = assignments.into_iter().unzip();
+
+                    let variable = match vars.into_iter().next() {
+                        Some(OneOrManyWithParens::One(v)) => Ok(v),
+                        Some(OneOrManyWithParens::Many(_)) => self.expected(
+                            "Single assignment or list of assignments",
+                            self.peek_token(),
+                        ),
+                        None => self.expected("At least one identifier", self.peek_token()),
+                    }?;
+
+                    Ok(Set::SingleAssignment {
+                        local: modifier == Some(Keyword::LOCAL),
+                        hivevar: modifier == Some(Keyword::HIVEVAR),
+                        variable,
+                        values,
+                    }
+                    .into())
+                };
+            }
+        }
+
+        let variables = if self.dialect.supports_parenthesized_set_variables()
+            && self.consume_token(&Token::LParen)
+        {
+            let vars = OneOrManyWithParens::Many(
+                self.parse_comma_separated(|parser: &mut Parser<'a>| parser.parse_identifier())?
+                    .into_iter()
+                    .map(|ident| ObjectName::from(vec![ident]))
+                    .collect(),
+            );
+            self.expect_token(&Token::RParen)?;
+            vars
         } else {
-            self.expected("equals sign or TO", self.peek_token())
+            OneOrManyWithParens::One(self.parse_object_name(false)?)
+        };
+
+        if self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO) {
+            let stmt = match variables {
+                OneOrManyWithParens::One(var) => Set::SingleAssignment {
+                    local: modifier == Some(Keyword::LOCAL),
+                    hivevar: modifier == Some(Keyword::HIVEVAR),
+                    variable: var,
+                    values: self.parse_set_values(false)?,
+                },
+                OneOrManyWithParens::Many(vars) => Set::ParenthesizedAssignments {
+                    variables: vars,
+                    values: self.parse_set_values(true)?,
+                },
+            };
+
+            return Ok(stmt.into());
         }
+
+        if self.dialect.supports_set_stmt_without_operator() {
+            self.prev_token();
+            return self.parse_set_session_params();
+        };
+
+        self.expected("equals sign or TO", self.peek_token())
     }
 
     pub fn parse_set_session_params(&mut self) -> Result<Statement, ParserError> {
@@ -11100,15 +11193,20 @@ impl<'a> Parser<'a> {
                 _ => return self.expected("IO, PROFILE, TIME or XML", self.peek_token()),
             };
             let value = self.parse_session_param_value()?;
-            Ok(Statement::SetSessionParam(SetSessionParamKind::Statistics(
-                SetSessionParamStatistics { topic, value },
-            )))
+            Ok(
+                Set::SetSessionParam(SetSessionParamKind::Statistics(SetSessionParamStatistics {
+                    topic,
+                    value,
+                }))
+                .into(),
+            )
         } else if self.parse_keyword(Keyword::IDENTITY_INSERT) {
             let obj = self.parse_object_name(false)?;
             let value = self.parse_session_param_value()?;
-            Ok(Statement::SetSessionParam(
-                SetSessionParamKind::IdentityInsert(SetSessionParamIdentityInsert { obj, value }),
+            Ok(Set::SetSessionParam(SetSessionParamKind::IdentityInsert(
+                SetSessionParamIdentityInsert { obj, value },
             ))
+            .into())
         } else if self.parse_keyword(Keyword::OFFSETS) {
             let keywords = self.parse_comma_separated(|parser| {
                 let next_token = parser.next_token();
@@ -11118,9 +11216,13 @@ impl<'a> Parser<'a> {
                 }
             })?;
             let value = self.parse_session_param_value()?;
-            Ok(Statement::SetSessionParam(SetSessionParamKind::Offsets(
-                SetSessionParamOffsets { keywords, value },
-            )))
+            Ok(
+                Set::SetSessionParam(SetSessionParamKind::Offsets(SetSessionParamOffsets {
+                    keywords,
+                    value,
+                }))
+                .into(),
+            )
         } else {
             let names = self.parse_comma_separated(|parser| {
                 let next_token = parser.next_token();
@@ -11130,9 +11232,13 @@ impl<'a> Parser<'a> {
                 }
             })?;
             let value = self.parse_expr()?.to_string();
-            Ok(Statement::SetSessionParam(SetSessionParamKind::Generic(
-                SetSessionParamGeneric { names, value },
-            )))
+            Ok(
+                Set::SetSessionParam(SetSessionParamKind::Generic(SetSessionParamGeneric {
+                    names,
+                    value,
+                }))
+                .into(),
+            )
         }
     }