Snowflake: Add support for CREATE USER (#1950)

This commit is contained in:
Yoav Cohen 2025-07-23 18:52:08 +03:00 committed by GitHub
parent 492184643a
commit 2ed2cbe291
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 281 additions and 85 deletions

View file

@ -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))
}
}

View file

@ -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;

View file

@ -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(),
}
}
}

View file

@ -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

View file

@ -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 {

View file

@ -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!(),
}
}