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

iffyio 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 2ed2cbe2 Snowflake: Add support for `CREATE USER` (#1950)
2ed2cbe2 is described below

commit 2ed2cbe291150d4a28f2a4f20f9c95ceead57b60
Author: Yoav Cohen <[email protected]>
AuthorDate: Wed Jul 23 18:52:08 2025 +0300

    Snowflake: Add support for `CREATE USER` (#1950)
---
 src/ast/helpers/key_value_options.rs |  28 +++++----
 src/ast/mod.rs                       |  50 ++++++++++++++++
 src/ast/spans.rs                     |   1 +
 src/dialect/snowflake.rs             | 109 ++++++++++++-----------------------
 src/parser/mod.rs                    | 107 +++++++++++++++++++++++++++++++++-
 tests/sqlparser_common.rs            |  71 +++++++++++++++++++++++
 6 files changed, 281 insertions(+), 85 deletions(-)

diff --git a/src/ast/helpers/key_value_options.rs 
b/src/ast/helpers/key_value_options.rs
index 796bfd5e..7f1bb0fd 100644
--- a/src/ast/helpers/key_value_options.rs
+++ b/src/ast/helpers/key_value_options.rs
@@ -31,11 +31,22 @@ use serde::{Deserialize, Serialize};
 #[cfg(feature = "visitor")]
 use sqlparser_derive::{Visit, VisitMut};
 
+use crate::ast::display_separated;
+
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
 pub struct KeyValueOptions {
     pub options: Vec<KeyValueOption>,
+    pub delimiter: 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 KeyValueOptionsDelimiter {
+    Space,
+    Comma,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -59,18 +70,11 @@ pub struct KeyValueOption {
 
 impl fmt::Display for KeyValueOptions {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
-        if !self.options.is_empty() {
-            let mut first = false;
-            for option in &self.options {
-                if !first {
-                    first = true;
-                } else {
-                    f.write_str(" ")?;
-                }
-                write!(f, "{option}")?;
-            }
-        }
-        Ok(())
+        let sep = match self.delimiter {
+            KeyValueOptionsDelimiter::Space => " ",
+            KeyValueOptionsDelimiter::Comma => ", ",
+        };
+        write!(f, "{}", display_separated(&self.options, sep))
     }
 }
 
diff --git a/src/ast/mod.rs b/src/ast/mod.rs
index 1798223f..e1646349 100644
--- a/src/ast/mod.rs
+++ b/src/ast/mod.rs
@@ -4355,6 +4355,11 @@ pub enum Statement {
     ///
     /// See [ReturnStatement]
     Return(ReturnStatement),
+    /// ```sql
+    /// CREATE [OR REPLACE] USER <user> [IF NOT EXISTS]
+    /// ```
+    /// 
[Snowflake](https://docs.snowflake.com/en/sql-reference/sql/create-user)
+    CreateUser(CreateUser),
 }
 
 /// ```sql
@@ -6193,6 +6198,7 @@ impl fmt::Display for Statement {
             Statement::Return(r) => write!(f, "{r}"),
             Statement::List(command) => write!(f, "LIST {command}"),
             Statement::Remove(command) => write!(f, "REMOVE {command}"),
+            Statement::CreateUser(s) => write!(f, "{s}"),
         }
     }
 }
@@ -10125,6 +10131,50 @@ impl fmt::Display for MemberOf {
     }
 }
 
+/// Creates a user
+///
+/// Syntax:
+/// ```sql
+/// CREATE [OR REPLACE] USER [IF NOT EXISTS] <name> [OPTIONS]
+/// ```
+///
+/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/create-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 CreateUser {
+    pub or_replace: bool,
+    pub if_not_exists: bool,
+    pub name: Ident,
+    pub options: KeyValueOptions,
+    pub with_tags: bool,
+    pub tags: KeyValueOptions,
+}
+
+impl fmt::Display for CreateUser {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "CREATE")?;
+        if self.or_replace {
+            write!(f, " OR REPLACE")?;
+        }
+        write!(f, " USER")?;
+        if self.if_not_exists {
+            write!(f, " IF NOT EXISTS")?;
+        }
+        write!(f, " {}", self.name)?;
+        if !self.options.options.is_empty() {
+            write!(f, " {}", self.options)?;
+        }
+        if !self.tags.options.is_empty() {
+            if self.with_tags {
+                write!(f, " WITH")?;
+            }
+            write!(f, " TAG ({})", self.tags)?;
+        }
+        Ok(())
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use crate::tokenizer::Location;
diff --git a/src/ast/spans.rs b/src/ast/spans.rs
index 3e82905e..4deedca0 100644
--- a/src/ast/spans.rs
+++ b/src/ast/spans.rs
@@ -531,6 +531,7 @@ impl Spanned for Statement {
             Statement::Print { .. } => Span::empty(),
             Statement::Return { .. } => Span::empty(),
             Statement::List(..) | Statement::Remove(..) => Span::empty(),
+            Statement::CreateUser(..) => Span::empty(),
         }
     }
 }
diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs
index baf99b84..9786aba0 100644
--- a/src/dialect/snowflake.rs
+++ b/src/dialect/snowflake.rs
@@ -17,7 +17,9 @@
 
 #[cfg(not(feature = "std"))]
 use crate::alloc::string::ToString;
-use crate::ast::helpers::key_value_options::{KeyValueOption, 
KeyValueOptionType, KeyValueOptions};
+use crate::ast::helpers::key_value_options::{
+    KeyValueOption, KeyValueOptionType, KeyValueOptions, 
KeyValueOptionsDelimiter,
+};
 use crate::ast::helpers::stmt_create_table::CreateTableBuilder;
 use crate::ast::helpers::stmt_data_loading::{
     FileStagingCommand, StageLoadSelectItem, StageLoadSelectItemKind, 
StageParamsObject,
@@ -31,7 +33,7 @@ use crate::ast::{
 use crate::dialect::{Dialect, Precedence};
 use crate::keywords::Keyword;
 use crate::parser::{IsOptional, Parser, ParserError};
-use crate::tokenizer::{Token, Word};
+use crate::tokenizer::Token;
 #[cfg(not(feature = "std"))]
 use alloc::boxed::Box;
 #[cfg(not(feature = "std"))]
@@ -516,6 +518,7 @@ fn parse_alter_session(parser: &mut Parser, set: bool) -> 
Result<Statement, Pars
         set,
         session_params: KeyValueOptions {
             options: session_options,
+            delimiter: KeyValueOptionsDelimiter::Space,
         },
     })
 }
@@ -777,19 +780,19 @@ pub fn parse_create_stage(
     // [ directoryTableParams ]
     if parser.parse_keyword(Keyword::DIRECTORY) {
         parser.expect_token(&Token::Eq)?;
-        directory_table_params = parse_parentheses_options(parser)?;
+        directory_table_params = parser.parse_key_value_options(true, &[])?;
     }
 
     // [ file_format]
     if parser.parse_keyword(Keyword::FILE_FORMAT) {
         parser.expect_token(&Token::Eq)?;
-        file_format = parse_parentheses_options(parser)?;
+        file_format = parser.parse_key_value_options(true, &[])?;
     }
 
     // [ copy_options ]
     if parser.parse_keyword(Keyword::COPY_OPTIONS) {
         parser.expect_token(&Token::Eq)?;
-        copy_options = parse_parentheses_options(parser)?;
+        copy_options = parser.parse_key_value_options(true, &[])?;
     }
 
     // [ comment ]
@@ -806,12 +809,15 @@ pub fn parse_create_stage(
         stage_params,
         directory_table_params: KeyValueOptions {
             options: directory_table_params,
+            delimiter: KeyValueOptionsDelimiter::Space,
         },
         file_format: KeyValueOptions {
             options: file_format,
+            delimiter: KeyValueOptionsDelimiter::Space,
         },
         copy_options: KeyValueOptions {
             options: copy_options,
+            delimiter: KeyValueOptionsDelimiter::Space,
         },
         comment,
     })
@@ -879,10 +885,16 @@ pub fn parse_copy_into(parser: &mut Parser) -> 
Result<Statement, ParserError> {
     let mut from_stage = None;
     let mut stage_params = StageParamsObject {
         url: None,
-        encryption: KeyValueOptions { options: vec![] },
+        encryption: KeyValueOptions {
+            options: vec![],
+            delimiter: KeyValueOptionsDelimiter::Space,
+        },
         endpoint: None,
         storage_integration: None,
-        credentials: KeyValueOptions { options: vec![] },
+        credentials: KeyValueOptions {
+            options: vec![],
+            delimiter: KeyValueOptionsDelimiter::Space,
+        },
     };
     let mut from_query = None;
     let mut partition = None;
@@ -944,7 +956,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 = parse_parentheses_options(parser)?;
+            file_format = parser.parse_key_value_options(true, &[])?;
         // PARTITION BY
         } else if parser.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) {
             partition = Some(Box::new(parser.parse_expr()?))
@@ -982,14 +994,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 = parse_parentheses_options(parser)?;
+            copy_options = parser.parse_key_value_options(true, &[])?;
         } 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(parse_option(parser, 
key)?),
+                Token::Word(key) => 
copy_options.push(parser.parse_key_value_option(key)?),
                 _ => return parser.expected("another copy option, ; or EOF'", 
parser.peek_token()),
             }
         }
@@ -1008,9 +1020,11 @@ pub fn parse_copy_into(parser: &mut Parser) -> 
Result<Statement, ParserError> {
         pattern,
         file_format: KeyValueOptions {
             options: file_format,
+            delimiter: KeyValueOptionsDelimiter::Space,
         },
         copy_options: KeyValueOptions {
             options: copy_options,
+            delimiter: KeyValueOptionsDelimiter::Space,
         },
         validation_mode,
         partition,
@@ -1110,8 +1124,14 @@ fn parse_select_item_for_data_load(
 
 fn parse_stage_params(parser: &mut Parser) -> Result<StageParamsObject, 
ParserError> {
     let (mut url, mut storage_integration, mut endpoint) = (None, None, None);
-    let mut encryption: KeyValueOptions = KeyValueOptions { options: vec![] };
-    let mut credentials: KeyValueOptions = KeyValueOptions { options: vec![] };
+    let mut encryption: KeyValueOptions = KeyValueOptions {
+        options: vec![],
+        delimiter: KeyValueOptionsDelimiter::Space,
+    };
+    let mut credentials: KeyValueOptions = KeyValueOptions {
+        options: vec![],
+        delimiter: KeyValueOptionsDelimiter::Space,
+    };
 
     // URL
     if parser.parse_keyword(Keyword::URL) {
@@ -1141,7 +1161,8 @@ fn parse_stage_params(parser: &mut Parser) -> 
Result<StageParamsObject, ParserEr
     if parser.parse_keyword(Keyword::CREDENTIALS) {
         parser.expect_token(&Token::Eq)?;
         credentials = KeyValueOptions {
-            options: parse_parentheses_options(parser)?,
+            options: parser.parse_key_value_options(true, &[])?,
+            delimiter: KeyValueOptionsDelimiter::Space,
         };
     }
 
@@ -1149,7 +1170,8 @@ fn parse_stage_params(parser: &mut Parser) -> 
Result<StageParamsObject, ParserEr
     if parser.parse_keyword(Keyword::ENCRYPTION) {
         parser.expect_token(&Token::Eq)?;
         encryption = KeyValueOptions {
-            options: parse_parentheses_options(parser)?,
+            options: parser.parse_key_value_options(true, &[])?,
+            delimiter: KeyValueOptionsDelimiter::Space,
         };
     }
 
@@ -1183,7 +1205,7 @@ fn parse_session_options(
             Token::Word(key) => {
                 parser.advance_token();
                 if set {
-                    let option = parse_option(parser, key)?;
+                    let option = parser.parse_key_value_option(key)?;
                     options.push(option);
                 } else {
                     options.push(KeyValueOption {
@@ -1207,63 +1229,6 @@ fn parse_session_options(
     }
 }
 
-/// Parses options provided within parentheses like:
-/// ( ENABLE = { TRUE | FALSE }
-///      [ AUTO_REFRESH = { TRUE | FALSE } ]
-///      [ REFRESH_ON_CREATE =  { TRUE | FALSE } ]
-///      [ NOTIFICATION_INTEGRATION = '<notification_integration_name>' ] )
-///
-fn parse_parentheses_options(parser: &mut Parser) -> 
Result<Vec<KeyValueOption>, ParserError> {
-    let mut options: Vec<KeyValueOption> = Vec::new();
-    parser.expect_token(&Token::LParen)?;
-    loop {
-        match parser.next_token().token {
-            Token::RParen => break,
-            Token::Comma => continue,
-            Token::Word(key) => options.push(parse_option(parser, key)?),
-            _ => return parser.expected("another option or ')'", 
parser.peek_token()),
-        };
-    }
-    Ok(options)
-}
-
-/// Parses a `KEY = VALUE` construct based on the specified key
-fn parse_option(parser: &mut Parser, key: Word) -> Result<KeyValueOption, 
ParserError> {
-    parser.expect_token(&Token::Eq)?;
-    if parser.parse_keyword(Keyword::TRUE) {
-        Ok(KeyValueOption {
-            option_name: key.value,
-            option_type: KeyValueOptionType::BOOLEAN,
-            value: "TRUE".to_string(),
-        })
-    } else if parser.parse_keyword(Keyword::FALSE) {
-        Ok(KeyValueOption {
-            option_name: key.value,
-            option_type: KeyValueOptionType::BOOLEAN,
-            value: "FALSE".to_string(),
-        })
-    } else {
-        match parser.next_token().token {
-            Token::SingleQuotedString(value) => Ok(KeyValueOption {
-                option_name: key.value,
-                option_type: KeyValueOptionType::STRING,
-                value,
-            }),
-            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,
-            }),
-            _ => parser.expected("expected option value", parser.peek_token()),
-        }
-    }
-}
-
 /// Parsing a property of identity or autoincrement column option
 /// Syntax:
 /// ```sql
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 8d5a55da..5ac19d13 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -32,7 +32,12 @@ use recursion::RecursionCounter;
 use IsLateral::*;
 use IsOptional::*;
 
-use crate::ast::helpers::stmt_create_table::{CreateTableBuilder, 
CreateTableConfiguration};
+use crate::ast::helpers::{
+    key_value_options::{
+        KeyValueOption, KeyValueOptionType, KeyValueOptions, 
KeyValueOptionsDelimiter,
+    },
+    stmt_create_table::{CreateTableBuilder, CreateTableConfiguration},
+};
 use crate::ast::Statement::CreatePolicy;
 use crate::ast::*;
 use crate::dialect::*;
@@ -4680,6 +4685,8 @@ impl<'a> Parser<'a> {
             self.parse_create_macro(or_replace, temporary)
         } else if self.parse_keyword(Keyword::SECRET) {
             self.parse_create_secret(or_replace, temporary, persistent)
+        } else if self.parse_keyword(Keyword::USER) {
+            self.parse_create_user(or_replace)
         } else if or_replace {
             self.expected(
                 "[EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION after 
CREATE OR REPLACE",
@@ -4714,6 +4721,32 @@ 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 with_tags = self.parse_keyword(Keyword::WITH);
+        let tags = if self.parse_keyword(Keyword::TAG) {
+            self.parse_key_value_options(true, &[])?
+        } else {
+            vec![]
+        };
+        Ok(Statement::CreateUser(CreateUser {
+            or_replace,
+            if_not_exists,
+            name,
+            options: KeyValueOptions {
+                options,
+                delimiter: KeyValueOptionsDelimiter::Space,
+            },
+            with_tags,
+            tags: KeyValueOptions {
+                options: tags,
+                delimiter: KeyValueOptionsDelimiter::Comma,
+            },
+        }))
+    }
+
     /// See [DuckDB 
Docs](https://duckdb.org/docs/sql/statements/create_secret.html) for more 
details.
     pub fn parse_create_secret(
         &mut self,
@@ -16612,6 +16645,78 @@ impl<'a> Parser<'a> {
     pub(crate) fn in_column_definition_state(&self) -> bool {
         matches!(self.state, ColumnDefinition)
     }
+
+    /// Parses options provided in key-value format.
+    ///
+    /// * `parenthesized` - true if the options are enclosed in parenthesis
+    /// * `end_words` - a list of keywords that any of them indicates the end 
of the options section
+    pub(crate) fn parse_key_value_options(
+        &mut self,
+        parenthesized: bool,
+        end_words: &[Keyword],
+    ) -> Result<Vec<KeyValueOption>, ParserError> {
+        let mut options: Vec<KeyValueOption> = Vec::new();
+        if parenthesized {
+            self.expect_token(&Token::LParen)?;
+        }
+        loop {
+            match self.next_token().token {
+                Token::RParen => {
+                    if parenthesized {
+                        break;
+                    } else {
+                        return self.expected(" another option or EOF", 
self.peek_token());
+                    }
+                }
+                Token::EOF => break,
+                Token::Comma => continue,
+                Token::Word(w) if !end_words.contains(&w.keyword) => {
+                    options.push(self.parse_key_value_option(w)?)
+                }
+                Token::Word(w) if end_words.contains(&w.keyword) => {
+                    self.prev_token();
+                    break;
+                }
+                _ => return self.expected("another option, EOF, Comma or ')'", 
self.peek_token()),
+            };
+        }
+        Ok(options)
+    }
+
+    /// Parses a `KEY = VALUE` construct based on the specified key
+    pub(crate) fn parse_key_value_option(
+        &mut self,
+        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,
+            }),
+            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(),
+                })
+            }
+            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,
+            }),
+            _ => self.expected("expected option value", self.peek_token()),
+        }
+    }
 }
 
 fn maybe_prefixed_expr(expr: Expr, prefix: Option<Ident>) -> Expr {
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index 5d8284a4..5ae12ff4 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -27,6 +27,8 @@ extern crate core;
 
 use helpers::attached_token::AttachedToken;
 use matches::assert_matches;
+use sqlparser::ast::helpers::key_value_options::*;
+use sqlparser::ast::helpers::key_value_options::{KeyValueOptions, 
KeyValueOptionsDelimiter};
 use sqlparser::ast::SelectItem::UnnamedExpr;
 use sqlparser::ast::TableFactor::{Pivot, Unpivot};
 use sqlparser::ast::*;
@@ -16256,3 +16258,72 @@ fn parse_notnull() {
     // for unsupported dialects, parsing should stop at `NOT NULL`
     notnull_unsupported_dialects.expr_parses_to("NOT NULL NOTNULL", "NOT 
NULL");
 }
+
+#[test]
+fn parse_create_user() {
+    let create = verified_stmt("CREATE USER u1");
+    match create {
+        Statement::CreateUser(stmt) => {
+            assert_eq!(stmt.name, Ident::new("u1"));
+        }
+        _ => unreachable!(),
+    }
+    verified_stmt("CREATE OR REPLACE USER u1");
+    verified_stmt("CREATE OR REPLACE USER IF NOT EXISTS u1");
+    verified_stmt("CREATE OR REPLACE USER IF NOT EXISTS u1 PASSWORD='secret'");
+    verified_stmt(
+        "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')");
+    match create {
+        Statement::CreateUser(stmt) => {
+            assert_eq!(stmt.name, Ident::new("u1"));
+            assert_eq!(stmt.or_replace, true);
+            assert_eq!(stmt.if_not_exists, true);
+            assert_eq!(
+                stmt.options,
+                KeyValueOptions {
+                    delimiter: KeyValueOptionsDelimiter::Space,
+                    options: vec![
+                        KeyValueOption {
+                            option_name: "PASSWORD".to_string(),
+                            value: "secret".to_string(),
+                            option_type: KeyValueOptionType::STRING
+                        },
+                        KeyValueOption {
+                            option_name: "MUST_CHANGE_PASSWORD".to_string(),
+                            value: "TRUE".to_string(),
+                            option_type: KeyValueOptionType::BOOLEAN
+                        },
+                        KeyValueOption {
+                            option_name: "TYPE".to_string(),
+                            value: "SERVICE".to_string(),
+                            option_type: KeyValueOptionType::ENUM
+                        },
+                    ],
+                },
+            );
+            assert_eq!(stmt.with_tags, true);
+            assert_eq!(
+                stmt.tags,
+                KeyValueOptions {
+                    delimiter: KeyValueOptionsDelimiter::Comma,
+                    options: vec![
+                        KeyValueOption {
+                            option_name: "t1".to_string(),
+                            value: "v1".to_string(),
+                            option_type: KeyValueOptionType::STRING
+                        },
+                        KeyValueOption {
+                            option_name: "t2".to_string(),
+                            value: "v2".to_string(),
+                            option_type: KeyValueOptionType::STRING
+                        },
+                    ]
+                }
+            );
+        }
+        _ => unreachable!(),
+    }
+}


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

Reply via email to