Skip to content

Commit 19e694a

Browse files
authored
Support ADD PROJECTION syntax for ClickHouse (#1390)
1 parent 11a6e6f commit 19e694a

File tree

6 files changed

+231
-65
lines changed

6 files changed

+231
-65
lines changed

src/ast/ddl.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use sqlparser_derive::{Visit, VisitMut};
2626
use crate::ast::value::escape_single_quote_string;
2727
use crate::ast::{
2828
display_comma_separated, display_separated, DataType, Expr, Ident, MySQLColumnPosition,
29-
ObjectName, SequenceOptions, SqlOption,
29+
ObjectName, ProjectionSelect, SequenceOptions, SqlOption,
3030
};
3131
use crate::tokenizer::Token;
3232

@@ -48,6 +48,15 @@ pub enum AlterTableOperation {
4848
/// MySQL `ALTER TABLE` only [FIRST | AFTER column_name]
4949
column_position: Option<MySQLColumnPosition>,
5050
},
51+
/// `ADD PROJECTION [IF NOT EXISTS] name ( SELECT <COLUMN LIST EXPR> [GROUP BY] [ORDER BY])`
52+
///
53+
/// Note: this is a ClickHouse-specific operation.
54+
/// Please refer to [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/projection#add-projection)
55+
AddProjection {
56+
if_not_exists: bool,
57+
name: Ident,
58+
select: ProjectionSelect,
59+
},
5160
/// `DISABLE ROW LEVEL SECURITY`
5261
///
5362
/// Note: this is a PostgreSQL-specific operation.
@@ -255,6 +264,17 @@ impl fmt::Display for AlterTableOperation {
255264

256265
Ok(())
257266
}
267+
AlterTableOperation::AddProjection {
268+
if_not_exists,
269+
name,
270+
select: query,
271+
} => {
272+
write!(f, "ADD PROJECTION")?;
273+
if *if_not_exists {
274+
write!(f, " IF NOT EXISTS")?;
275+
}
276+
write!(f, " {} ({})", name, query)
277+
}
258278
AlterTableOperation::AlterColumn { column_name, op } => {
259279
write!(f, "ALTER COLUMN {column_name} {op}")
260280
}

src/ast/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ pub use self::query::{
4848
InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonTableColumn,
4949
JsonTableColumnErrorHandling, LateralView, LockClause, LockType, MatchRecognizePattern,
5050
MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset,
51-
OffsetRows, OrderBy, OrderByExpr, PivotValueSource, Query, RenameSelectItem,
51+
OffsetRows, OrderBy, OrderByExpr, PivotValueSource, ProjectionSelect, Query, RenameSelectItem,
5252
RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select,
5353
SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table,
5454
TableAlias, TableFactor, TableFunctionArgs, TableVersion, TableWithJoins, Top, TopQuantity,

src/ast/query.rs

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,7 @@ impl fmt::Display for Query {
6868
}
6969
write!(f, "{}", self.body)?;
7070
if let Some(ref order_by) = self.order_by {
71-
write!(f, " ORDER BY")?;
72-
if !order_by.exprs.is_empty() {
73-
write!(f, " {}", display_comma_separated(&order_by.exprs))?;
74-
}
75-
if let Some(ref interpolate) = order_by.interpolate {
76-
match &interpolate.exprs {
77-
Some(exprs) => write!(f, " INTERPOLATE ({})", display_comma_separated(exprs))?,
78-
None => write!(f, " INTERPOLATE")?,
79-
}
80-
}
71+
write!(f, " {order_by}")?;
8172
}
8273
if let Some(ref limit) = self.limit {
8374
write!(f, " LIMIT {limit}")?;
@@ -107,6 +98,33 @@ impl fmt::Display for Query {
10798
}
10899
}
109100

101+
/// Query syntax for ClickHouse ADD PROJECTION statement.
102+
/// Its syntax is similar to SELECT statement, but it is used to add a new projection to a table.
103+
/// Syntax is `SELECT <COLUMN LIST EXPR> [GROUP BY] [ORDER BY]`
104+
///
105+
/// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/projection#add-projection)
106+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
107+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
108+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
109+
pub struct ProjectionSelect {
110+
pub projection: Vec<SelectItem>,
111+
pub order_by: Option<OrderBy>,
112+
pub group_by: Option<GroupByExpr>,
113+
}
114+
115+
impl fmt::Display for ProjectionSelect {
116+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
117+
write!(f, "SELECT {}", display_comma_separated(&self.projection))?;
118+
if let Some(ref group_by) = self.group_by {
119+
write!(f, " {group_by}")?;
120+
}
121+
if let Some(ref order_by) = self.order_by {
122+
write!(f, " {order_by}")?;
123+
}
124+
Ok(())
125+
}
126+
}
127+
110128
/// A node in a tree, representing a "query body" expression, roughly:
111129
/// `SELECT ... [ {UNION|EXCEPT|INTERSECT} SELECT ...]`
112130
#[allow(clippy::large_enum_variant)]
@@ -1717,6 +1735,22 @@ pub struct OrderBy {
17171735
pub interpolate: Option<Interpolate>,
17181736
}
17191737

1738+
impl fmt::Display for OrderBy {
1739+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1740+
write!(f, "ORDER BY")?;
1741+
if !self.exprs.is_empty() {
1742+
write!(f, " {}", display_comma_separated(&self.exprs))?;
1743+
}
1744+
if let Some(ref interpolate) = self.interpolate {
1745+
match &interpolate.exprs {
1746+
Some(exprs) => write!(f, " INTERPOLATE ({})", display_comma_separated(exprs))?,
1747+
None => write!(f, " INTERPOLATE")?,
1748+
}
1749+
}
1750+
Ok(())
1751+
}
1752+
}
1753+
17201754
/// An `ORDER BY` expression
17211755
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
17221756
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]

src/keywords.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,7 @@ define_keywords!(
576576
PRIVILEGES,
577577
PROCEDURE,
578578
PROGRAM,
579+
PROJECTION,
579580
PURGE,
580581
QUALIFY,
581582
QUARTER,

src/parser/mod.rs

Lines changed: 92 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6424,10 +6424,38 @@ impl<'a> Parser<'a> {
64246424
Ok(Partition::Partitions(partitions))
64256425
}
64266426

6427+
pub fn parse_projection_select(&mut self) -> Result<ProjectionSelect, ParserError> {
6428+
self.expect_token(&Token::LParen)?;
6429+
self.expect_keyword(Keyword::SELECT)?;
6430+
let projection = self.parse_projection()?;
6431+
let group_by = self.parse_optional_group_by()?;
6432+
let order_by = self.parse_optional_order_by()?;
6433+
self.expect_token(&Token::RParen)?;
6434+
Ok(ProjectionSelect {
6435+
projection,
6436+
group_by,
6437+
order_by,
6438+
})
6439+
}
6440+
pub fn parse_alter_table_add_projection(&mut self) -> Result<AlterTableOperation, ParserError> {
6441+
let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
6442+
let name = self.parse_identifier(false)?;
6443+
let query = self.parse_projection_select()?;
6444+
Ok(AlterTableOperation::AddProjection {
6445+
if_not_exists,
6446+
name,
6447+
select: query,
6448+
})
6449+
}
6450+
64276451
pub fn parse_alter_table_operation(&mut self) -> Result<AlterTableOperation, ParserError> {
64286452
let operation = if self.parse_keyword(Keyword::ADD) {
64296453
if let Some(constraint) = self.parse_optional_table_constraint()? {
64306454
AlterTableOperation::AddConstraint(constraint)
6455+
} else if dialect_of!(self is ClickHouseDialect|GenericDialect)
6456+
&& self.parse_keyword(Keyword::PROJECTION)
6457+
{
6458+
return self.parse_alter_table_add_projection();
64316459
} else {
64326460
let if_not_exists =
64336461
self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
@@ -7672,6 +7700,66 @@ impl<'a> Parser<'a> {
76727700
}
76737701
}
76747702

7703+
pub fn parse_optional_group_by(&mut self) -> Result<Option<GroupByExpr>, ParserError> {
7704+
if self.parse_keywords(&[Keyword::GROUP, Keyword::BY]) {
7705+
let expressions = if self.parse_keyword(Keyword::ALL) {
7706+
None
7707+
} else {
7708+
Some(self.parse_comma_separated(Parser::parse_group_by_expr)?)
7709+
};
7710+
7711+
let mut modifiers = vec![];
7712+
if dialect_of!(self is ClickHouseDialect | GenericDialect) {
7713+
loop {
7714+
if !self.parse_keyword(Keyword::WITH) {
7715+
break;
7716+
}
7717+
let keyword = self.expect_one_of_keywords(&[
7718+
Keyword::ROLLUP,
7719+
Keyword::CUBE,
7720+
Keyword::TOTALS,
7721+
])?;
7722+
modifiers.push(match keyword {
7723+
Keyword::ROLLUP => GroupByWithModifier::Rollup,
7724+
Keyword::CUBE => GroupByWithModifier::Cube,
7725+
Keyword::TOTALS => GroupByWithModifier::Totals,
7726+
_ => {
7727+
return parser_err!(
7728+
"BUG: expected to match GroupBy modifier keyword",
7729+
self.peek_token().location
7730+
)
7731+
}
7732+
});
7733+
}
7734+
}
7735+
let group_by = match expressions {
7736+
None => GroupByExpr::All(modifiers),
7737+
Some(exprs) => GroupByExpr::Expressions(exprs, modifiers),
7738+
};
7739+
Ok(Some(group_by))
7740+
} else {
7741+
Ok(None)
7742+
}
7743+
}
7744+
7745+
pub fn parse_optional_order_by(&mut self) -> Result<Option<OrderBy>, ParserError> {
7746+
if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) {
7747+
let order_by_exprs = self.parse_comma_separated(Parser::parse_order_by_expr)?;
7748+
let interpolate = if dialect_of!(self is ClickHouseDialect | GenericDialect) {
7749+
self.parse_interpolations()?
7750+
} else {
7751+
None
7752+
};
7753+
7754+
Ok(Some(OrderBy {
7755+
exprs: order_by_exprs,
7756+
interpolate,
7757+
}))
7758+
} else {
7759+
Ok(None)
7760+
}
7761+
}
7762+
76757763
/// Parse a possibly qualified, possibly quoted identifier, e.g.
76767764
/// `foo` or `myschema."table"
76777765
///
@@ -8264,21 +8352,7 @@ impl<'a> Parser<'a> {
82648352
} else {
82658353
let body = self.parse_boxed_query_body(self.dialect.prec_unknown())?;
82668354

8267-
let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) {
8268-
let order_by_exprs = self.parse_comma_separated(Parser::parse_order_by_expr)?;
8269-
let interpolate = if dialect_of!(self is ClickHouseDialect | GenericDialect) {
8270-
self.parse_interpolations()?
8271-
} else {
8272-
None
8273-
};
8274-
8275-
Some(OrderBy {
8276-
exprs: order_by_exprs,
8277-
interpolate,
8278-
})
8279-
} else {
8280-
None
8281-
};
8355+
let order_by = self.parse_optional_order_by()?;
82828356

82838357
let mut limit = None;
82848358
let mut offset = None;
@@ -8746,44 +8820,9 @@ impl<'a> Parser<'a> {
87468820
None
87478821
};
87488822

8749-
let group_by = if self.parse_keywords(&[Keyword::GROUP, Keyword::BY]) {
8750-
let expressions = if self.parse_keyword(Keyword::ALL) {
8751-
None
8752-
} else {
8753-
Some(self.parse_comma_separated(Parser::parse_group_by_expr)?)
8754-
};
8755-
8756-
let mut modifiers = vec![];
8757-
if dialect_of!(self is ClickHouseDialect | GenericDialect) {
8758-
loop {
8759-
if !self.parse_keyword(Keyword::WITH) {
8760-
break;
8761-
}
8762-
let keyword = self.expect_one_of_keywords(&[
8763-
Keyword::ROLLUP,
8764-
Keyword::CUBE,
8765-
Keyword::TOTALS,
8766-
])?;
8767-
modifiers.push(match keyword {
8768-
Keyword::ROLLUP => GroupByWithModifier::Rollup,
8769-
Keyword::CUBE => GroupByWithModifier::Cube,
8770-
Keyword::TOTALS => GroupByWithModifier::Totals,
8771-
_ => {
8772-
return parser_err!(
8773-
"BUG: expected to match GroupBy modifier keyword",
8774-
self.peek_token().location
8775-
)
8776-
}
8777-
});
8778-
}
8779-
}
8780-
match expressions {
8781-
None => GroupByExpr::All(modifiers),
8782-
Some(exprs) => GroupByExpr::Expressions(exprs, modifiers),
8783-
}
8784-
} else {
8785-
GroupByExpr::Expressions(vec![], vec![])
8786-
};
8823+
let group_by = self
8824+
.parse_optional_group_by()?
8825+
.unwrap_or_else(|| GroupByExpr::Expressions(vec![], vec![]));
87878826

87888827
let cluster_by = if self.parse_keywords(&[Keyword::CLUSTER, Keyword::BY]) {
87898828
self.parse_comma_separated(Parser::parse_expr)?

tests/sqlparser_clickhouse.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,78 @@ fn parse_alter_table_attach_and_detach_partition() {
287287
}
288288
}
289289

290+
#[test]
291+
fn parse_alter_table_add_projection() {
292+
match clickhouse_and_generic().verified_stmt(concat!(
293+
"ALTER TABLE t0 ADD PROJECTION IF NOT EXISTS my_name",
294+
" (SELECT a, b GROUP BY a ORDER BY b)",
295+
)) {
296+
Statement::AlterTable {
297+
name, operations, ..
298+
} => {
299+
assert_eq!(name, ObjectName(vec!["t0".into()]));
300+
assert_eq!(1, operations.len());
301+
assert_eq!(
302+
operations[0],
303+
AlterTableOperation::AddProjection {
304+
if_not_exists: true,
305+
name: "my_name".into(),
306+
select: ProjectionSelect {
307+
projection: vec![
308+
UnnamedExpr(Identifier(Ident::new("a"))),
309+
UnnamedExpr(Identifier(Ident::new("b"))),
310+
],
311+
group_by: Some(GroupByExpr::Expressions(
312+
vec![Identifier(Ident::new("a"))],
313+
vec![]
314+
)),
315+
order_by: Some(OrderBy {
316+
exprs: vec![OrderByExpr {
317+
expr: Identifier(Ident::new("b")),
318+
asc: None,
319+
nulls_first: None,
320+
with_fill: None,
321+
}],
322+
interpolate: None,
323+
}),
324+
}
325+
}
326+
)
327+
}
328+
_ => unreachable!(),
329+
}
330+
331+
// leave out IF NOT EXISTS is allowed
332+
clickhouse_and_generic()
333+
.verified_stmt("ALTER TABLE t0 ADD PROJECTION my_name (SELECT a, b GROUP BY a ORDER BY b)");
334+
// leave out GROUP BY is allowed
335+
clickhouse_and_generic()
336+
.verified_stmt("ALTER TABLE t0 ADD PROJECTION my_name (SELECT a, b ORDER BY b)");
337+
// leave out ORDER BY is allowed
338+
clickhouse_and_generic()
339+
.verified_stmt("ALTER TABLE t0 ADD PROJECTION my_name (SELECT a, b GROUP BY a)");
340+
341+
// missing select query is not allowed
342+
assert_eq!(
343+
clickhouse_and_generic()
344+
.parse_sql_statements("ALTER TABLE t0 ADD PROJECTION my_name")
345+
.unwrap_err(),
346+
ParserError("Expected: (, found: EOF".to_string())
347+
);
348+
assert_eq!(
349+
clickhouse_and_generic()
350+
.parse_sql_statements("ALTER TABLE t0 ADD PROJECTION my_name ()")
351+
.unwrap_err(),
352+
ParserError("Expected: SELECT, found: )".to_string())
353+
);
354+
assert_eq!(
355+
clickhouse_and_generic()
356+
.parse_sql_statements("ALTER TABLE t0 ADD PROJECTION my_name (SELECT)")
357+
.unwrap_err(),
358+
ParserError("Expected: an expression:, found: )".to_string())
359+
);
360+
}
361+
290362
#[test]
291363
fn parse_optimize_table() {
292364
clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE t0");

0 commit comments

Comments
 (0)