diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c7f461418..c904d4bc9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -48,7 +48,7 @@ pub use self::query::{ MatchRecognizePattern, MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OrderByExpr, PivotValueSource, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, - SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, SymbolDefinition, Table, + SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableFactor, TableVersion, TableWithJoins, Top, TopQuantity, ValueTableMode, Values, WildcardAdditionalOptions, With, }; diff --git a/src/ast/query.rs b/src/ast/query.rs index d00a0dfcc..241e45a9c 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -50,6 +50,10 @@ pub struct Query { /// `FOR JSON { AUTO | PATH } [ , INCLUDE_NULL_VALUES ]` /// (MSSQL-specific) pub for_clause: Option, + /// ClickHouse syntax: `SELECT * FROM t SETTINGS key1 = value1, key2 = value2` + /// + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/select#settings-in-select-query) + pub settings: Option>, } impl fmt::Display for Query { @@ -70,6 +74,9 @@ impl fmt::Display for Query { if !self.limit_by.is_empty() { write!(f, " BY {}", display_separated(&self.limit_by, ", "))?; } + if let Some(ref settings) = self.settings { + write!(f, " SETTINGS {}", display_comma_separated(settings))?; + } if let Some(ref fetch) = self.fetch { write!(f, " {fetch}")?; } @@ -828,6 +835,20 @@ impl fmt::Display for ConnectBy { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Setting { + pub key: Ident, + pub value: Value, +} + +impl fmt::Display for Setting { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} = {}", self.key, self.value) + } +} + /// An expression optionally followed by an alias. /// /// Example: diff --git a/src/keywords.rs b/src/keywords.rs index 5db55e9da..cbba92c5b 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -650,6 +650,7 @@ define_keywords!( SESSION_USER, SET, SETS, + SETTINGS, SHARE, SHOW, SIMILAR, @@ -850,6 +851,8 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::FOR, // for MYSQL PARTITION SELECTION Keyword::PARTITION, + // for ClickHouse SELECT * FROM t SETTINGS ... + Keyword::SETTINGS, // for Snowflake START WITH .. CONNECT BY Keyword::START, Keyword::CONNECT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4e9c3836b..7614307bf 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7871,6 +7871,7 @@ impl<'a> Parser<'a> { fetch: None, locks: vec![], for_clause: None, + settings: None, }) } else if self.parse_keyword(Keyword::UPDATE) { Ok(Query { @@ -7883,6 +7884,7 @@ impl<'a> Parser<'a> { fetch: None, locks: vec![], for_clause: None, + settings: None, }) } else { let body = self.parse_boxed_query_body(0)?; @@ -7928,6 +7930,20 @@ impl<'a> Parser<'a> { vec![] }; + let settings = if dialect_of!(self is ClickHouseDialect|GenericDialect) + && self.parse_keyword(Keyword::SETTINGS) + { + let key_values = self.parse_comma_separated(|p| { + let key = p.parse_identifier(false)?; + p.expect_token(&Token::Eq)?; + let value = p.parse_value()?; + Ok(Setting { key, value }) + })?; + Some(key_values) + } else { + None + }; + let fetch = if self.parse_keyword(Keyword::FETCH) { Some(self.parse_fetch()?) } else { @@ -7955,6 +7971,7 @@ impl<'a> Parser<'a> { fetch, locks, for_clause, + settings, }) } } @@ -9091,6 +9108,7 @@ impl<'a> Parser<'a> { fetch: None, locks: vec![], for_clause: None, + settings: None, }), alias, }) diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 0c188a24b..b3e03c4ab 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -21,8 +21,8 @@ use test_utils::*; use sqlparser::ast::Expr::{BinaryOp, Identifier, MapAccess}; use sqlparser::ast::SelectItem::UnnamedExpr; use sqlparser::ast::TableFactor::Table; +use sqlparser::ast::Value::Number; use sqlparser::ast::*; - use sqlparser::dialect::ClickHouseDialect; use sqlparser::dialect::GenericDialect; @@ -549,6 +549,42 @@ fn parse_limit_by() { ); } +#[test] +fn parse_settings_in_query() { + match clickhouse_and_generic() + .verified_stmt(r#"SELECT * FROM t SETTINGS max_threads = 1, max_block_size = 10000"#) + { + Statement::Query(query) => { + assert_eq!( + query.settings, + Some(vec![ + Setting { + key: Ident::new("max_threads"), + value: Number("1".parse().unwrap(), false) + }, + Setting { + key: Ident::new("max_block_size"), + value: Number("10000".parse().unwrap(), false) + }, + ]) + ); + } + _ => unreachable!(), + } + + let invalid_cases = vec![ + "SELECT * FROM t SETTINGS a", + "SELECT * FROM t SETTINGS a=", + "SELECT * FROM t SETTINGS a=1, b", + "SELECT * FROM t SETTINGS a=1, b=", + "SELECT * FROM t SETTINGS a=1, b=c", + ]; + for sql in invalid_cases { + clickhouse_and_generic() + .parse_sql_statements(sql) + .expect_err("Expected: SETTINGS key = value, found: "); + } +} #[test] fn parse_select_star_except() { clickhouse().verified_stmt("SELECT * EXCEPT (prev_status) FROM anomalies"); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ac2133946..609d2600d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -413,6 +413,7 @@ fn parse_update_set_from() { fetch: None, locks: vec![], for_clause: None, + settings: None, }), alias: Some(TableAlias { name: Ident::new("t2"), @@ -3427,6 +3428,7 @@ fn parse_create_table_as_table() { fetch: None, locks: vec![], for_clause: None, + settings: None, }); match verified_stmt(sql1) { @@ -3452,6 +3454,7 @@ fn parse_create_table_as_table() { fetch: None, locks: vec![], for_clause: None, + settings: None, }); match verified_stmt(sql2) { @@ -4996,6 +4999,7 @@ fn parse_interval_and_or_xor() { fetch: None, locks: vec![], for_clause: None, + settings: None, }))]; assert_eq!(actual_ast, expected_ast); @@ -7649,6 +7653,7 @@ fn parse_merge() { fetch: None, locks: vec![], for_clause: None, + settings: None, }), alias: Some(TableAlias { name: Ident { @@ -9156,6 +9161,7 @@ fn parse_unload() { locks: vec![], for_clause: None, order_by: vec![], + settings: None, }), to: Ident { value: "s3://...".to_string(), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 993850299..84ab474b0 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -103,6 +103,7 @@ fn parse_create_procedure() { locks: vec![], for_clause: None, order_by: vec![], + settings: None, body: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, @@ -546,6 +547,7 @@ fn parse_substring_in_select() { fetch: None, locks: vec![], for_clause: None, + settings: None, }), query ); diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 4c18d4a75..cf9b717be 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -925,6 +925,7 @@ fn parse_escaped_quote_identifiers_with_escape() { fetch: None, locks: vec![], for_clause: None, + settings: None, })) ); } @@ -972,6 +973,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { fetch: None, locks: vec![], for_clause: None, + settings: None, })) ); } @@ -1016,6 +1018,7 @@ fn parse_escaped_backticks_with_escape() { fetch: None, locks: vec![], for_clause: None, + settings: None, })) ); } @@ -1060,6 +1063,7 @@ fn parse_escaped_backticks_with_no_escape() { fetch: None, locks: vec![], for_clause: None, + settings: None, })) ); } @@ -1264,6 +1268,7 @@ fn parse_simple_insert() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), source ); @@ -1306,6 +1311,7 @@ fn parse_ignore_insert() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), source ); @@ -1348,6 +1354,7 @@ fn parse_priority_insert() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), source ); @@ -1387,6 +1394,7 @@ fn parse_priority_insert() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), source ); @@ -1434,6 +1442,7 @@ fn parse_insert_as() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), source ); @@ -1493,6 +1502,7 @@ fn parse_insert_as() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), source ); @@ -1536,6 +1546,7 @@ fn parse_replace_insert() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), source ); @@ -1573,6 +1584,7 @@ fn parse_empty_row_insert() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), source ); @@ -1633,6 +1645,7 @@ fn parse_insert_with_on_duplicate_update() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), source ); @@ -2273,6 +2286,7 @@ fn parse_substring_in_select() { fetch: None, locks: vec![], for_clause: None, + settings: None, }), query ); @@ -2578,6 +2592,7 @@ fn parse_hex_string_introducer() { fetch: None, locks: vec![], for_clause: None, + settings: None, })) ) } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 2606fb96e..243116a3f 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1093,6 +1093,7 @@ fn parse_copy_to() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), to: true, target: CopyTarget::File { @@ -2421,6 +2422,7 @@ fn parse_array_subquery_expr() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), filter: None, null_treatment: None, @@ -3941,7 +3943,8 @@ fn test_simple_postgres_insert_with_alias() { offset: None, fetch: None, locks: vec![], - for_clause: None + for_clause: None, + settings: None, })), partitioned: None, after_columns: vec![], @@ -4008,7 +4011,8 @@ fn test_simple_postgres_insert_with_alias() { offset: None, fetch: None, locks: vec![], - for_clause: None + for_clause: None, + settings: None, })), partitioned: None, after_columns: vec![], @@ -4071,7 +4075,8 @@ fn test_simple_insert_with_quoted_alias() { offset: None, fetch: None, locks: vec![], - for_clause: None + for_clause: None, + settings: None, })), partitioned: None, after_columns: vec![],