From d7ad2c12b044e5a2cab4d359c338d1487f92b1ba Mon Sep 17 00:00:00 2001 From: osipovartem Date: Thu, 6 Feb 2025 13:24:05 +0300 Subject: [PATCH 1/7] Implement SnowFlake ALTER SESSION --- src/ast/mod.rs | 30 +++++++++++++++++++++ src/ast/spans.rs | 1 + src/dialect/snowflake.rs | 52 ++++++++++++++++++++++++++++++++++++ src/keywords.rs | 1 + tests/sqlparser_snowflake.rs | 26 ++++++++++++++++++ 5 files changed, 110 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index dc944c9e7..2b8bbb62a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2722,6 +2722,17 @@ pub enum Statement { owner: Option, }, /// ```sql + /// ALTER SESSION SET sessionParam + /// ALTER SESSION UNSET [ , , ... ] + /// ``` + /// See + AlterSession { + /// true is to set for the session parameters, false is to unset + set: bool, + /// The session parameters to set or unset + session_params: DataLoadingOptions, + }, + /// ```sql /// ATTACH DATABASE 'path/to/file' AS alias /// ``` /// (SQLite-specific) @@ -4464,6 +4475,25 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::AlterSession { + set, + session_params, + } => { + write!( f, "ALTER SESSION {set}", set = if *set { "SET"} else { "UNSET" })?; + if !session_params.options.is_empty() { + if *set { + write!(f, " {}", session_params)?; + } else { + let options = session_params + .options + .iter() + .map(|p| p.option_name.clone()) + .collect::>() ; + write!(f, " {}", display_separated(&options, " "))?; + } + } + Ok(()) + } Statement::Drop { object_type, if_exists, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index f0c38942e..3e6ffaf06 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -428,6 +428,7 @@ impl Spanned for Statement { ), // These statements need to be implemented Statement::AlterRole { .. } => Span::empty(), + Statement::AlterSession { .. } => Span::empty(), Statement::AttachDatabase { .. } => Span::empty(), Statement::AttachDuckDBDatabase { .. } => Span::empty(), Statement::DetachDuckDBDatabase { .. } => Span::empty(), diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index fc1926719..4d0e130c8 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -120,6 +120,12 @@ impl Dialect for SnowflakeDialect { } fn parse_statement(&self, parser: &mut Parser) -> Option> { + if parser.parse_keywords(&[Keyword::ALTER, Keyword::SESSION]) { + // ALTER SESSION + let set = parser.parse_keyword(Keyword::SET) | !parser.parse_keyword(Keyword::UNSET); + return Some(parse_alter_session(parser, set)); + } + if parser.parse_keyword(Keyword::CREATE) { // possibly CREATE STAGE //[ OR REPLACE ] @@ -335,6 +341,20 @@ fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result +fn parse_alter_session(parser: &mut Parser, set: bool) -> Result { + let session_options = parse_session_options(parser, set)?; + Ok( + Statement::AlterSession { + set, + session_params: DataLoadingOptions { + options: session_options, + }, + } + ) +} + /// Parse snowflake create table statement. /// /// @@ -960,6 +980,38 @@ fn parse_stage_params(parser: &mut Parser) -> Result ] +fn parse_session_options(parser: &mut Parser, set: bool) -> Result, ParserError> { + let mut options: Vec = Vec::new(); + let empty = String::new; + loop { + match parser.next_token().token { + Token::EOF => break, + Token::Comma => continue, + Token::Word(key) => { + if set { + let option = parse_copy_option(parser, key)?; + options.push(option); + } else { + options.push(DataLoadingOption { + option_name: key.value, + option_type: DataLoadingOptionType::STRING, + value: empty(), + }); + } + + }, + _ => return parser.expected("another option", parser.peek_token()), + } + } + Ok(options) +} + + /// Parses options provided within parentheses like: /// ( ENABLE = { TRUE | FALSE } /// [ AUTO_REFRESH = { TRUE | FALSE } ] diff --git a/src/keywords.rs b/src/keywords.rs index 5f36fa737..3dddd6295 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -906,6 +906,7 @@ define_keywords!( UNNEST, UNPIVOT, UNSAFE, + UNSET, UNSIGNED, UNTIL, UPDATE, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index a18f1a4d8..f43c7ca29 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3404,3 +3404,29 @@ fn test_grant_database_role_to() { snowflake_and_generic().verified_stmt("GRANT DATABASE ROLE r1 TO ROLE r2"); snowflake_and_generic().verified_stmt("GRANT DATABASE ROLE db1.sc1.r1 TO ROLE db1.sc1.r2"); } + +#[test] +fn test_alter_session() { + snowflake().verified_stmt("ALTER SESSION SET"); + snowflake().verified_stmt("ALTER SESSION UNSET"); + snowflake().verified_stmt("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'", + ); + snowflake().one_statement_parses_to( + "ALTER SESSION SET A=true \nB='tag'", + "ALTER SESSION SET A=TRUE B='tag'", + ); + snowflake().one_statement_parses_to( + "ALTER SESSION UNSET a, b", + "ALTER SESSION UNSET a b", + ); + snowflake().one_statement_parses_to( + "ALTER SESSION UNSET a\nB", + "ALTER SESSION UNSET a B", + ); +} \ No newline at end of file From a15056d40c08a9b08db92aed8bbe367e16bdf50e Mon Sep 17 00:00:00 2001 From: osipovartem Date: Thu, 6 Feb 2025 13:24:45 +0300 Subject: [PATCH 2/7] Implement SnowFlake ALTER SESSION --- tests/sqlparser_snowflake.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index f43c7ca29..759d2d1e3 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3429,4 +3429,4 @@ fn test_alter_session() { "ALTER SESSION UNSET a\nB", "ALTER SESSION UNSET a B", ); -} \ No newline at end of file +} From 09fe160a3ab46d19b968602e371dc53997fe6049 Mon Sep 17 00:00:00 2001 From: osipovartem Date: Fri, 7 Feb 2025 14:39:40 +0300 Subject: [PATCH 3/7] Fix EOF parsing as next_token skips it --- src/dialect/snowflake.rs | 7 ++++++- tests/sqlparser_snowflake.rs | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 4d0e130c8..ce3c2faa5 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -1005,7 +1005,12 @@ fn parse_session_options(parser: &mut Parser, set: bool) -> Result return parser.expected("another option", parser.peek_token()), + _ => { + if parser.peek_token().token == Token::EOF { + break + } + return parser.expected("another option", parser.peek_token()) + }, } } Ok(options) diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 759d2d1e3..c8516a289 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3414,7 +3414,7 @@ fn test_alter_session() { 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( From bd17ced7ab703ecaa5df3c835fe010dd08f0e022 Mon Sep 17 00:00:00 2001 From: osipovartem Date: Fri, 7 Feb 2025 14:56:07 +0300 Subject: [PATCH 4/7] Fix EOF parsing as next_token skips it --- src/dialect/snowflake.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index ce3c2faa5..446d21e24 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -990,7 +990,6 @@ fn parse_session_options(parser: &mut Parser, set: bool) -> Result break, Token::Comma => continue, Token::Word(key) => { if set { From ee4be6b7b99f4a679a258252af3854db30bc7beb Mon Sep 17 00:00:00 2001 From: osipovartem Date: Wed, 12 Feb 2025 12:07:26 +0300 Subject: [PATCH 5/7] Move key_value to a separate helper file --- src/ast/helpers/key_value_options.rs | 92 +++++++++++++++++++ src/ast/helpers/mod.rs | 1 + src/ast/helpers/stmt_data_loading.rs | 63 +------------- src/ast/mod.rs | 19 ++-- src/dialect/snowflake.rs | 80 +++++++++-------- tests/sqlparser_snowflake.rs | 126 ++++++++++++++------------- 6 files changed, 216 insertions(+), 165 deletions(-) create mode 100644 src/ast/helpers/key_value_options.rs diff --git a/src/ast/helpers/key_value_options.rs b/src/ast/helpers/key_value_options.rs new file mode 100644 index 000000000..e2ebdcd28 --- /dev/null +++ b/src/ast/helpers/key_value_options.rs @@ -0,0 +1,92 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Key-value options for SQL statements. +//! 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 core::fmt; +use core::fmt::Formatter; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "visitor")] +use sqlparser_derive::{Visit, VisitMut}; + +#[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, +} + +#[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, +} + +#[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, +} + + +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(()) + } +} + +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)?; + } + KeyValueOptionType::ENUM + | KeyValueOptionType::BOOLEAN + | KeyValueOptionType::NUMBER => { + write!(f, "{}={}", self.option_name, self.value)?; + } + } + Ok(()) + } +} diff --git a/src/ast/helpers/mod.rs b/src/ast/helpers/mod.rs index a96bffc51..b992b4682 100644 --- a/src/ast/helpers/mod.rs +++ b/src/ast/helpers/mod.rs @@ -17,3 +17,4 @@ pub mod attached_token; pub mod stmt_create_table; pub mod stmt_data_loading; +pub mod key_value_options; diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs index 77de5d9ec..138a98e7b 100644 --- a/src/ast/helpers/stmt_data_loading.rs +++ b/src/ast/helpers/stmt_data_loading.rs @@ -32,42 +32,17 @@ use serde::{Deserialize, Serialize}; use crate::ast::{Ident, ObjectName}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; +use crate::ast::helpers::key_value_options::KeyValueOptions; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct StageParamsObject { pub url: Option, - pub encryption: DataLoadingOptions, + pub encryption: KeyValueOptions, pub endpoint: Option, pub storage_integration: Option, - pub credentials: DataLoadingOptions, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct DataLoadingOptions { - pub options: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum DataLoadingOptionType { - STRING, - BOOLEAN, - ENUM, - NUMBER, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct DataLoadingOption { - pub option_name: String, - pub option_type: DataLoadingOptionType, - pub value: String, + pub credentials: KeyValueOptions, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -106,38 +81,6 @@ impl fmt::Display for StageParamsObject { } } -impl fmt::Display for DataLoadingOptions { - 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(()) - } -} - -impl fmt::Display for DataLoadingOption { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self.option_type { - DataLoadingOptionType::STRING => { - write!(f, "{}='{}'", self.option_name, self.value)?; - } - DataLoadingOptionType::ENUM - | DataLoadingOptionType::BOOLEAN - | DataLoadingOptionType::NUMBER => { - write!(f, "{}={}", self.option_name, self.value)?; - } - } - Ok(()) - } -} impl fmt::Display for StageLoadSelectItem { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2b8bbb62a..36d9ab148 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -87,9 +87,8 @@ pub use self::value::{ NormalizationForm, TrimWhereField, Value, }; -use crate::ast::helpers::stmt_data_loading::{ - DataLoadingOptions, StageLoadSelectItem, StageParamsObject, -}; +use crate::ast::helpers::key_value_options::KeyValueOptions; +use crate::ast::helpers::stmt_data_loading::{StageLoadSelectItem, StageParamsObject}; #[cfg(feature = "visitor")] pub use visitor::*; @@ -2518,8 +2517,8 @@ pub enum Statement { from_query: Option>, files: Option>, pattern: Option, - file_format: DataLoadingOptions, - copy_options: DataLoadingOptions, + file_format: KeyValueOptions, + copy_options: KeyValueOptions, validation_mode: Option, partition: Option>, }, @@ -2730,7 +2729,7 @@ pub enum Statement { /// true is to set for the session parameters, false is to unset set: bool, /// The session parameters to set or unset - session_params: DataLoadingOptions, + session_params: KeyValueOptions, }, /// ```sql /// ATTACH DATABASE 'path/to/file' AS alias @@ -3254,9 +3253,9 @@ pub enum Statement { if_not_exists: bool, name: ObjectName, stage_params: StageParamsObject, - directory_table_params: DataLoadingOptions, - file_format: DataLoadingOptions, - copy_options: DataLoadingOptions, + directory_table_params: KeyValueOptions, + file_format: KeyValueOptions, + copy_options: KeyValueOptions, comment: Option, }, /// ```sql @@ -4489,7 +4488,7 @@ impl fmt::Display for Statement { .iter() .map(|p| p.option_name.clone()) .collect::>() ; - write!(f, " {}", display_separated(&options, " "))?; + write!(f, " {}", display_separated(&options, ", "))?; } } Ok(()) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 446d21e24..ade66a871 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -19,8 +19,10 @@ use crate::alloc::string::ToString; use crate::ast::helpers::stmt_create_table::CreateTableBuilder; use crate::ast::helpers::stmt_data_loading::{ - DataLoadingOption, DataLoadingOptionType, DataLoadingOptions, FileStagingCommand, - StageLoadSelectItem, StageParamsObject, + FileStagingCommand, StageLoadSelectItem, StageParamsObject, +}; +use crate::ast::helpers::key_value_options::{ + KeyValueOption, KeyValueOptionType, KeyValueOptions }; use crate::ast::{ ColumnOption, ColumnPolicy, ColumnPolicyProperty, CopyIntoSnowflakeKind, Ident, @@ -122,7 +124,11 @@ impl Dialect for SnowflakeDialect { fn parse_statement(&self, parser: &mut Parser) -> Option> { if parser.parse_keywords(&[Keyword::ALTER, Keyword::SESSION]) { // ALTER SESSION - let set = parser.parse_keyword(Keyword::SET) | !parser.parse_keyword(Keyword::UNSET); + let set = match parser.parse_one_of_keywords(&[Keyword::SET, Keyword::UNSET]) { + Some(Keyword::SET) => true, + Some(Keyword::UNSET) => false, + _ => return Some(parser.expected("SET or UNSET", parser.peek_token())) + }; return Some(parse_alter_session(parser, set)); } @@ -348,7 +354,7 @@ fn parse_alter_session(parser: &mut Parser, set: bool) -> Result Result { let mut from_stage = None; let mut stage_params = StageParamsObject { url: None, - encryption: DataLoadingOptions { options: vec![] }, + encryption: KeyValueOptions { options: vec![] }, endpoint: None, storage_integration: None, - credentials: DataLoadingOptions { options: vec![] }, + credentials: KeyValueOptions { options: vec![] }, }; let mut from_query = None; let mut partition = None; @@ -815,7 +821,7 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { Token::Comma => continue, // In `COPY INTO ` the copy options do not have a shared key // like in `COPY INTO ` - Token::Word(key) => copy_options.push(parse_copy_option(parser, key)?), + Token::Word(key) => copy_options.push(parse_option(parser, key)?), _ => return parser.expected("another copy option, ; or EOF'", parser.peek_token()), } } @@ -831,10 +837,10 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { from_query, files: if files.is_empty() { None } else { Some(files) }, pattern, - file_format: DataLoadingOptions { + file_format: KeyValueOptions { options: file_format, }, - copy_options: DataLoadingOptions { + copy_options: KeyValueOptions { options: copy_options, }, validation_mode, @@ -928,8 +934,8 @@ fn parse_select_items_for_data_load( fn parse_stage_params(parser: &mut Parser) -> Result { let (mut url, mut storage_integration, mut endpoint) = (None, None, None); - let mut encryption: DataLoadingOptions = DataLoadingOptions { options: vec![] }; - let mut credentials: DataLoadingOptions = DataLoadingOptions { options: vec![] }; + let mut encryption: KeyValueOptions = KeyValueOptions { options: vec![] }; + let mut credentials: KeyValueOptions = KeyValueOptions { options: vec![] }; // URL if parser.parse_keyword(Keyword::URL) { @@ -958,7 +964,7 @@ fn parse_stage_params(parser: &mut Parser) -> Result Result Result ] -fn parse_session_options(parser: &mut Parser, set: bool) -> Result, ParserError> { - let mut options: Vec = Vec::new(); +fn parse_session_options(parser: &mut Parser, set: bool) -> Result, ParserError> { + let mut options: Vec = Vec::new(); let empty = String::new; loop { match parser.next_token().token { Token::Comma => continue, Token::Word(key) => { if set { - let option = parse_copy_option(parser, key)?; + let option = parse_option(parser, key)?; options.push(option); } else { - options.push(DataLoadingOption { + options.push(KeyValueOption { option_name: key.value, - option_type: DataLoadingOptionType::STRING, + option_type: KeyValueOptionType::STRING, value: empty(), }); } @@ -1012,7 +1018,9 @@ fn parse_session_options(parser: &mut Parser, set: bool) -> Result Result Result, ParserError> { - let mut options: Vec = Vec::new(); +fn parse_parentheses_options(parser: &mut Parser) -> Result, ParserError> { + let mut options: Vec = 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_copy_option(parser, key)?), + Token::Word(key) => options.push(parse_option(parser, key)?), _ => return parser.expected("another option or ')'", parser.peek_token()), }; } @@ -1037,35 +1045,35 @@ fn parse_parentheses_options(parser: &mut Parser) -> Result Result { +fn parse_option(parser: &mut Parser, key: Word) -> Result { parser.expect_token(&Token::Eq)?; if parser.parse_keyword(Keyword::TRUE) { - Ok(DataLoadingOption { + Ok(KeyValueOption { option_name: key.value, - option_type: DataLoadingOptionType::BOOLEAN, + option_type: KeyValueOptionType::BOOLEAN, value: "TRUE".to_string(), }) } else if parser.parse_keyword(Keyword::FALSE) { - Ok(DataLoadingOption { + Ok(KeyValueOption { option_name: key.value, - option_type: DataLoadingOptionType::BOOLEAN, + option_type: KeyValueOptionType::BOOLEAN, value: "FALSE".to_string(), }) } else { match parser.next_token().token { - Token::SingleQuotedString(value) => Ok(DataLoadingOption { + Token::SingleQuotedString(value) => Ok(KeyValueOption { option_name: key.value, - option_type: DataLoadingOptionType::STRING, + option_type: KeyValueOptionType::STRING, value, }), - Token::Word(word) => Ok(DataLoadingOption { + Token::Word(word) => Ok(KeyValueOption { option_name: key.value, - option_type: DataLoadingOptionType::ENUM, + option_type: KeyValueOptionType::ENUM, value: word.value, }), - Token::Number(n, _) => Ok(DataLoadingOption { + Token::Number(n, _) => Ok(KeyValueOption { option_name: key.value, - option_type: DataLoadingOptionType::NUMBER, + option_type: KeyValueOptionType::NUMBER, value: n, }), _ => parser.expected("expected option value", parser.peek_token()), diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index c8516a289..d1ca483d1 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -19,9 +19,8 @@ //! 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::stmt_data_loading::{ - DataLoadingOption, DataLoadingOptionType, StageLoadSelectItem, -}; +use sqlparser::ast::helpers::key_value_options::{KeyValueOption, KeyValueOptionType}; +use sqlparser::ast::helpers::stmt_data_loading::StageLoadSelectItem; use sqlparser::ast::*; use sqlparser::dialect::{Dialect, GenericDialect, SnowflakeDialect}; use sqlparser::parser::{ParserError, ParserOptions}; @@ -1891,33 +1890,33 @@ fn test_create_stage_with_stage_params() { assert!(stage_params .credentials .options - .contains(&DataLoadingOption { + .contains(&KeyValueOption { option_name: "AWS_KEY_ID".to_string(), - option_type: DataLoadingOptionType::STRING, + option_type: KeyValueOptionType::STRING, value: "1a2b3c".to_string() })); assert!(stage_params .credentials .options - .contains(&DataLoadingOption { + .contains(&KeyValueOption { option_name: "AWS_SECRET_KEY".to_string(), - option_type: DataLoadingOptionType::STRING, + option_type: KeyValueOptionType::STRING, value: "4x5y6z".to_string() })); assert!(stage_params .encryption .options - .contains(&DataLoadingOption { + .contains(&KeyValueOption { option_name: "MASTER_KEY".to_string(), - option_type: DataLoadingOptionType::STRING, + option_type: KeyValueOptionType::STRING, value: "key".to_string() })); assert!(stage_params .encryption .options - .contains(&DataLoadingOption { + .contains(&KeyValueOption { option_name: "TYPE".to_string(), - option_type: DataLoadingOptionType::STRING, + option_type: KeyValueOptionType::STRING, value: "AWS_SSE_KMS".to_string() })); } @@ -1940,19 +1939,19 @@ fn test_create_stage_with_directory_table_params() { directory_table_params, .. } => { - assert!(directory_table_params.options.contains(&DataLoadingOption { + assert!(directory_table_params.options.contains(&KeyValueOption { option_name: "ENABLE".to_string(), - option_type: DataLoadingOptionType::BOOLEAN, + option_type: KeyValueOptionType::BOOLEAN, value: "TRUE".to_string() })); - assert!(directory_table_params.options.contains(&DataLoadingOption { + assert!(directory_table_params.options.contains(&KeyValueOption { option_name: "REFRESH_ON_CREATE".to_string(), - option_type: DataLoadingOptionType::BOOLEAN, + option_type: KeyValueOptionType::BOOLEAN, value: "FALSE".to_string() })); - assert!(directory_table_params.options.contains(&DataLoadingOption { + assert!(directory_table_params.options.contains(&KeyValueOption { option_name: "NOTIFICATION_INTEGRATION".to_string(), - option_type: DataLoadingOptionType::STRING, + option_type: KeyValueOptionType::STRING, value: "some-string".to_string() })); } @@ -1971,19 +1970,19 @@ fn test_create_stage_with_file_format() { match snowflake_without_unescape().verified_stmt(sql) { Statement::CreateStage { file_format, .. } => { - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "COMPRESSION".to_string(), - option_type: DataLoadingOptionType::ENUM, + option_type: KeyValueOptionType::ENUM, value: "AUTO".to_string() })); - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "BINARY_FORMAT".to_string(), - option_type: DataLoadingOptionType::ENUM, + option_type: KeyValueOptionType::ENUM, value: "HEX".to_string() })); - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "ESCAPE".to_string(), - option_type: DataLoadingOptionType::STRING, + option_type: KeyValueOptionType::STRING, value: r#"\\"#.to_string() })); } @@ -2004,14 +2003,14 @@ fn test_create_stage_with_copy_options() { ); match snowflake().verified_stmt(sql) { Statement::CreateStage { copy_options, .. } => { - assert!(copy_options.options.contains(&DataLoadingOption { + assert!(copy_options.options.contains(&KeyValueOption { option_name: "ON_ERROR".to_string(), - option_type: DataLoadingOptionType::ENUM, + option_type: KeyValueOptionType::ENUM, value: "CONTINUE".to_string() })); - assert!(copy_options.options.contains(&DataLoadingOption { + assert!(copy_options.options.contains(&KeyValueOption { option_name: "FORCE".to_string(), - option_type: DataLoadingOptionType::BOOLEAN, + option_type: KeyValueOptionType::BOOLEAN, value: "TRUE".to_string() })); } @@ -2144,33 +2143,33 @@ fn test_copy_into_with_stage_params() { assert!(stage_params .credentials .options - .contains(&DataLoadingOption { + .contains(&KeyValueOption { option_name: "AWS_KEY_ID".to_string(), - option_type: DataLoadingOptionType::STRING, + option_type: KeyValueOptionType::STRING, value: "1a2b3c".to_string() })); assert!(stage_params .credentials .options - .contains(&DataLoadingOption { + .contains(&KeyValueOption { option_name: "AWS_SECRET_KEY".to_string(), - option_type: DataLoadingOptionType::STRING, + option_type: KeyValueOptionType::STRING, value: "4x5y6z".to_string() })); assert!(stage_params .encryption .options - .contains(&DataLoadingOption { + .contains(&KeyValueOption { option_name: "MASTER_KEY".to_string(), - option_type: DataLoadingOptionType::STRING, + option_type: KeyValueOptionType::STRING, value: "key".to_string() })); assert!(stage_params .encryption .options - .contains(&DataLoadingOption { + .contains(&KeyValueOption { option_name: "TYPE".to_string(), - option_type: DataLoadingOptionType::STRING, + option_type: KeyValueOptionType::STRING, value: "AWS_SSE_KMS".to_string() })); } @@ -2300,19 +2299,19 @@ fn test_copy_into_file_format() { match snowflake_without_unescape().verified_stmt(sql) { Statement::CopyIntoSnowflake { file_format, .. } => { - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "COMPRESSION".to_string(), - option_type: DataLoadingOptionType::ENUM, + option_type: KeyValueOptionType::ENUM, value: "AUTO".to_string() })); - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "BINARY_FORMAT".to_string(), - option_type: DataLoadingOptionType::ENUM, + option_type: KeyValueOptionType::ENUM, value: "HEX".to_string() })); - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "ESCAPE".to_string(), - option_type: DataLoadingOptionType::STRING, + option_type: KeyValueOptionType::STRING, value: r#"\\"#.to_string() })); } @@ -2339,19 +2338,19 @@ fn test_copy_into_file_format() { .unwrap() { Statement::CopyIntoSnowflake { file_format, .. } => { - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "COMPRESSION".to_string(), - option_type: DataLoadingOptionType::ENUM, + option_type: KeyValueOptionType::ENUM, value: "AUTO".to_string() })); - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "BINARY_FORMAT".to_string(), - option_type: DataLoadingOptionType::ENUM, + option_type: KeyValueOptionType::ENUM, value: "HEX".to_string() })); - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "ESCAPE".to_string(), - option_type: DataLoadingOptionType::STRING, + option_type: KeyValueOptionType::STRING, value: r#"\\"#.to_string() })); } @@ -2371,14 +2370,14 @@ fn test_copy_into_copy_options() { match snowflake().verified_stmt(sql) { Statement::CopyIntoSnowflake { copy_options, .. } => { - assert!(copy_options.options.contains(&DataLoadingOption { + assert!(copy_options.options.contains(&KeyValueOption { option_name: "ON_ERROR".to_string(), - option_type: DataLoadingOptionType::ENUM, + option_type: KeyValueOptionType::ENUM, value: "CONTINUE".to_string() })); - assert!(copy_options.options.contains(&DataLoadingOption { + assert!(copy_options.options.contains(&KeyValueOption { option_name: "FORCE".to_string(), - option_type: DataLoadingOptionType::BOOLEAN, + option_type: KeyValueOptionType::BOOLEAN, value: "TRUE".to_string() })); } @@ -3407,12 +3406,25 @@ fn test_grant_database_role_to() { #[test] fn test_alter_session() { - snowflake().verified_stmt("ALTER SESSION SET"); - snowflake().verified_stmt("ALTER SESSION UNSET"); + assert_eq!( + snowflake() + .parse_sql_statements("ALTER SESSION SET") + .unwrap_err() + .to_string(), + "sql parser error: expected at least one option" + ); + assert_eq!( + snowflake() + .parse_sql_statements("ALTER SESSION UNSET") + .unwrap_err() + .to_string(), + "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().verified_stmt("ALTER SESSION UNSET AUTOCOMMIT"); - snowflake().verified_stmt("ALTER SESSION UNSET AUTOCOMMIT QUERY_TAG"); + 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'", @@ -3421,12 +3433,8 @@ fn test_alter_session() { "ALTER SESSION SET A=true \nB='tag'", "ALTER SESSION SET A=TRUE B='tag'", ); - snowflake().one_statement_parses_to( - "ALTER SESSION UNSET a, b", - "ALTER SESSION UNSET a b", - ); snowflake().one_statement_parses_to( "ALTER SESSION UNSET a\nB", - "ALTER SESSION UNSET a B", + "ALTER SESSION UNSET a, B", ); } From d1a535b118cc557e233d8808e7e9a533e6d3ea25 Mon Sep 17 00:00:00 2001 From: osipovartem Date: Wed, 12 Feb 2025 12:08:05 +0300 Subject: [PATCH 6/7] Move key_value to a separate helper file --- src/ast/helpers/key_value_options.rs | 5 +- src/ast/helpers/mod.rs | 2 +- src/ast/helpers/stmt_data_loading.rs | 4 +- src/ast/mod.rs | 8 +- src/dialect/snowflake.rs | 47 ++++++------ tests/sqlparser_snowflake.rs | 111 ++++++++++----------------- 6 files changed, 75 insertions(+), 102 deletions(-) diff --git a/src/ast/helpers/key_value_options.rs b/src/ast/helpers/key_value_options.rs index e2ebdcd28..06f028dd2 100644 --- a/src/ast/helpers/key_value_options.rs +++ b/src/ast/helpers/key_value_options.rs @@ -57,7 +57,6 @@ pub struct KeyValueOption { pub value: String, } - impl fmt::Display for KeyValueOptions { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { if !self.options.is_empty() { @@ -81,9 +80,7 @@ impl fmt::Display for KeyValueOption { KeyValueOptionType::STRING => { write!(f, "{}='{}'", self.option_name, self.value)?; } - KeyValueOptionType::ENUM - | KeyValueOptionType::BOOLEAN - | KeyValueOptionType::NUMBER => { + KeyValueOptionType::ENUM | KeyValueOptionType::BOOLEAN | KeyValueOptionType::NUMBER => { write!(f, "{}={}", self.option_name, self.value)?; } } diff --git a/src/ast/helpers/mod.rs b/src/ast/helpers/mod.rs index b992b4682..55831220d 100644 --- a/src/ast/helpers/mod.rs +++ b/src/ast/helpers/mod.rs @@ -15,6 +15,6 @@ // specific language governing permissions and limitations // under the License. pub mod attached_token; +pub mod key_value_options; pub mod stmt_create_table; pub mod stmt_data_loading; -pub mod key_value_options; diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs index 138a98e7b..cc4fa12fa 100644 --- a/src/ast/helpers/stmt_data_loading.rs +++ b/src/ast/helpers/stmt_data_loading.rs @@ -24,15 +24,14 @@ use alloc::string::String; #[cfg(not(feature = "std"))] use alloc::vec::Vec; use core::fmt; -use core::fmt::Formatter; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use crate::ast::helpers::key_value_options::KeyValueOptions; use crate::ast::{Ident, ObjectName}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use crate::ast::helpers::key_value_options::KeyValueOptions; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -81,7 +80,6 @@ impl fmt::Display for StageParamsObject { } } - impl fmt::Display for StageLoadSelectItem { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if self.alias.is_some() { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 36d9ab148..594934c96 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4478,7 +4478,11 @@ impl fmt::Display for Statement { set, session_params, } => { - write!( f, "ALTER SESSION {set}", set = if *set { "SET"} else { "UNSET" })?; + write!( + f, + "ALTER SESSION {set}", + set = if *set { "SET" } else { "UNSET" } + )?; if !session_params.options.is_empty() { if *set { write!(f, " {}", session_params)?; @@ -4487,7 +4491,7 @@ impl fmt::Display for Statement { .options .iter() .map(|p| p.option_name.clone()) - .collect::>() ; + .collect::>(); write!(f, " {}", display_separated(&options, ", "))?; } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index ade66a871..c13da8320 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -17,13 +17,11 @@ #[cfg(not(feature = "std"))] use crate::alloc::string::ToString; +use crate::ast::helpers::key_value_options::{KeyValueOption, KeyValueOptionType, KeyValueOptions}; use crate::ast::helpers::stmt_create_table::CreateTableBuilder; use crate::ast::helpers::stmt_data_loading::{ FileStagingCommand, StageLoadSelectItem, StageParamsObject, }; -use crate::ast::helpers::key_value_options::{ - KeyValueOption, KeyValueOptionType, KeyValueOptions -}; use crate::ast::{ ColumnOption, ColumnPolicy, ColumnPolicyProperty, CopyIntoSnowflakeKind, Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, @@ -127,11 +125,11 @@ impl Dialect for SnowflakeDialect { let set = match parser.parse_one_of_keywords(&[Keyword::SET, Keyword::UNSET]) { Some(Keyword::SET) => true, Some(Keyword::UNSET) => false, - _ => return Some(parser.expected("SET or UNSET", parser.peek_token())) + _ => return Some(parser.expected("SET or UNSET", parser.peek_token())), }; return Some(parse_alter_session(parser, set)); } - + if parser.parse_keyword(Keyword::CREATE) { // possibly CREATE STAGE //[ OR REPLACE ] @@ -351,14 +349,12 @@ fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result fn parse_alter_session(parser: &mut Parser, set: bool) -> Result { let session_options = parse_session_options(parser, set)?; - Ok( - Statement::AlterSession { - set, - session_params: KeyValueOptions { - options: session_options, - }, - } - ) + Ok(Statement::AlterSession { + set, + session_params: KeyValueOptions { + options: session_options, + }, + }) } /// Parse snowflake create table statement. @@ -986,12 +982,14 @@ fn parse_stage_params(parser: &mut Parser) -> Result ] -fn parse_session_options(parser: &mut Parser, set: bool) -> Result, ParserError> { +fn parse_session_options( + parser: &mut Parser, + set: bool, +) -> Result, ParserError> { let mut options: Vec = Vec::new(); let empty = String::new; loop { @@ -1008,22 +1006,25 @@ fn parse_session_options(parser: &mut Parser, set: bool) -> Result { if parser.peek_token().token == Token::EOF { - break + break; } - return parser.expected("another option", parser.peek_token()) - }, + return parser.expected("another option", parser.peek_token()); + } } } - options.is_empty() - .then(|| Err(ParserError::ParserError("expected at least one option".to_string()))) + options + .is_empty() + .then(|| { + Err(ParserError::ParserError( + "expected at least one option".to_string(), + )) + }) .unwrap_or(Ok(options)) } - /// Parses options provided within parentheses like: /// ( ENABLE = { TRUE | FALSE } /// [ AUTO_REFRESH = { TRUE | FALSE } ] diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index d1ca483d1..f5cc75245 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1887,38 +1887,26 @@ fn test_create_stage_with_stage_params() { "", stage_params.endpoint.unwrap() ); - assert!(stage_params - .credentials - .options - .contains(&KeyValueOption { - option_name: "AWS_KEY_ID".to_string(), - option_type: KeyValueOptionType::STRING, - value: "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() - })); - assert!(stage_params - .encryption - .options - .contains(&KeyValueOption { - option_name: "MASTER_KEY".to_string(), - option_type: KeyValueOptionType::STRING, - value: "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() - })); + assert!(stage_params.credentials.options.contains(&KeyValueOption { + option_name: "AWS_KEY_ID".to_string(), + option_type: KeyValueOptionType::STRING, + value: "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() + })); + assert!(stage_params.encryption.options.contains(&KeyValueOption { + option_name: "MASTER_KEY".to_string(), + option_type: KeyValueOptionType::STRING, + value: "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() + })); } _ => unreachable!(), }; @@ -2140,38 +2128,26 @@ fn test_copy_into_with_stage_params() { "", stage_params.endpoint.unwrap() ); - assert!(stage_params - .credentials - .options - .contains(&KeyValueOption { - option_name: "AWS_KEY_ID".to_string(), - option_type: KeyValueOptionType::STRING, - value: "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() - })); - assert!(stage_params - .encryption - .options - .contains(&KeyValueOption { - option_name: "MASTER_KEY".to_string(), - option_type: KeyValueOptionType::STRING, - value: "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() - })); + assert!(stage_params.credentials.options.contains(&KeyValueOption { + option_name: "AWS_KEY_ID".to_string(), + option_type: KeyValueOptionType::STRING, + value: "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() + })); + assert!(stage_params.encryption.options.contains(&KeyValueOption { + option_name: "MASTER_KEY".to_string(), + option_type: KeyValueOptionType::STRING, + value: "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() + })); } _ => unreachable!(), }; @@ -3420,7 +3396,7 @@ fn test_alter_session() { .to_string(), "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().verified_stmt("ALTER SESSION UNSET AUTOCOMMIT"); @@ -3433,8 +3409,5 @@ fn test_alter_session() { "ALTER SESSION SET A=true \nB='tag'", "ALTER SESSION SET A=TRUE B='tag'", ); - snowflake().one_statement_parses_to( - "ALTER SESSION UNSET a\nB", - "ALTER SESSION UNSET a, B", - ); + snowflake().one_statement_parses_to("ALTER SESSION UNSET a\nB", "ALTER SESSION UNSET a, B"); } From 28772e2344b7822615dcb3e89c8b5d147974b416 Mon Sep 17 00:00:00 2001 From: osipovartem Date: Thu, 20 Feb 2025 11:44:10 +0300 Subject: [PATCH 7/7] Fix invalid_html_tags --- src/dialect/snowflake.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index c13da8320..4f213191f 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -985,7 +985,7 @@ fn parse_stage_params(parser: &mut Parser) -> Result ] +/// [ BINARY_INPUT_FORMAT = '\' ] fn parse_session_options( parser: &mut Parser, set: bool,