Skip to content

Commit 097a929

Browse files
Cristhian Lopez VidalCopilot
andcommitted
feat(clickhouse): add ARRAY JOIN, LEFT/INNER ARRAY JOIN support
ClickHouse supports ARRAY JOIN clauses for unnesting arrays inline. This adds JoinOperator variants for ARRAY JOIN, LEFT ARRAY JOIN, and INNER ARRAY JOIN. These joins take a table expression (the array to unnest) rather than a standard table reference, and do not use ON/USING constraints. Also adds Spanned impls for the new variants in spans.rs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1970fc9 commit 097a929

File tree

5 files changed

+141
-0
lines changed

5 files changed

+141
-0
lines changed

src/ast/query.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2755,6 +2755,13 @@ impl fmt::Display for Join {
27552755
self.relation,
27562756
suffix(constraint)
27572757
)),
2758+
JoinOperator::ArrayJoin => f.write_fmt(format_args!("ARRAY JOIN {}", self.relation)),
2759+
JoinOperator::LeftArrayJoin => {
2760+
f.write_fmt(format_args!("LEFT ARRAY JOIN {}", self.relation))
2761+
}
2762+
JoinOperator::InnerArrayJoin => {
2763+
f.write_fmt(format_args!("INNER ARRAY JOIN {}", self.relation))
2764+
}
27582765
}
27592766
}
27602767
}
@@ -2809,6 +2816,14 @@ pub enum JoinOperator {
28092816
///
28102817
/// See <https://dev.mysql.com/doc/refman/8.4/en/join.html>.
28112818
StraightJoin(JoinConstraint),
2819+
/// ClickHouse: `ARRAY JOIN` for unnesting arrays inline.
2820+
///
2821+
/// See <https://clickhouse.com/docs/en/sql-reference/statements/select/array-join>.
2822+
ArrayJoin,
2823+
/// ClickHouse: `LEFT ARRAY JOIN` for unnesting arrays inline (preserves rows with empty arrays).
2824+
LeftArrayJoin,
2825+
/// ClickHouse: `INNER ARRAY JOIN` for unnesting arrays inline (filters rows with empty arrays).
2826+
InnerArrayJoin,
28122827
}
28132828

28142829
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]

src/ast/spans.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2234,6 +2234,9 @@ impl Spanned for JoinOperator {
22342234
JoinOperator::Anti(join_constraint) => join_constraint.span(),
22352235
JoinOperator::Semi(join_constraint) => join_constraint.span(),
22362236
JoinOperator::StraightJoin(join_constraint) => join_constraint.span(),
2237+
JoinOperator::ArrayJoin => Span::empty(),
2238+
JoinOperator::LeftArrayJoin => Span::empty(),
2239+
JoinOperator::InnerArrayJoin => Span::empty(),
22372240
}
22382241
}
22392242
}

src/keywords.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,8 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[
12311231
Keyword::FOR,
12321232
// for MYSQL PARTITION SELECTION
12331233
Keyword::PARTITION,
1234+
// for Clickhouse ARRAY JOIN (ARRAY must not be parsed as a table alias)
1235+
Keyword::ARRAY,
12341236
// for Clickhouse PREWHERE
12351237
Keyword::PREWHERE,
12361238
Keyword::SETTINGS,

src/parser/mod.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15342,6 +15342,33 @@ impl<'a> Parser<'a> {
1534215342
constraint: self.parse_join_constraint(false)?,
1534315343
},
1534415344
}
15345+
} else if dialect_of!(self is ClickHouseDialect | GenericDialect)
15346+
&& self.parse_keywords(&[Keyword::INNER, Keyword::ARRAY, Keyword::JOIN])
15347+
{
15348+
// ClickHouse: INNER ARRAY JOIN
15349+
Join {
15350+
relation: self.parse_table_factor()?,
15351+
global,
15352+
join_operator: JoinOperator::InnerArrayJoin,
15353+
}
15354+
} else if dialect_of!(self is ClickHouseDialect | GenericDialect)
15355+
&& self.parse_keywords(&[Keyword::LEFT, Keyword::ARRAY, Keyword::JOIN])
15356+
{
15357+
// ClickHouse: LEFT ARRAY JOIN
15358+
Join {
15359+
relation: self.parse_table_factor()?,
15360+
global,
15361+
join_operator: JoinOperator::LeftArrayJoin,
15362+
}
15363+
} else if dialect_of!(self is ClickHouseDialect | GenericDialect)
15364+
&& self.parse_keywords(&[Keyword::ARRAY, Keyword::JOIN])
15365+
{
15366+
// ClickHouse: ARRAY JOIN
15367+
Join {
15368+
relation: self.parse_table_factor()?,
15369+
global,
15370+
join_operator: JoinOperator::ArrayJoin,
15371+
}
1534515372
} else {
1534615373
let natural = self.parse_keyword(Keyword::NATURAL);
1534715374
let peek_keyword = if let Token::Word(w) = &self.peek_token_ref().token {

tests/sqlparser_clickhouse.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,43 @@ fn parse_create_table_partition_by_after_order_by() {
253253
"PARTITION BY col1 % 64"
254254
),
255255
);
256+
257+
// PARTITION BY after ORDER BY works with both ClickHouseDialect and GenericDialect
258+
clickhouse_and_generic()
259+
.verified_stmt("CREATE TABLE t (a INT) ENGINE = MergeTree ORDER BY a PARTITION BY a");
260+
261+
// Arithmetic expression in PARTITION BY (roundtrip)
262+
clickhouse_and_generic()
263+
.verified_stmt("CREATE TABLE t (a INT) ENGINE = MergeTree ORDER BY a PARTITION BY a % 64");
264+
265+
// AST: partition_by is populated with the correct expression
266+
match clickhouse_and_generic()
267+
.verified_stmt("CREATE TABLE t (a INT) ENGINE = MergeTree ORDER BY a PARTITION BY a % 64")
268+
{
269+
Statement::CreateTable(CreateTable { partition_by, .. }) => {
270+
assert_eq!(
271+
partition_by,
272+
Some(Box::new(BinaryOp {
273+
left: Box::new(Identifier(Ident::new("a"))),
274+
op: BinaryOperator::Modulo,
275+
right: Box::new(Expr::Value(
276+
Value::Number("64".parse().unwrap(), false).with_empty_span(),
277+
)),
278+
}))
279+
);
280+
}
281+
_ => unreachable!(),
282+
}
283+
284+
// Function call expression in PARTITION BY (ClickHouse-specific function)
285+
clickhouse().verified_stmt(
286+
"CREATE TABLE t (d DATE) ENGINE = MergeTree ORDER BY d PARTITION BY toYYYYMM(d)",
287+
);
288+
289+
// Negative: PARTITION BY with no expression should fail
290+
clickhouse_and_generic()
291+
.parse_sql_statements("CREATE TABLE t (a INT) ENGINE = MergeTree ORDER BY a PARTITION BY")
292+
.expect_err("PARTITION BY with no expression should fail");
256293
}
257294

258295
#[test]
@@ -1751,6 +1788,63 @@ fn test_parse_not_null_in_column_options() {
17511788
);
17521789
}
17531790

1791+
#[test]
1792+
fn parse_array_join() {
1793+
// ARRAY JOIN works with both ClickHouseDialect and GenericDialect (roundtrip)
1794+
clickhouse_and_generic().verified_stmt("SELECT x FROM t ARRAY JOIN arr AS x");
1795+
1796+
// AST: join_operator is the unit variant ArrayJoin (no constraint)
1797+
match clickhouse_and_generic().verified_stmt("SELECT x FROM t ARRAY JOIN arr AS x") {
1798+
Statement::Query(query) => {
1799+
let select = query.body.as_select().unwrap();
1800+
let join = &select.from[0].joins[0];
1801+
assert_eq!(join.join_operator, JoinOperator::ArrayJoin);
1802+
}
1803+
_ => unreachable!(),
1804+
}
1805+
1806+
// Combined: regular JOIN followed by ARRAY JOIN
1807+
clickhouse_and_generic()
1808+
.verified_stmt("SELECT x FROM t JOIN u ON t.id = u.id ARRAY JOIN arr AS x");
1809+
1810+
// Negative: ARRAY JOIN with no table expression should fail
1811+
clickhouse_and_generic()
1812+
.parse_sql_statements("SELECT x FROM t ARRAY JOIN")
1813+
.expect_err("ARRAY JOIN requires a table expression");
1814+
}
1815+
1816+
#[test]
1817+
fn parse_left_array_join() {
1818+
// LEFT ARRAY JOIN preserves rows with empty/null arrays (roundtrip)
1819+
clickhouse_and_generic().verified_stmt("SELECT x FROM t LEFT ARRAY JOIN arr AS x");
1820+
1821+
// AST: join_operator is LeftArrayJoin
1822+
match clickhouse_and_generic().verified_stmt("SELECT x FROM t LEFT ARRAY JOIN arr AS x") {
1823+
Statement::Query(query) => {
1824+
let select = query.body.as_select().unwrap();
1825+
let join = &select.from[0].joins[0];
1826+
assert_eq!(join.join_operator, JoinOperator::LeftArrayJoin);
1827+
}
1828+
_ => unreachable!(),
1829+
}
1830+
}
1831+
1832+
#[test]
1833+
fn parse_inner_array_join() {
1834+
// INNER ARRAY JOIN filters rows with empty/null arrays (roundtrip)
1835+
clickhouse_and_generic().verified_stmt("SELECT x FROM t INNER ARRAY JOIN arr AS x");
1836+
1837+
// AST: join_operator is InnerArrayJoin
1838+
match clickhouse_and_generic().verified_stmt("SELECT x FROM t INNER ARRAY JOIN arr AS x") {
1839+
Statement::Query(query) => {
1840+
let select = query.body.as_select().unwrap();
1841+
let join = &select.from[0].joins[0];
1842+
assert_eq!(join.join_operator, JoinOperator::InnerArrayJoin);
1843+
}
1844+
_ => unreachable!(),
1845+
}
1846+
}
1847+
17541848
fn clickhouse() -> TestedDialects {
17551849
TestedDialects::new(vec![Box::new(ClickHouseDialect {})])
17561850
}

0 commit comments

Comments
 (0)