Skip to content

Commit

Permalink
Add support of FORMAT clause for ClickHouse parser
Browse files Browse the repository at this point in the history
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
  • Loading branch information
git-hulk committed Jul 8, 2024
1 parent 0884dd9 commit 3c4c5c1
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 8 deletions.
16 changes: 8 additions & 8 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
28 changes: 28 additions & 0 deletions src/ast/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ pub struct Query {
///
/// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/select#settings-in-select-query)
pub settings: Option<Vec<Setting>>,
/// `SELECT * FROM t FORMAT JSONCompact`
///
/// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/select/format)
/// (ClickHouse-specific)
pub format_clause: Option<FormatClause>,
}

impl fmt::Display for Query {
Expand Down Expand Up @@ -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(())
}
}
Expand Down Expand Up @@ -1959,6 +1967,26 @@ impl fmt::Display for GroupByExpr {
}
}

/// FORMAT identifier or FORMAT NULL clause, specific to ClickHouse.
///
/// [ClickHouse]: <https://clickhouse.com/docs/en/sql-reference/statements/select/format>
#[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)]
Expand Down
2 changes: 2 additions & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)?;
Expand Down Expand Up @@ -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,
Expand All @@ -7972,6 +7986,7 @@ impl<'a> Parser<'a> {
locks,
for_clause,
settings,
format_clause,
})
}
}
Expand Down Expand Up @@ -9118,6 +9133,7 @@ impl<'a> Parser<'a> {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
}),
alias,
})
Expand Down
32 changes: 32 additions & 0 deletions tests/sqlparser_clickhouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {})],
Expand Down
6 changes: 6 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -3430,6 +3431,7 @@ fn parse_create_table_as_table() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
});

match verified_stmt(sql1) {
Expand All @@ -3456,6 +3458,7 @@ fn parse_create_table_as_table() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
});

match verified_stmt(sql2) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -7659,6 +7663,7 @@ fn parse_merge() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
}),
alias: Some(TableAlias {
name: Ident {
Expand Down Expand Up @@ -9168,6 +9173,7 @@ fn parse_unload() {
for_clause: None,
order_by: vec![],
settings: None,
format_clause: None,
}),
to: Ident {
value: "s3://...".to_string(),
Expand Down
2 changes: 2 additions & 0 deletions tests/sqlparser_mssql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -550,6 +551,7 @@ fn parse_substring_in_select() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
}),
query
);
Expand Down
15 changes: 15 additions & 0 deletions tests/sqlparser_mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,7 @@ fn parse_escaped_quote_identifiers_with_escape() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
}))
);
}
Expand Down Expand Up @@ -976,6 +977,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
}))
);
}
Expand Down Expand Up @@ -1022,6 +1024,7 @@ fn parse_escaped_backticks_with_escape() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
}))
);
}
Expand Down Expand Up @@ -1068,6 +1071,7 @@ fn parse_escaped_backticks_with_no_escape() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
}))
);
}
Expand Down Expand Up @@ -1273,6 +1277,7 @@ fn parse_simple_insert() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
})),
source
);
Expand Down Expand Up @@ -1316,6 +1321,7 @@ fn parse_ignore_insert() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
})),
source
);
Expand Down Expand Up @@ -1359,6 +1365,7 @@ fn parse_priority_insert() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
})),
source
);
Expand Down Expand Up @@ -1399,6 +1406,7 @@ fn parse_priority_insert() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
})),
source
);
Expand Down Expand Up @@ -1447,6 +1455,7 @@ fn parse_insert_as() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
})),
source
);
Expand Down Expand Up @@ -1507,6 +1516,7 @@ fn parse_insert_as() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
})),
source
);
Expand Down Expand Up @@ -1551,6 +1561,7 @@ fn parse_replace_insert() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
})),
source
);
Expand Down Expand Up @@ -1589,6 +1600,7 @@ fn parse_empty_row_insert() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
})),
source
);
Expand Down Expand Up @@ -1650,6 +1662,7 @@ fn parse_insert_with_on_duplicate_update() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
})),
source
);
Expand Down Expand Up @@ -2294,6 +2307,7 @@ fn parse_substring_in_select() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
}),
query
);
Expand Down Expand Up @@ -2601,6 +2615,7 @@ fn parse_hex_string_introducer() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
}))
)
}
Expand Down
4 changes: 4 additions & 0 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,7 @@ fn parse_copy_to() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
})),
to: true,
target: CopyTarget::File {
Expand Down Expand Up @@ -2426,6 +2427,7 @@ fn parse_array_subquery_expr() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
})),
filter: None,
null_treatment: None,
Expand Down Expand Up @@ -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![],
Expand Down Expand Up @@ -4080,6 +4083,7 @@ fn test_simple_insert_with_quoted_alias() {
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
})),
partitioned: None,
after_columns: vec![],
Expand Down

0 comments on commit 3c4c5c1

Please sign in to comment.