Skip to content

Commit f6b8c3a

Browse files
git-hulklustefaniak
authored andcommitted
Add support of ATTACH/DETACH PARTITION for ClickHouse (apache#1362)
# Conflicts: # src/ast/ddl.rs # src/parser/mod.rs # tests/sqlparser_clickhouse.rs
1 parent 9c97177 commit f6b8c3a

File tree

5 files changed

+167
-3
lines changed

5 files changed

+167
-3
lines changed

src/ast/ddl.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@ pub enum AlterTableOperation {
5858
if_exists: bool,
5959
cascade: bool,
6060
},
61+
/// `ATTACH PART|PARTITION <partition_expr>`
62+
/// Note: this is a ClickHouse-specific operation, please refer to
63+
/// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/pakrtition#attach-partitionpart)
64+
AttachPartition {
65+
// PART is not a short form of PARTITION, it's a separate keyword
66+
// which represents a physical file on disk and partition is a logical entity.
67+
partition: Partition,
68+
},
69+
/// `DETACH PART|PARTITION <partition_expr>`
70+
/// Note: this is a ClickHouse-specific operation, please refer to
71+
/// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/partition#detach-partitionpart)
72+
DetachPartition {
73+
// See `AttachPartition` for more details
74+
partition: Partition,
75+
},
6176
/// `DROP PRIMARY KEY`
6277
///
6378
/// Note: this is a MySQL-specific operation.
@@ -183,6 +198,12 @@ impl fmt::Display for AlterTableOperation {
183198
column_name,
184199
if *cascade { " CASCADE" } else { "" }
185200
),
201+
AlterTableOperation::AttachPartition { partition } => {
202+
write!(f, "ATTACH {partition}")
203+
}
204+
AlterTableOperation::DetachPartition { partition } => {
205+
write!(f, "DETACH {partition}")
206+
}
186207
AlterTableOperation::RenamePartitions {
187208
old_partitions,
188209
new_partitions,
@@ -929,3 +950,50 @@ impl fmt::Display for UserDefinedTypeCompositeAttributeDef {
929950
Ok(())
930951
}
931952
}
953+
954+
/// PARTITION statement used in ALTER TABLE et al. such as in Hive and ClickHouse SQL.
955+
/// For example, ClickHouse's OPTIMIZE TABLE supports syntax like PARTITION ID 'partition_id' and PARTITION expr.
956+
/// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/optimize)
957+
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
958+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
959+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
960+
pub enum Partition {
961+
Identifier(Ident),
962+
Expr(Expr),
963+
/// ClickHouse supports PART expr which represents physical partition in disk.
964+
/// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/partition#attach-partitionpart)
965+
Part(Expr),
966+
Partitions(Vec<Expr>),
967+
}
968+
969+
impl fmt::Display for Partition {
970+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
971+
match self {
972+
Partition::Identifier(id) => write!(f, "PARTITION ID {id}"),
973+
Partition::Expr(expr) => write!(f, "PARTITION {expr}"),
974+
Partition::Part(expr) => write!(f, "PART {expr}"),
975+
Partition::Partitions(partitions) => {
976+
write!(f, "PARTITION ({})", display_comma_separated(partitions))
977+
}
978+
}
979+
}
980+
}
981+
982+
/// DEDUPLICATE statement used in OPTIMIZE TABLE et al. such as in ClickHouse SQL
983+
/// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/optimize)
984+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
985+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
986+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
987+
pub enum Deduplicate {
988+
All,
989+
ByExpression(Expr),
990+
}
991+
992+
impl fmt::Display for Deduplicate {
993+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
994+
match self {
995+
Deduplicate::All => write!(f, "DEDUPLICATE"),
996+
Deduplicate::ByExpression(expr) => write!(f, "DEDUPLICATE BY {expr}"),
997+
}
998+
}
999+
}

src/ast/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ pub use self::data_type::{
3434
pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue};
3535
pub use self::ddl::{
3636
AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnLocation,
37-
ColumnOption, ColumnOptionDef, ConstraintCharacteristics, GeneratedAs, IndexType,
38-
KeyOrIndexDisplay, ProcedureParam, ReferentialAction, TableConstraint,
37+
ColumnOption, ColumnOptionDef, ConstraintCharacteristics, Deduplicate, GeneratedAs, IndexType,
38+
KeyOrIndexDisplay, Partition, ProcedureParam, ReferentialAction, TableConstraint,
3939
UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation,
4040
};
4141
pub use self::operator::{BinaryOperator, UnaryOperator};

src/keywords.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ define_keywords!(
9898
ASYMMETRIC,
9999
AT,
100100
ATOMIC,
101+
ATTACH,
101102
AUTHORIZATION,
102103
AUTO,
103104
AUTOINCREMENT,
@@ -223,6 +224,7 @@ define_keywords!(
223224
DEREF,
224225
DESC,
225226
DESCRIBE,
227+
DETACH,
226228
DETAIL,
227229
DETERMINISTIC,
228230
DIRECTORY,
@@ -481,6 +483,7 @@ define_keywords!(
481483
OWNED,
482484
PARAMETER,
483485
PARQUET,
486+
PART,
484487
PARTITION,
485488
PARTITIONED,
486489
PARTITIONS,

src/parser/mod.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5128,6 +5128,18 @@ impl<'a> Parser<'a> {
51285128
self.prev_token();
51295129
let options = self.parse_options(Keyword::OPTIONS)?;
51305130
AlterTableOperation::SetOptions { options }
5131+
} else if dialect_of!(self is ClickHouseDialect|GenericDialect)
5132+
&& self.parse_keyword(Keyword::ATTACH)
5133+
{
5134+
AlterTableOperation::AttachPartition {
5135+
partition: self.parse_part_or_partition()?,
5136+
}
5137+
} else if dialect_of!(self is ClickHouseDialect|GenericDialect)
5138+
&& self.parse_keyword(Keyword::DETACH)
5139+
{
5140+
AlterTableOperation::DetachPartition {
5141+
partition: self.parse_part_or_partition()?,
5142+
}
51315143
} else {
51325144
return self.expected(
51335145
"ADD, RENAME, PARTITION, SWAP or DROP after ALTER TABLE",
@@ -5137,6 +5149,16 @@ impl<'a> Parser<'a> {
51375149
Ok(operation)
51385150
}
51395151

5152+
fn parse_part_or_partition(&mut self) -> Result<Partition, ParserError> {
5153+
let keyword = self.expect_one_of_keywords(&[Keyword::PART, Keyword::PARTITION])?;
5154+
match keyword {
5155+
Keyword::PART => Ok(Partition::Part(self.parse_expr()?)),
5156+
Keyword::PARTITION => Ok(Partition::Expr(self.parse_expr()?)),
5157+
// unreachable because expect_one_of_keywords used above
5158+
_ => unreachable!(),
5159+
}
5160+
}
5161+
51405162
pub fn parse_alter(&mut self) -> Result<Statement, ParserError> {
51415163
let object_type = self.expect_one_of_keywords(&[
51425164
Keyword::VIEW,

tests/sqlparser_clickhouse.rs

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@
1414
//! Test SQL syntax specific to ClickHouse.
1515
1616
#[cfg(test)]
17-
use pretty_assertions::assert_eq;
17+
use assert_eq;
1818
use sqlparser::ast::Expr::{ArrayIndex, BinaryOp, Identifier};
1919
use sqlparser::ast::Ident;
2020
use sqlparser::ast::SelectItem::UnnamedExpr;
2121
use sqlparser::ast::TableFactor::Table;
2222
use sqlparser::ast::*;
2323
use sqlparser::dialect::{ClickHouseDialect, GenericDialect};
24+
use sqlparser::parser;
2425
use test_utils::*;
2526

2627
#[macro_use]
@@ -441,6 +442,76 @@ fn parse_create_view() {
441442
clickhouse().verified_stmt(r#"CREATE VIEW analytics.runs_audit_ingest_daily (`count` UInt64, `ts` DateTime('UTC')) AS SELECT count(*) AS count, toStartOfDay(ingested_at) AS ts FROM analytics.runs_int_runs GROUP BY ts ORDER BY ts DESC"#);
442443
}
443444

445+
#[test]
446+
fn parse_alter_table_attach_and_detach_partition() {
447+
for operation in &["ATTACH", "DETACH"] {
448+
match clickhouse_and_generic()
449+
.verified_stmt(format!("ALTER TABLE t0 {operation} PARTITION part").as_str())
450+
{
451+
Statement::AlterTable {
452+
name, operations, ..
453+
} => {
454+
assert_eq!("t0", name.to_string());
455+
assert_eq!(
456+
operations[0],
457+
if operation == &"ATTACH" {
458+
AlterTableOperation::AttachPartition {
459+
partition: Partition::Expr(Identifier(Ident::new("part").empty_span())),
460+
}
461+
} else {
462+
AlterTableOperation::DetachPartition {
463+
partition: Partition::Expr(Identifier(Ident::new("part").empty_span())),
464+
}
465+
}
466+
);
467+
}
468+
_ => unreachable!(),
469+
}
470+
471+
match clickhouse_and_generic()
472+
.verified_stmt(format!("ALTER TABLE t1 {operation} PART part").as_str())
473+
{
474+
Statement::AlterTable {
475+
name, operations, ..
476+
} => {
477+
assert_eq!("t1", name.to_string());
478+
assert_eq!(
479+
operations[0],
480+
if operation == &"ATTACH" {
481+
AlterTableOperation::AttachPartition {
482+
partition: Partition::Part(Identifier(Ident::new("part").empty_span())),
483+
}
484+
} else {
485+
AlterTableOperation::DetachPartition {
486+
partition: Partition::Part(Identifier(Ident::new("part").empty_span())),
487+
}
488+
}
489+
);
490+
}
491+
_ => unreachable!(),
492+
}
493+
494+
// negative cases
495+
assert_eq!(
496+
clickhouse_and_generic()
497+
.parse_sql_statements(format!("ALTER TABLE t0 {operation} PARTITION").as_str())
498+
.unwrap_err(),
499+
parser::ParserError::ParserError(format!("Expected an expression:, found: EOF\nNear `ALTER TABLE t0 {operation} PARTITION`").to_string())
500+
);
501+
assert_eq!(
502+
clickhouse_and_generic()
503+
.parse_sql_statements(format!("ALTER TABLE t0 {operation} PART").as_str())
504+
.unwrap_err(),
505+
parser::ParserError::ParserError(
506+
format!(
507+
"Expected an expression:, found: EOF\nNear `ALTER TABLE t0 {operation} PART`"
508+
)
509+
.to_string()
510+
)
511+
);
512+
}
513+
}
514+
444515
#[test]
445516
fn parse_create_materialized_view() {
446517
clickhouse().verified_stmt(

0 commit comments

Comments
 (0)