Skip to content

Commit 3c33ac1

Browse files
author
Aleksei Piianin
authored
ClickHouse: support of create table query with primary key and parametrised table engine (#1289)
1 parent 4b60866 commit 3c33ac1

File tree

6 files changed

+168
-25
lines changed

6 files changed

+168
-25
lines changed

src/ast/dml.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ pub use super::ddl::{ColumnDef, TableConstraint};
2424
use super::{
2525
display_comma_separated, display_separated, Expr, FileFormat, FromTable, HiveDistributionStyle,
2626
HiveFormat, HiveIOFormat, HiveRowFormat, Ident, InsertAliases, MysqlInsertPriority, ObjectName,
27-
OnCommit, OnInsert, OrderByExpr, Query, SelectItem, SqlOption, SqliteOnConflict,
28-
TableWithJoins,
27+
OnCommit, OnInsert, OneOrManyWithParens, OrderByExpr, Query, SelectItem, SqlOption,
28+
SqliteOnConflict, TableEngine, TableWithJoins,
2929
};
3030

3131
/// CREATE INDEX statement.
@@ -73,7 +73,7 @@ pub struct CreateTable {
7373
pub without_rowid: bool,
7474
pub like: Option<ObjectName>,
7575
pub clone: Option<ObjectName>,
76-
pub engine: Option<String>,
76+
pub engine: Option<TableEngine>,
7777
pub comment: Option<String>,
7878
pub auto_increment_offset: Option<u32>,
7979
pub default_charset: Option<String>,
@@ -82,10 +82,13 @@ pub struct CreateTable {
8282
/// ClickHouse "ON CLUSTER" clause:
8383
/// <https://clickhouse.com/docs/en/sql-reference/distributed-ddl/>
8484
pub on_cluster: Option<String>,
85+
/// ClickHouse "PRIMARY KEY " clause.
86+
/// <https://clickhouse.com/docs/en/sql-reference/statements/create/table/>
87+
pub primary_key: Option<Box<Expr>>,
8588
/// ClickHouse "ORDER BY " clause. Note that omitted ORDER BY is different
8689
/// than empty (represented as ()), the latter meaning "no sorting".
8790
/// <https://clickhouse.com/docs/en/sql-reference/statements/create/table/>
88-
pub order_by: Option<Vec<Ident>>,
91+
pub order_by: Option<OneOrManyWithParens<Expr>>,
8992
/// BigQuery: A partition expression for the table.
9093
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#partition_expression>
9194
pub partition_by: Option<Box<Expr>>,
@@ -263,8 +266,11 @@ impl Display for CreateTable {
263266
if let Some(auto_increment_offset) = self.auto_increment_offset {
264267
write!(f, " AUTO_INCREMENT {auto_increment_offset}")?;
265268
}
269+
if let Some(primary_key) = &self.primary_key {
270+
write!(f, " PRIMARY KEY {}", primary_key)?;
271+
}
266272
if let Some(order_by) = &self.order_by {
267-
write!(f, " ORDER BY ({})", display_comma_separated(order_by))?;
273+
write!(f, " ORDER BY {}", order_by)?;
268274
}
269275
if let Some(partition_by) = self.partition_by.as_ref() {
270276
write!(f, " PARTITION BY {partition_by}")?;

src/ast/helpers/stmt_create_table.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use sqlparser_derive::{Visit, VisitMut};
1010
use super::super::dml::CreateTable;
1111
use crate::ast::{
1212
ColumnDef, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, OnCommit,
13-
Query, SqlOption, Statement, TableConstraint,
13+
OneOrManyWithParens, Query, SqlOption, Statement, TableConstraint, TableEngine,
1414
};
1515
use crate::parser::ParserError;
1616

@@ -65,14 +65,15 @@ pub struct CreateTableBuilder {
6565
pub without_rowid: bool,
6666
pub like: Option<ObjectName>,
6767
pub clone: Option<ObjectName>,
68-
pub engine: Option<String>,
68+
pub engine: Option<TableEngine>,
6969
pub comment: Option<String>,
7070
pub auto_increment_offset: Option<u32>,
7171
pub default_charset: Option<String>,
7272
pub collation: Option<String>,
7373
pub on_commit: Option<OnCommit>,
7474
pub on_cluster: Option<String>,
75-
pub order_by: Option<Vec<Ident>>,
75+
pub primary_key: Option<Box<Expr>>,
76+
pub order_by: Option<OneOrManyWithParens<Expr>>,
7677
pub partition_by: Option<Box<Expr>>,
7778
pub cluster_by: Option<Vec<Ident>>,
7879
pub options: Option<Vec<SqlOption>>,
@@ -108,6 +109,7 @@ impl CreateTableBuilder {
108109
collation: None,
109110
on_commit: None,
110111
on_cluster: None,
112+
primary_key: None,
111113
order_by: None,
112114
partition_by: None,
113115
cluster_by: None,
@@ -203,7 +205,7 @@ impl CreateTableBuilder {
203205
self
204206
}
205207

206-
pub fn engine(mut self, engine: Option<String>) -> Self {
208+
pub fn engine(mut self, engine: Option<TableEngine>) -> Self {
207209
self.engine = engine;
208210
self
209211
}
@@ -238,7 +240,12 @@ impl CreateTableBuilder {
238240
self
239241
}
240242

241-
pub fn order_by(mut self, order_by: Option<Vec<Ident>>) -> Self {
243+
pub fn primary_key(mut self, primary_key: Option<Box<Expr>>) -> Self {
244+
self.primary_key = primary_key;
245+
self
246+
}
247+
248+
pub fn order_by(mut self, order_by: Option<OneOrManyWithParens<Expr>>) -> Self {
242249
self.order_by = order_by;
243250
self
244251
}
@@ -291,6 +298,7 @@ impl CreateTableBuilder {
291298
collation: self.collation,
292299
on_commit: self.on_commit,
293300
on_cluster: self.on_cluster,
301+
primary_key: self.primary_key,
294302
order_by: self.order_by,
295303
partition_by: self.partition_by,
296304
cluster_by: self.cluster_by,
@@ -334,6 +342,7 @@ impl TryFrom<Statement> for CreateTableBuilder {
334342
collation,
335343
on_commit,
336344
on_cluster,
345+
primary_key,
337346
order_by,
338347
partition_by,
339348
cluster_by,
@@ -366,6 +375,7 @@ impl TryFrom<Statement> for CreateTableBuilder {
366375
collation,
367376
on_commit,
368377
on_cluster,
378+
primary_key,
369379
order_by,
370380
partition_by,
371381
cluster_by,

src/ast/mod.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6315,6 +6315,29 @@ impl Display for MySQLColumnPosition {
63156315
}
63166316
}
63176317

6318+
/// Engine of DB. Some warehouse has parameters of engine, e.g. [clickhouse]
6319+
///
6320+
/// [clickhouse]: https://clickhouse.com/docs/en/engines/table-engines
6321+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
6322+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
6323+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
6324+
pub struct TableEngine {
6325+
pub name: String,
6326+
pub parameters: Option<Vec<Ident>>,
6327+
}
6328+
6329+
impl Display for TableEngine {
6330+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
6331+
write!(f, "{}", self.name)?;
6332+
6333+
if let Some(parameters) = self.parameters.as_ref() {
6334+
write!(f, "({})", display_comma_separated(parameters))?;
6335+
}
6336+
6337+
Ok(())
6338+
}
6339+
}
6340+
63186341
#[cfg(test)]
63196342
mod tests {
63206343
use super::*;

src/parser/mod.rs

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5262,7 +5262,15 @@ impl<'a> Parser<'a> {
52625262
self.expect_token(&Token::Eq)?;
52635263
let next_token = self.next_token();
52645264
match next_token.token {
5265-
Token::Word(w) => Some(w.value),
5265+
Token::Word(w) => {
5266+
let name = w.value;
5267+
let parameters = if self.peek_token() == Token::LParen {
5268+
Some(self.parse_parenthesized_identifiers()?)
5269+
} else {
5270+
None
5271+
};
5272+
Some(TableEngine { name, parameters })
5273+
}
52665274
_ => self.expected("identifier", next_token)?,
52675275
}
52685276
} else {
@@ -5280,17 +5288,27 @@ impl<'a> Parser<'a> {
52805288
None
52815289
};
52825290

5291+
// ClickHouse supports `PRIMARY KEY`, before `ORDER BY`
5292+
// https://clickhouse.com/docs/en/sql-reference/statements/create/table#primary-key
5293+
let primary_key = if dialect_of!(self is ClickHouseDialect | GenericDialect)
5294+
&& self.parse_keywords(&[Keyword::PRIMARY, Keyword::KEY])
5295+
{
5296+
Some(Box::new(self.parse_expr()?))
5297+
} else {
5298+
None
5299+
};
5300+
52835301
let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) {
52845302
if self.consume_token(&Token::LParen) {
52855303
let columns = if self.peek_token() != Token::RParen {
5286-
self.parse_comma_separated(|p| p.parse_identifier(false))?
5304+
self.parse_comma_separated(|p| p.parse_expr())?
52875305
} else {
52885306
vec![]
52895307
};
52905308
self.expect_token(&Token::RParen)?;
5291-
Some(columns)
5309+
Some(OneOrManyWithParens::Many(columns))
52925310
} else {
5293-
Some(vec![self.parse_identifier(false)?])
5311+
Some(OneOrManyWithParens::One(self.parse_expr()?))
52945312
}
52955313
} else {
52965314
None
@@ -5388,6 +5406,7 @@ impl<'a> Parser<'a> {
53885406
.partition_by(big_query_config.partition_by)
53895407
.cluster_by(big_query_config.cluster_by)
53905408
.options(big_query_config.options)
5409+
.primary_key(primary_key)
53915410
.strict(strict)
53925411
.build())
53935412
}
@@ -9041,7 +9060,7 @@ impl<'a> Parser<'a> {
90419060
let partitions: Vec<Ident> = if dialect_of!(self is MySqlDialect | GenericDialect)
90429061
&& self.parse_keyword(Keyword::PARTITION)
90439062
{
9044-
self.parse_partitions()?
9063+
self.parse_parenthesized_identifiers()?
90459064
} else {
90469065
vec![]
90479066
};
@@ -10969,7 +10988,7 @@ impl<'a> Parser<'a> {
1096910988
})
1097010989
}
1097110990

10972-
fn parse_partitions(&mut self) -> Result<Vec<Ident>, ParserError> {
10991+
fn parse_parenthesized_identifiers(&mut self) -> Result<Vec<Ident>, ParserError> {
1097310992
self.expect_token(&Token::LParen)?;
1097410993
let partitions = self.parse_comma_separated(|p| p.parse_identifier(false))?;
1097510994
self.expect_token(&Token::RParen)?;

tests/sqlparser_clickhouse.rs

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,9 @@ fn parse_delimited_identifiers() {
211211
#[test]
212212
fn parse_create_table() {
213213
clickhouse().verified_stmt(r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY ("x")"#);
214-
clickhouse().one_statement_parses_to(
215-
r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY "x""#,
216-
r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY ("x")"#,
217-
);
214+
clickhouse().verified_stmt(r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY "x""#);
218215
clickhouse().verified_stmt(
219-
r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY ("x") AS SELECT * FROM "t" WHERE true"#,
216+
r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY "x" AS SELECT * FROM "t" WHERE true"#,
220217
);
221218
}
222219

@@ -248,7 +245,7 @@ fn parse_clickhouse_data_types() {
248245
.replace(" Float64", " FLOAT64");
249246

250247
match clickhouse_and_generic().one_statement_parses_to(sql, &canonical_sql) {
251-
Statement::CreateTable { name, columns, .. } => {
248+
Statement::CreateTable(CreateTable { name, columns, .. }) => {
252249
assert_eq!(name, ObjectName(vec!["table".into()]));
253250
assert_eq!(
254251
columns,
@@ -289,7 +286,7 @@ fn parse_create_table_with_nullable() {
289286
let canonical_sql = sql.replace("String", "STRING");
290287

291288
match clickhouse_and_generic().one_statement_parses_to(sql, &canonical_sql) {
292-
Statement::CreateTable { name, columns, .. } => {
289+
Statement::CreateTable(CreateTable { name, columns, .. }) => {
293290
assert_eq!(name, ObjectName(vec!["table".into()]));
294291
assert_eq!(
295292
columns,
@@ -338,7 +335,7 @@ fn parse_create_table_with_nested_data_types() {
338335
);
339336

340337
match clickhouse().one_statement_parses_to(sql, "") {
341-
Statement::CreateTable { name, columns, .. } => {
338+
Statement::CreateTable(CreateTable { name, columns, .. }) => {
342339
assert_eq!(name, ObjectName(vec!["table".into()]));
343340
assert_eq!(
344341
columns,
@@ -410,6 +407,88 @@ fn parse_create_table_with_nested_data_types() {
410407
}
411408
}
412409

410+
#[test]
411+
fn parse_create_table_with_primary_key() {
412+
match clickhouse_and_generic().verified_stmt(concat!(
413+
r#"CREATE TABLE db.table (`i` INT, `k` INT)"#,
414+
" ENGINE=SharedMergeTree('/clickhouse/tables/{uuid}/{shard}', '{replica}')",
415+
" PRIMARY KEY tuple(i)",
416+
" ORDER BY tuple(i)",
417+
)) {
418+
Statement::CreateTable(CreateTable {
419+
name,
420+
columns,
421+
engine,
422+
primary_key,
423+
order_by,
424+
..
425+
}) => {
426+
assert_eq!(name.to_string(), "db.table");
427+
assert_eq!(
428+
vec![
429+
ColumnDef {
430+
name: Ident::with_quote('`', "i"),
431+
data_type: DataType::Int(None),
432+
collation: None,
433+
options: vec![],
434+
},
435+
ColumnDef {
436+
name: Ident::with_quote('`', "k"),
437+
data_type: DataType::Int(None),
438+
collation: None,
439+
options: vec![],
440+
},
441+
],
442+
columns
443+
);
444+
assert_eq!(
445+
engine,
446+
Some(TableEngine {
447+
name: "SharedMergeTree".to_string(),
448+
parameters: Some(vec![
449+
Ident::with_quote('\'', "/clickhouse/tables/{uuid}/{shard}"),
450+
Ident::with_quote('\'', "{replica}"),
451+
]),
452+
})
453+
);
454+
fn assert_function(actual: &Function, name: &str, arg: &str) -> bool {
455+
assert_eq!(actual.name, ObjectName(vec![Ident::new(name)]));
456+
assert_eq!(
457+
actual.args,
458+
FunctionArguments::List(FunctionArgumentList {
459+
args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Identifier(
460+
Ident::new(arg)
461+
)),)],
462+
duplicate_treatment: None,
463+
clauses: vec![],
464+
})
465+
);
466+
true
467+
}
468+
match primary_key.unwrap().as_ref() {
469+
Expr::Function(primary_key) => {
470+
assert!(assert_function(primary_key, "tuple", "i"));
471+
}
472+
_ => panic!("unexpected primary key type"),
473+
}
474+
match order_by {
475+
Some(OneOrManyWithParens::One(Expr::Function(order_by))) => {
476+
assert!(assert_function(&order_by, "tuple", "i"));
477+
}
478+
_ => panic!("unexpected order by type"),
479+
};
480+
}
481+
_ => unreachable!(),
482+
}
483+
484+
clickhouse_and_generic()
485+
.parse_sql_statements(concat!(
486+
r#"CREATE TABLE db.table (`i` Int, `k` Int)"#,
487+
" ORDER BY tuple(i), tuple(k)",
488+
))
489+
.expect_err("ORDER BY supports one expression with tuple");
490+
}
491+
413492
#[test]
414493
fn parse_create_view_with_fields_data_types() {
415494
match clickhouse().verified_stmt(r#"CREATE VIEW v (i "int", f "String") AS SELECT * FROM t"#) {

tests/sqlparser_mysql.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -773,7 +773,13 @@ fn parse_create_table_engine_default_charset() {
773773
},],
774774
columns
775775
);
776-
assert_eq!(engine, Some("InnoDB".to_string()));
776+
assert_eq!(
777+
engine,
778+
Some(TableEngine {
779+
name: "InnoDB".to_string(),
780+
parameters: None
781+
})
782+
);
777783
assert_eq!(default_charset, Some("utf8mb3".to_string()));
778784
}
779785
_ => unreachable!(),

0 commit comments

Comments
 (0)