diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c904d4bc9..6ebabcfd3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -43,14 +43,14 @@ pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode, ExceptSelectItem, ExcludeSelectItem, ExprWithAlias, Fetch, ForClause, ForJson, ForXml, - GroupByExpr, GroupByWithModifier, IdentWithAlias, IlikeSelectItem, Join, JoinConstraint, - JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, LateralView, LockClause, LockType, - MatchRecognizePattern, MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, - NonBlock, Offset, OffsetRows, OrderByExpr, PivotValueSource, Query, RenameSelectItem, - RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, - SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, - TableAlias, TableFactor, TableVersion, TableWithJoins, Top, TopQuantity, ValueTableMode, - Values, WildcardAdditionalOptions, With, + FormatClause, GroupByExpr, GroupByWithModifier, IdentWithAlias, IlikeSelectItem, Join, + JoinConstraint, JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, LateralView, + LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure, + NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OrderByExpr, + PivotValueSource, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, + ReplaceSelectItem, RowsPerMatch, Select, SelectInto, SelectItem, SetExpr, SetOperator, + SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableFactor, TableVersion, + TableWithJoins, Top, TopQuantity, ValueTableMode, Values, WildcardAdditionalOptions, With, }; pub use self::value::{ escape_double_quote_string, escape_quoted_string, DateTimeField, DollarQuotedString, diff --git a/src/ast/query.rs b/src/ast/query.rs index 7d2626b2d..70c781409 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -54,6 +54,11 @@ pub struct Query { /// /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/select#settings-in-select-query) pub settings: Option>, + /// `SELECT * FROM t FORMAT JSONCompact` + /// + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/select/format) + /// (ClickHouse-specific) + pub format_clause: Option, } impl fmt::Display for Query { @@ -86,6 +91,9 @@ impl fmt::Display for Query { if let Some(ref for_clause) = self.for_clause { write!(f, " {}", for_clause)?; } + if let Some(ref format) = self.format_clause { + write!(f, " {}", format)?; + } Ok(()) } } @@ -1959,6 +1967,26 @@ impl fmt::Display for GroupByExpr { } } +/// FORMAT identifier or FORMAT NULL clause, specific to ClickHouse. +/// +/// [ClickHouse]: +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum FormatClause { + Identifier(Ident), + Null, +} + +impl fmt::Display for FormatClause { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FormatClause::Identifier(ident) => write!(f, "FORMAT {}", ident), + FormatClause::Null => write!(f, "FORMAT NULL"), + } + } +} + /// FOR XML or FOR JSON clause, specific to MSSQL /// (formats the output of a query as XML or JSON) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/keywords.rs b/src/keywords.rs index eb69a209b..edd3271f3 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -856,6 +856,8 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::PREWHERE, // for ClickHouse SELECT * FROM t SETTINGS ... Keyword::SETTINGS, + // for ClickHouse SELECT * FROM t FORMAT... + Keyword::FORMAT, // for Snowflake START WITH .. CONNECT BY Keyword::START, Keyword::CONNECT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a81d53e7c..76fde30bd 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7872,6 +7872,7 @@ impl<'a> Parser<'a> { locks: vec![], for_clause: None, settings: None, + format_clause: None, }) } else if self.parse_keyword(Keyword::UPDATE) { Ok(Query { @@ -7885,6 +7886,7 @@ impl<'a> Parser<'a> { locks: vec![], for_clause: None, settings: None, + format_clause: None, }) } else { let body = self.parse_boxed_query_body(0)?; @@ -7960,6 +7962,18 @@ impl<'a> Parser<'a> { locks.push(self.parse_lock()?); } } + let format_clause = if dialect_of!(self is ClickHouseDialect | GenericDialect) + && self.parse_keyword(Keyword::FORMAT) + { + if self.parse_keyword(Keyword::NULL) { + Some(FormatClause::Null) + } else { + let ident = self.parse_identifier(false)?; + Some(FormatClause::Identifier(ident)) + } + } else { + None + }; Ok(Query { with, @@ -7972,6 +7986,7 @@ impl<'a> Parser<'a> { locks, for_clause, settings, + format_clause, }) } } @@ -9118,6 +9133,7 @@ impl<'a> Parser<'a> { locks: vec![], for_clause: None, settings: None, + format_clause: None, }), alias, }) diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 29a5b15aa..f6b787f5c 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -768,6 +768,38 @@ fn test_prewhere() { } } +#[test] +fn test_query_with_format_clause() { + let format_options = vec!["TabSeparated", "JSONCompact", "NULL"]; + for format in &format_options { + let sql = format!("SELECT * FROM t FORMAT {}", format); + match clickhouse_and_generic().verified_stmt(&sql) { + Statement::Query(query) => { + if *format == "NULL" { + assert_eq!(query.format_clause, Some(FormatClause::Null)); + } else { + assert_eq!( + query.format_clause, + Some(FormatClause::Identifier(Ident::new(*format))) + ); + } + } + _ => unreachable!(), + } + } + + let invalid_cases = [ + "SELECT * FROM t FORMAT", + "SELECT * FROM t FORMAT TabSeparated JSONCompact", + "SELECT * FROM t FORMAT TabSeparated TabSeparated", + ]; + for sql in &invalid_cases { + clickhouse_and_generic() + .parse_sql_statements(sql) + .expect_err("Expected: FORMAT {identifier}, found: "); + } +} + fn clickhouse() -> TestedDialects { TestedDialects { dialects: vec![Box::new(ClickHouseDialect {})], diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 256680b3e..df171dc8f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -415,6 +415,7 @@ fn parse_update_set_from() { locks: vec![], for_clause: None, settings: None, + format_clause: None, }), alias: Some(TableAlias { name: Ident::new("t2"), @@ -3430,6 +3431,7 @@ fn parse_create_table_as_table() { locks: vec![], for_clause: None, settings: None, + format_clause: None, }); match verified_stmt(sql1) { @@ -3456,6 +3458,7 @@ fn parse_create_table_as_table() { locks: vec![], for_clause: None, settings: None, + format_clause: None, }); match verified_stmt(sql2) { @@ -5003,6 +5006,7 @@ fn parse_interval_and_or_xor() { locks: vec![], for_clause: None, settings: None, + format_clause: None, }))]; assert_eq!(actual_ast, expected_ast); @@ -7659,6 +7663,7 @@ fn parse_merge() { locks: vec![], for_clause: None, settings: None, + format_clause: None, }), alias: Some(TableAlias { name: Ident { @@ -9168,6 +9173,7 @@ fn parse_unload() { for_clause: None, order_by: vec![], settings: None, + format_clause: None, }), to: Ident { value: "s3://...".to_string(), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index e0e0f7c70..6968347ec 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -104,6 +104,7 @@ fn parse_create_procedure() { for_clause: None, order_by: vec![], settings: None, + format_clause: None, body: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, @@ -550,6 +551,7 @@ fn parse_substring_in_select() { locks: vec![], for_clause: None, settings: None, + format_clause: None, }), query ); diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index a5fa75200..74def31bf 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -927,6 +927,7 @@ fn parse_escaped_quote_identifiers_with_escape() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })) ); } @@ -976,6 +977,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })) ); } @@ -1022,6 +1024,7 @@ fn parse_escaped_backticks_with_escape() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })) ); } @@ -1068,6 +1071,7 @@ fn parse_escaped_backticks_with_no_escape() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })) ); } @@ -1273,6 +1277,7 @@ fn parse_simple_insert() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), source ); @@ -1316,6 +1321,7 @@ fn parse_ignore_insert() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), source ); @@ -1359,6 +1365,7 @@ fn parse_priority_insert() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), source ); @@ -1399,6 +1406,7 @@ fn parse_priority_insert() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), source ); @@ -1447,6 +1455,7 @@ fn parse_insert_as() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), source ); @@ -1507,6 +1516,7 @@ fn parse_insert_as() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), source ); @@ -1551,6 +1561,7 @@ fn parse_replace_insert() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), source ); @@ -1589,6 +1600,7 @@ fn parse_empty_row_insert() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), source ); @@ -1650,6 +1662,7 @@ fn parse_insert_with_on_duplicate_update() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), source ); @@ -2294,6 +2307,7 @@ fn parse_substring_in_select() { locks: vec![], for_clause: None, settings: None, + format_clause: None, }), query ); @@ -2601,6 +2615,7 @@ fn parse_hex_string_introducer() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })) ) } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 2d3097cf9..bf493a5e6 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1095,6 +1095,7 @@ fn parse_copy_to() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), to: true, target: CopyTarget::File { @@ -2426,6 +2427,7 @@ fn parse_array_subquery_expr() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), filter: None, null_treatment: None, @@ -3948,6 +3950,7 @@ fn test_simple_postgres_insert_with_alias() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), partitioned: None, after_columns: vec![], @@ -4016,6 +4019,7 @@ fn test_simple_postgres_insert_with_alias() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), partitioned: None, after_columns: vec![], @@ -4080,6 +4084,7 @@ fn test_simple_insert_with_quoted_alias() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), partitioned: None, after_columns: vec![],