Skip to content

Commit 5c86877

Browse files
authored
feat(parser): add support for ClickHouse [LEFT] ARRAY JOIN syntax (#2482)
* feat(Join): add array join support to Join class * feat(parser): add support for ClickHouse ARRAY JOIN syntax and deparsing * fix(parser): correct array literal parsing in JSqlParserCC.jjt * style: apply spotless code formatting
1 parent ad69ecc commit 5c86877

4 files changed

Lines changed: 77 additions & 17 deletions

File tree

src/main/java/net/sf/jsqlparser/statement/select/Join.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public class Join extends ASTNodeAccessImpl {
4040
private boolean straight = false;
4141
private boolean apply = false;
4242
private boolean fetch = false;
43+
private boolean array = false;
4344
private FromItem fromItem;
4445
private KSQLJoinWindow joinWindow;
4546

@@ -73,7 +74,10 @@ public boolean isInnerJoin() {
7374
|| cross
7475

7576
/* Natural Join */
76-
|| natural);
77+
|| natural
78+
79+
/* Array Join */
80+
|| array);
7781
}
7882

7983
/**
@@ -337,6 +341,19 @@ public Join withCross(boolean cross) {
337341
return this;
338342
}
339343

344+
public boolean isArray() {
345+
return array;
346+
}
347+
348+
public void setArray(boolean array) {
349+
this.array = array;
350+
}
351+
352+
public Join withArray(boolean array) {
353+
this.setArray(array);
354+
return this;
355+
}
356+
340357
/**
341358
* Returns the right item of the join
342359
*/
@@ -489,6 +506,10 @@ public String toString() {
489506
builder.append("SEMI ");
490507
}
491508

509+
if (isArray()) {
510+
builder.append("ARRAY ");
511+
}
512+
492513
if (isStraight()) {
493514
builder.append("STRAIGHT_JOIN ");
494515
} else if (isApply()) {

src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,10 @@ public void deparseJoin(Join join) {
683683
builder.append(" SEMI");
684684
}
685685

686+
if (join.isArray()) {
687+
builder.append(" ARRAY");
688+
}
689+
686690
if (join.isStraight()) {
687691
builder.append(" STRAIGHT_JOIN ");
688692
} else if (join.isApply()) {

src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -829,9 +829,11 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
829829
case K_CONNECT: return nextKind != K_BY;
830830
case K_START: return nextKind != K_WITH;
831831
case K_LEFT: return nextKind != K_JOIN && nextKind != K_OUTER
832-
&& nextKind != K_SEMI;
832+
&& nextKind != K_SEMI && nextKind != K_ARRAY_LITERAL;
833833
case K_RIGHT: return nextKind != K_JOIN && nextKind != K_OUTER
834-
&& nextKind != K_SEMI;
834+
&& nextKind != K_SEMI && nextKind != K_ARRAY_LITERAL;
835+
case K_ARRAY_LITERAL:
836+
return nextKind != K_JOIN;
835837
case K_ALL: return nextKind != K_JOIN;
836838
case K_ANY: return nextKind != OPENING_BRACKET;
837839
case K_SOME: return nextKind != OPENING_BRACKET;
@@ -993,29 +995,17 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
993995
// String-literal alias: SELECT col 'myAlias'
994996
if (kind == S_CHAR_LITERAL) return true;
995997

996-
// Base identifier tokens
997-
if (kind == S_IDENTIFIER || kind == S_QUOTED_IDENTIFIER
998-
|| kind == DATA_TYPE || kind == K_DATETIMELITERAL
999-
|| kind == K_DATE_LITERAL) {
1000-
return true;
1001-
}
1002-
1003998
// OPTION (...) introduces a query hint clause, not an alias
1004999
if (kind == K_OPTION && getToken(2).kind == OPENING_BRACKET) {
10051000
return false;
10061001
}
10071002

1008-
// Non-reserved keywords
1009-
if (kind >= MIN_NON_RESERVED_WORD && kind <= MAX_NON_RESERVED_WORD) {
1010-
return true;
1011-
}
1012-
10131003
// For reserved keywords in alias position, skip the structural-
10141004
// keyword whitelist and go directly to follower disambiguation.
10151005
// Only check keywords that are actually in RelObjectName's token
10161006
// alternatives — don't fire for brackets, operators, literals, etc.
10171007
switch (kind) {
1018-
case K_ALL: case K_ANY: case K_CASEWHEN: case K_CONNECT:
1008+
case K_ALL: case K_ARRAY_LITERAL: case K_ANY: case K_CASEWHEN: case K_CONNECT:
10191009
case K_CREATE: case K_DEFAULT:
10201010
case K_GLOBAL: case K_GROUP: case K_GROUPING: case K_IF:
10211011
case K_IIF: case K_IGNORE: case K_IN: case K_INTERVAL:
@@ -1026,8 +1016,22 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
10261016
case K_TOP: case K_VALUE: case K_VALUES:
10271017
return isReservedKeywordSafeByFollower();
10281018
default:
1029-
return false;
1019+
break;
10301020
}
1021+
1022+
// Base identifier tokens
1023+
if (kind == S_IDENTIFIER || kind == S_QUOTED_IDENTIFIER
1024+
|| kind == DATA_TYPE || kind == K_DATETIMELITERAL
1025+
|| kind == K_DATE_LITERAL) {
1026+
return true;
1027+
}
1028+
1029+
// Non-reserved keywords
1030+
if (kind >= MIN_NON_RESERVED_WORD && kind <= MAX_NON_RESERVED_WORD) {
1031+
return true;
1032+
}
1033+
1034+
return false;
10311035
}
10321036

10331037
/**
@@ -6492,6 +6496,8 @@ Join JoinerExpression() #JoinerExpression:
64926496
<K_OUTER> { join.setOuter(true); }
64936497
]
64946498

6499+
[ <K_ARRAY_LITERAL> { join.setArray(true); } ]
6500+
64956501
(
64966502
(
64976503
[ joinHint=JoinHint() {join.setJoinHint(joinHint); } ]

src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6527,4 +6527,33 @@ void testIssue2445FunctionWithBracketParameters() throws JSQLParserException {
65276527
" , cast(ex.value_date - f.appraisal_date AS DECIMAL) / 365 )";
65286528
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
65296529
}
6530+
6531+
@Test
6532+
void testClickHouseArrayJoin() throws Exception {
6533+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT s, x FROM t LEFT ARRAY JOIN arr AS x",
6534+
true);
6535+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT s, x FROM t ARRAY JOIN arr AS x", true);
6536+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT s, arr FROM t ARRAY JOIN arr", true);
6537+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT s, arr FROM t LEFT ARRAY JOIN arr", true);
6538+
6539+
TestUtils.assertSqlCanBeParsedAndDeparsed(
6540+
"SELECT s, x, o.name FROM t LEFT ARRAY JOIN arr AS x INNER JOIN other_table o ON t.id = o.t_id",
6541+
true);
6542+
TestUtils.assertSqlCanBeParsedAndDeparsed(
6543+
"WITH exploded AS (SELECT s, x FROM t LEFT ARRAY JOIN arr AS x) SELECT * FROM exploded",
6544+
true);
6545+
6546+
PlainSelect selectLeft =
6547+
(PlainSelect) CCJSqlParserUtil.parse("SELECT s, x FROM t LEFT ARRAY JOIN arr AS x");
6548+
Join joinLeft = selectLeft.getJoins().get(0);
6549+
Assertions.assertTrue(joinLeft.isArray(), "Should be an array join");
6550+
Assertions.assertTrue(joinLeft.isLeft(), "Should be a left join");
6551+
6552+
PlainSelect selectInner =
6553+
(PlainSelect) CCJSqlParserUtil.parse("SELECT s, x FROM t ARRAY JOIN arr AS x");
6554+
Join joinInner = selectInner.getJoins().get(0);
6555+
Assertions.assertTrue(joinInner.isArray(), "Should be an array join");
6556+
Assertions.assertFalse(joinInner.isLeft(), "Should not be a left join");
6557+
}
6558+
65306559
}

0 commit comments

Comments
 (0)