Skip to content

Commit 3f26e3b

Browse files
git-hulkayman-sigma
authored andcommitted
Support ADD PROJECTION syntax for ClickHouse (apache#1390)
1 parent eef0959 commit 3f26e3b

File tree

6 files changed

+231
-65
lines changed

6 files changed

+231
-65
lines changed

src/ast/ddl.rs

+21-1
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

+1-1
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

+44-10
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)]
@@ -1729,6 +1747,22 @@ pub struct OrderBy {
17291747
pub interpolate: Option<Interpolate>,
17301748
}
17311749

1750+
impl fmt::Display for OrderBy {
1751+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1752+
write!(f, "ORDER BY")?;
1753+
if !self.exprs.is_empty() {
1754+
write!(f, " {}", display_comma_separated(&self.exprs))?;
1755+
}
1756+
if let Some(ref interpolate) = self.interpolate {
1757+
match &interpolate.exprs {
1758+
Some(exprs) => write!(f, " INTERPOLATE ({})", display_comma_separated(exprs))?,
1759+
None => write!(f, " INTERPOLATE")?,
1760+
}
1761+
}
1762+
Ok(())
1763+
}
1764+
}
1765+
17321766
/// An `ORDER BY` expression
17331767
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
17341768
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]

src/keywords.rs

+1
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

+92-53
Original file line numberDiff line numberDiff line change
@@ -6438,10 +6438,38 @@ impl<'a> Parser<'a> {
64386438
Ok(Partition::Partitions(partitions))
64396439
}
64406440

6441+
pub fn parse_projection_select(&mut self) -> Result<ProjectionSelect, ParserError> {
6442+
self.expect_token(&Token::LParen)?;
6443+
self.expect_keyword(Keyword::SELECT)?;
6444+
let projection = self.parse_projection()?;
6445+
let group_by = self.parse_optional_group_by()?;
6446+
let order_by = self.parse_optional_order_by()?;
6447+
self.expect_token(&Token::RParen)?;
6448+
Ok(ProjectionSelect {
6449+
projection,
6450+
group_by,
6451+
order_by,
6452+
})
6453+
}
6454+
pub fn parse_alter_table_add_projection(&mut self) -> Result<AlterTableOperation, ParserError> {
6455+
let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
6456+
let name = self.parse_identifier(false)?;
6457+
let query = self.parse_projection_select()?;
6458+
Ok(AlterTableOperation::AddProjection {
6459+
if_not_exists,
6460+
name,
6461+
select: query,
6462+
})
6463+
}
6464+
64416465
pub fn parse_alter_table_operation(&mut self) -> Result<AlterTableOperation, ParserError> {
64426466
let operation = if self.parse_keyword(Keyword::ADD) {
64436467
if let Some(constraint) = self.parse_optional_table_constraint()? {
64446468
AlterTableOperation::AddConstraint(constraint)
6469+
} else if dialect_of!(self is ClickHouseDialect|GenericDialect)
6470+
&& self.parse_keyword(Keyword::PROJECTION)
6471+
{
6472+
return self.parse_alter_table_add_projection();
64456473
} else {
64466474
let if_not_exists =
64476475
self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
@@ -7686,6 +7714,66 @@ impl<'a> Parser<'a> {
76867714
}
76877715
}
76887716

7717+
pub fn parse_optional_group_by(&mut self) -> Result<Option<GroupByExpr>, ParserError> {
7718+
if self.parse_keywords(&[Keyword::GROUP, Keyword::BY]) {
7719+
let expressions = if self.parse_keyword(Keyword::ALL) {
7720+
None
7721+
} else {
7722+
Some(self.parse_comma_separated(Parser::parse_group_by_expr)?)
7723+
};
7724+
7725+
let mut modifiers = vec![];
7726+
if dialect_of!(self is ClickHouseDialect | GenericDialect) {
7727+
loop {
7728+
if !self.parse_keyword(Keyword::WITH) {
7729+
break;
7730+
}
7731+
let keyword = self.expect_one_of_keywords(&[
7732+
Keyword::ROLLUP,
7733+
Keyword::CUBE,
7734+
Keyword::TOTALS,
7735+
])?;
7736+
modifiers.push(match keyword {
7737+
Keyword::ROLLUP => GroupByWithModifier::Rollup,
7738+
Keyword::CUBE => GroupByWithModifier::Cube,
7739+
Keyword::TOTALS => GroupByWithModifier::Totals,
7740+
_ => {
7741+
return parser_err!(
7742+
"BUG: expected to match GroupBy modifier keyword",
7743+
self.peek_token().location
7744+
)
7745+
}
7746+
});
7747+
}
7748+
}
7749+
let group_by = match expressions {
7750+
None => GroupByExpr::All(modifiers),
7751+
Some(exprs) => GroupByExpr::Expressions(exprs, modifiers),
7752+
};
7753+
Ok(Some(group_by))
7754+
} else {
7755+
Ok(None)
7756+
}
7757+
}
7758+
7759+
pub fn parse_optional_order_by(&mut self) -> Result<Option<OrderBy>, ParserError> {
7760+
if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) {
7761+
let order_by_exprs = self.parse_comma_separated(Parser::parse_order_by_expr)?;
7762+
let interpolate = if dialect_of!(self is ClickHouseDialect | GenericDialect) {
7763+
self.parse_interpolations()?
7764+
} else {
7765+
None
7766+
};
7767+
7768+
Ok(Some(OrderBy {
7769+
exprs: order_by_exprs,
7770+
interpolate,
7771+
}))
7772+
} else {
7773+
Ok(None)
7774+
}
7775+
}
7776+
76897777
/// Parse a possibly qualified, possibly quoted identifier, e.g.
76907778
/// `foo` or `myschema."table"
76917779
///
@@ -8278,21 +8366,7 @@ impl<'a> Parser<'a> {
82788366
} else {
82798367
let body = self.parse_boxed_query_body(self.dialect.prec_unknown())?;
82808368

8281-
let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) {
8282-
let order_by_exprs = self.parse_comma_separated(Parser::parse_order_by_expr)?;
8283-
let interpolate = if dialect_of!(self is ClickHouseDialect | GenericDialect) {
8284-
self.parse_interpolations()?
8285-
} else {
8286-
None
8287-
};
8288-
8289-
Some(OrderBy {
8290-
exprs: order_by_exprs,
8291-
interpolate,
8292-
})
8293-
} else {
8294-
None
8295-
};
8369+
let order_by = self.parse_optional_order_by()?;
82968370

82978371
let mut limit = None;
82988372
let mut offset = None;
@@ -8760,44 +8834,9 @@ impl<'a> Parser<'a> {
87608834
None
87618835
};
87628836

8763-
let group_by = if self.parse_keywords(&[Keyword::GROUP, Keyword::BY]) {
8764-
let expressions = if self.parse_keyword(Keyword::ALL) {
8765-
None
8766-
} else {
8767-
Some(self.parse_comma_separated(Parser::parse_group_by_expr)?)
8768-
};
8769-
8770-
let mut modifiers = vec![];
8771-
if dialect_of!(self is ClickHouseDialect | GenericDialect) {
8772-
loop {
8773-
if !self.parse_keyword(Keyword::WITH) {
8774-
break;
8775-
}
8776-
let keyword = self.expect_one_of_keywords(&[
8777-
Keyword::ROLLUP,
8778-
Keyword::CUBE,
8779-
Keyword::TOTALS,
8780-
])?;
8781-
modifiers.push(match keyword {
8782-
Keyword::ROLLUP => GroupByWithModifier::Rollup,
8783-
Keyword::CUBE => GroupByWithModifier::Cube,
8784-
Keyword::TOTALS => GroupByWithModifier::Totals,
8785-
_ => {
8786-
return parser_err!(
8787-
"BUG: expected to match GroupBy modifier keyword",
8788-
self.peek_token().location
8789-
)
8790-
}
8791-
});
8792-
}
8793-
}
8794-
match expressions {
8795-
None => GroupByExpr::All(modifiers),
8796-
Some(exprs) => GroupByExpr::Expressions(exprs, modifiers),
8797-
}
8798-
} else {
8799-
GroupByExpr::Expressions(vec![], vec![])
8800-
};
8837+
let group_by = self
8838+
.parse_optional_group_by()?
8839+
.unwrap_or_else(|| GroupByExpr::Expressions(vec![], vec![]));
88018840

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

tests/sqlparser_clickhouse.rs

+72
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)