Skip to content

Commit 6776f83

Browse files
committed
Add support of FORMAT clause for ClickHouse parser
ClickHouse allows to use the FORMAT clause to choose the select result format: `SELECT * FORM table FORMAT NULL|Identifier` For more information, please refer to: https://clickhouse.com/docs/en/sql-reference/statements/select/format
1 parent 0884dd9 commit 6776f83

9 files changed

+114
-8
lines changed

src/ast/mod.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,14 @@ pub use self::operator::{BinaryOperator, UnaryOperator};
4343
pub use self::query::{
4444
AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode,
4545
ExceptSelectItem, ExcludeSelectItem, ExprWithAlias, Fetch, ForClause, ForJson, ForXml,
46-
GroupByExpr, GroupByWithModifier, IdentWithAlias, IlikeSelectItem, Join, JoinConstraint,
47-
JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, LateralView, LockClause, LockType,
48-
MatchRecognizePattern, MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr,
49-
NonBlock, Offset, OffsetRows, OrderByExpr, PivotValueSource, Query, RenameSelectItem,
50-
RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select,
51-
SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table,
52-
TableAlias, TableFactor, TableVersion, TableWithJoins, Top, TopQuantity, ValueTableMode,
53-
Values, WildcardAdditionalOptions, With,
46+
FormatClause, GroupByExpr, GroupByWithModifier, IdentWithAlias, IlikeSelectItem, Join,
47+
JoinConstraint, JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, LateralView,
48+
LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure,
49+
NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OrderByExpr,
50+
PivotValueSource, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement,
51+
ReplaceSelectItem, RowsPerMatch, Select, SelectInto, SelectItem, SetExpr, SetOperator,
52+
SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableFactor, TableVersion,
53+
TableWithJoins, Top, TopQuantity, ValueTableMode, Values, WildcardAdditionalOptions, With,
5454
};
5555
pub use self::value::{
5656
escape_double_quote_string, escape_quoted_string, DateTimeField, DollarQuotedString,

src/ast/query.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ pub struct Query {
5454
///
5555
/// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/select#settings-in-select-query)
5656
pub settings: Option<Vec<Setting>>,
57+
/// `SELECT * FROM t FORMAT JSONCompact`
58+
///
59+
/// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/select/format)
60+
/// (ClickHouse-specific)
61+
pub format_clause: Option<FormatClause>,
5762
}
5863

5964
impl fmt::Display for Query {
@@ -86,6 +91,9 @@ impl fmt::Display for Query {
8691
if let Some(ref for_clause) = self.for_clause {
8792
write!(f, " {}", for_clause)?;
8893
}
94+
if let Some(ref format) = self.format_clause {
95+
write!(f, " {}", format)?;
96+
}
8997
Ok(())
9098
}
9199
}
@@ -1959,6 +1967,26 @@ impl fmt::Display for GroupByExpr {
19591967
}
19601968
}
19611969

1970+
/// FORMAT identifier or FORMAT NULL clause, specific to ClickHouse.
1971+
///
1972+
/// [ClickHouse]: <https://clickhouse.com/docs/en/sql-reference/statements/select/format>
1973+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1974+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1975+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
1976+
pub enum FormatClause {
1977+
Identifier(Ident),
1978+
Null,
1979+
}
1980+
1981+
impl fmt::Display for FormatClause {
1982+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1983+
match self {
1984+
FormatClause::Identifier(ident) => write!(f, "FORMAT {}", ident),
1985+
FormatClause::Null => write!(f, "FORMAT NULL"),
1986+
}
1987+
}
1988+
}
1989+
19621990
/// FOR XML or FOR JSON clause, specific to MSSQL
19631991
/// (formats the output of a query as XML or JSON)
19641992
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]

src/keywords.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,8 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[
856856
Keyword::PREWHERE,
857857
// for ClickHouse SELECT * FROM t SETTINGS ...
858858
Keyword::SETTINGS,
859+
// for ClickHouse SELECT * FROM t FORMAT...
860+
Keyword::FORMAT,
859861
// for Snowflake START WITH .. CONNECT BY
860862
Keyword::START,
861863
Keyword::CONNECT,

src/parser/mod.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7872,6 +7872,7 @@ impl<'a> Parser<'a> {
78727872
locks: vec![],
78737873
for_clause: None,
78747874
settings: None,
7875+
format_clause: None,
78757876
})
78767877
} else if self.parse_keyword(Keyword::UPDATE) {
78777878
Ok(Query {
@@ -7885,6 +7886,7 @@ impl<'a> Parser<'a> {
78857886
locks: vec![],
78867887
for_clause: None,
78877888
settings: None,
7889+
format_clause: None,
78887890
})
78897891
} else {
78907892
let body = self.parse_boxed_query_body(0)?;
@@ -7960,6 +7962,18 @@ impl<'a> Parser<'a> {
79607962
locks.push(self.parse_lock()?);
79617963
}
79627964
}
7965+
let format_clause = if dialect_of!(self is ClickHouseDialect | GenericDialect)
7966+
&& self.parse_keyword(Keyword::FORMAT)
7967+
{
7968+
if self.parse_keyword(Keyword::NULL) {
7969+
Some(FormatClause::Null)
7970+
} else {
7971+
let ident = self.parse_identifier(false)?;
7972+
Some(FormatClause::Identifier(ident))
7973+
}
7974+
} else {
7975+
None
7976+
};
79637977

79647978
Ok(Query {
79657979
with,
@@ -7972,6 +7986,7 @@ impl<'a> Parser<'a> {
79727986
locks,
79737987
for_clause,
79747988
settings,
7989+
format_clause,
79757990
})
79767991
}
79777992
}
@@ -9118,6 +9133,7 @@ impl<'a> Parser<'a> {
91189133
locks: vec![],
91199134
for_clause: None,
91209135
settings: None,
9136+
format_clause: None,
91219137
}),
91229138
alias,
91239139
})

tests/sqlparser_clickhouse.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,38 @@ fn test_prewhere() {
768768
}
769769
}
770770

771+
#[test]
772+
fn test_query_with_format_clause() {
773+
let format_options = vec!["TabSeparated", "JSONCompact", "NULL"];
774+
for format in &format_options {
775+
let sql = format!("SELECT * FROM t FORMAT {}", format);
776+
match clickhouse_and_generic().verified_stmt(&sql) {
777+
Statement::Query(query) => {
778+
if *format == "NULL" {
779+
assert_eq!(query.format_clause, Some(FormatClause::Null));
780+
} else {
781+
assert_eq!(
782+
query.format_clause,
783+
Some(FormatClause::Identifier(Ident::new(*format)))
784+
);
785+
}
786+
}
787+
_ => unreachable!(),
788+
}
789+
}
790+
791+
let invalid_cases = [
792+
"SELECT * FROM t FORMAT",
793+
"SELECT * FROM t FORMAT TabSeparated JSONCompact",
794+
"SELECT * FROM t FORMAT TabSeparated TabSeparated",
795+
];
796+
for sql in &invalid_cases {
797+
clickhouse_and_generic()
798+
.parse_sql_statements(sql)
799+
.expect_err("Expected: FORMAT {identifier}, found: ");
800+
}
801+
}
802+
771803
fn clickhouse() -> TestedDialects {
772804
TestedDialects {
773805
dialects: vec![Box::new(ClickHouseDialect {})],

tests/sqlparser_common.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@ fn parse_update_set_from() {
415415
locks: vec![],
416416
for_clause: None,
417417
settings: None,
418+
format_clause: None,
418419
}),
419420
alias: Some(TableAlias {
420421
name: Ident::new("t2"),
@@ -3430,6 +3431,7 @@ fn parse_create_table_as_table() {
34303431
locks: vec![],
34313432
for_clause: None,
34323433
settings: None,
3434+
format_clause: None,
34333435
});
34343436

34353437
match verified_stmt(sql1) {
@@ -3456,6 +3458,7 @@ fn parse_create_table_as_table() {
34563458
locks: vec![],
34573459
for_clause: None,
34583460
settings: None,
3461+
format_clause: None,
34593462
});
34603463

34613464
match verified_stmt(sql2) {
@@ -5003,6 +5006,7 @@ fn parse_interval_and_or_xor() {
50035006
locks: vec![],
50045007
for_clause: None,
50055008
settings: None,
5009+
format_clause: None,
50065010
}))];
50075011

50085012
assert_eq!(actual_ast, expected_ast);
@@ -7659,6 +7663,7 @@ fn parse_merge() {
76597663
locks: vec![],
76607664
for_clause: None,
76617665
settings: None,
7666+
format_clause: None,
76627667
}),
76637668
alias: Some(TableAlias {
76647669
name: Ident {
@@ -9168,6 +9173,7 @@ fn parse_unload() {
91689173
for_clause: None,
91699174
order_by: vec![],
91709175
settings: None,
9176+
format_clause: None,
91719177
}),
91729178
to: Ident {
91739179
value: "s3://...".to_string(),

tests/sqlparser_mssql.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ fn parse_create_procedure() {
104104
for_clause: None,
105105
order_by: vec![],
106106
settings: None,
107+
format_clause: None,
107108
body: Box::new(SetExpr::Select(Box::new(Select {
108109
distinct: None,
109110
top: None,
@@ -550,6 +551,7 @@ fn parse_substring_in_select() {
550551
locks: vec![],
551552
for_clause: None,
552553
settings: None,
554+
format_clause: None,
553555
}),
554556
query
555557
);

tests/sqlparser_mysql.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,7 @@ fn parse_escaped_quote_identifiers_with_escape() {
927927
locks: vec![],
928928
for_clause: None,
929929
settings: None,
930+
format_clause: None,
930931
}))
931932
);
932933
}
@@ -976,6 +977,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() {
976977
locks: vec![],
977978
for_clause: None,
978979
settings: None,
980+
format_clause: None,
979981
}))
980982
);
981983
}
@@ -1022,6 +1024,7 @@ fn parse_escaped_backticks_with_escape() {
10221024
locks: vec![],
10231025
for_clause: None,
10241026
settings: None,
1027+
format_clause: None,
10251028
}))
10261029
);
10271030
}
@@ -1068,6 +1071,7 @@ fn parse_escaped_backticks_with_no_escape() {
10681071
locks: vec![],
10691072
for_clause: None,
10701073
settings: None,
1074+
format_clause: None,
10711075
}))
10721076
);
10731077
}
@@ -1273,6 +1277,7 @@ fn parse_simple_insert() {
12731277
locks: vec![],
12741278
for_clause: None,
12751279
settings: None,
1280+
format_clause: None,
12761281
})),
12771282
source
12781283
);
@@ -1316,6 +1321,7 @@ fn parse_ignore_insert() {
13161321
locks: vec![],
13171322
for_clause: None,
13181323
settings: None,
1324+
format_clause: None,
13191325
})),
13201326
source
13211327
);
@@ -1359,6 +1365,7 @@ fn parse_priority_insert() {
13591365
locks: vec![],
13601366
for_clause: None,
13611367
settings: None,
1368+
format_clause: None,
13621369
})),
13631370
source
13641371
);
@@ -1399,6 +1406,7 @@ fn parse_priority_insert() {
13991406
locks: vec![],
14001407
for_clause: None,
14011408
settings: None,
1409+
format_clause: None,
14021410
})),
14031411
source
14041412
);
@@ -1447,6 +1455,7 @@ fn parse_insert_as() {
14471455
locks: vec![],
14481456
for_clause: None,
14491457
settings: None,
1458+
format_clause: None,
14501459
})),
14511460
source
14521461
);
@@ -1507,6 +1516,7 @@ fn parse_insert_as() {
15071516
locks: vec![],
15081517
for_clause: None,
15091518
settings: None,
1519+
format_clause: None,
15101520
})),
15111521
source
15121522
);
@@ -1551,6 +1561,7 @@ fn parse_replace_insert() {
15511561
locks: vec![],
15521562
for_clause: None,
15531563
settings: None,
1564+
format_clause: None,
15541565
})),
15551566
source
15561567
);
@@ -1589,6 +1600,7 @@ fn parse_empty_row_insert() {
15891600
locks: vec![],
15901601
for_clause: None,
15911602
settings: None,
1603+
format_clause: None,
15921604
})),
15931605
source
15941606
);
@@ -1650,6 +1662,7 @@ fn parse_insert_with_on_duplicate_update() {
16501662
locks: vec![],
16511663
for_clause: None,
16521664
settings: None,
1665+
format_clause: None,
16531666
})),
16541667
source
16551668
);
@@ -2294,6 +2307,7 @@ fn parse_substring_in_select() {
22942307
locks: vec![],
22952308
for_clause: None,
22962309
settings: None,
2310+
format_clause: None,
22972311
}),
22982312
query
22992313
);
@@ -2601,6 +2615,7 @@ fn parse_hex_string_introducer() {
26012615
locks: vec![],
26022616
for_clause: None,
26032617
settings: None,
2618+
format_clause: None,
26042619
}))
26052620
)
26062621
}

tests/sqlparser_postgres.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,7 @@ fn parse_copy_to() {
10951095
locks: vec![],
10961096
for_clause: None,
10971097
settings: None,
1098+
format_clause: None,
10981099
})),
10991100
to: true,
11001101
target: CopyTarget::File {
@@ -2426,6 +2427,7 @@ fn parse_array_subquery_expr() {
24262427
locks: vec![],
24272428
for_clause: None,
24282429
settings: None,
2430+
format_clause: None,
24292431
})),
24302432
filter: None,
24312433
null_treatment: None,
@@ -3948,6 +3950,7 @@ fn test_simple_postgres_insert_with_alias() {
39483950
locks: vec![],
39493951
for_clause: None,
39503952
settings: None,
3953+
format_clause: None,
39513954
})),
39523955
partitioned: None,
39533956
after_columns: vec![],
@@ -4016,6 +4019,7 @@ fn test_simple_postgres_insert_with_alias() {
40164019
locks: vec![],
40174020
for_clause: None,
40184021
settings: None,
4022+
format_clause: None,
40194023
})),
40204024
partitioned: None,
40214025
after_columns: vec![],
@@ -4080,6 +4084,7 @@ fn test_simple_insert_with_quoted_alias() {
40804084
locks: vec![],
40814085
for_clause: None,
40824086
settings: None,
4087+
format_clause: None,
40834088
})),
40844089
partitioned: None,
40854090
after_columns: vec![],

0 commit comments

Comments
 (0)