diff --git a/src/main/java/net/sf/jsqlparser/statement/select/Join.java b/src/main/java/net/sf/jsqlparser/statement/select/Join.java index 191465e27..c8d675cec 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/Join.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/Join.java @@ -40,6 +40,7 @@ public class Join extends ASTNodeAccessImpl { private boolean straight = false; private boolean apply = false; private boolean fetch = false; + private boolean array = false; private FromItem fromItem; private KSQLJoinWindow joinWindow; @@ -73,7 +74,10 @@ public boolean isInnerJoin() { || cross /* Natural Join */ - || natural); + || natural + + /* Array Join */ + || array); } /** @@ -337,6 +341,19 @@ public Join withCross(boolean cross) { return this; } + public boolean isArray() { + return array; + } + + public void setArray(boolean array) { + this.array = array; + } + + public Join withArray(boolean array) { + this.setArray(array); + return this; + } + /** * Returns the right item of the join */ @@ -489,6 +506,10 @@ public String toString() { builder.append("SEMI "); } + if (isArray()) { + builder.append("ARRAY "); + } + if (isStraight()) { builder.append("STRAIGHT_JOIN "); } else if (isApply()) { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java index c061ff45f..066d40d06 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java @@ -683,6 +683,10 @@ public void deparseJoin(Join join) { builder.append(" SEMI"); } + if (join.isArray()) { + builder.append(" ARRAY"); + } + if (join.isStraight()) { builder.append(" STRAIGHT_JOIN "); } else if (join.isApply()) { diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 9d0314204..da45e5887 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -829,9 +829,11 @@ public class CCJSqlParser extends AbstractJSqlParser { case K_CONNECT: return nextKind != K_BY; case K_START: return nextKind != K_WITH; case K_LEFT: return nextKind != K_JOIN && nextKind != K_OUTER - && nextKind != K_SEMI; + && nextKind != K_SEMI && nextKind != K_ARRAY_LITERAL; case K_RIGHT: return nextKind != K_JOIN && nextKind != K_OUTER - && nextKind != K_SEMI; + && nextKind != K_SEMI && nextKind != K_ARRAY_LITERAL; + case K_ARRAY_LITERAL: + return nextKind != K_JOIN; case K_ALL: return nextKind != K_JOIN; case K_ANY: return nextKind != OPENING_BRACKET; case K_SOME: return nextKind != OPENING_BRACKET; @@ -993,29 +995,17 @@ public class CCJSqlParser extends AbstractJSqlParser { // String-literal alias: SELECT col 'myAlias' if (kind == S_CHAR_LITERAL) return true; - // Base identifier tokens - if (kind == S_IDENTIFIER || kind == S_QUOTED_IDENTIFIER - || kind == DATA_TYPE || kind == K_DATETIMELITERAL - || kind == K_DATE_LITERAL) { - return true; - } - // OPTION (...) introduces a query hint clause, not an alias if (kind == K_OPTION && getToken(2).kind == OPENING_BRACKET) { return false; } - // Non-reserved keywords - if (kind >= MIN_NON_RESERVED_WORD && kind <= MAX_NON_RESERVED_WORD) { - return true; - } - // For reserved keywords in alias position, skip the structural- // keyword whitelist and go directly to follower disambiguation. // Only check keywords that are actually in RelObjectName's token // alternatives — don't fire for brackets, operators, literals, etc. switch (kind) { - case K_ALL: case K_ANY: case K_CASEWHEN: case K_CONNECT: + case K_ALL: case K_ARRAY_LITERAL: case K_ANY: case K_CASEWHEN: case K_CONNECT: case K_CREATE: case K_DEFAULT: case K_GLOBAL: case K_GROUP: case K_GROUPING: case K_IF: case K_IIF: case K_IGNORE: case K_IN: case K_INTERVAL: @@ -1026,8 +1016,22 @@ public class CCJSqlParser extends AbstractJSqlParser { case K_TOP: case K_VALUE: case K_VALUES: return isReservedKeywordSafeByFollower(); default: - return false; + break; } + + // Base identifier tokens + if (kind == S_IDENTIFIER || kind == S_QUOTED_IDENTIFIER + || kind == DATA_TYPE || kind == K_DATETIMELITERAL + || kind == K_DATE_LITERAL) { + return true; + } + + // Non-reserved keywords + if (kind >= MIN_NON_RESERVED_WORD && kind <= MAX_NON_RESERVED_WORD) { + return true; + } + + return false; } /** @@ -6492,6 +6496,8 @@ Join JoinerExpression() #JoinerExpression: { join.setOuter(true); } ] + [ { join.setArray(true); } ] + ( ( [ joinHint=JoinHint() {join.setJoinHint(joinHint); } ] diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java index d0000c138..c664acd8a 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java @@ -6527,4 +6527,33 @@ void testIssue2445FunctionWithBracketParameters() throws JSQLParserException { " , cast(ex.value_date - f.appraisal_date AS DECIMAL) / 365 )"; TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @Test + void testClickHouseArrayJoin() throws Exception { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT s, x FROM t LEFT ARRAY JOIN arr AS x", + true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT s, x FROM t ARRAY JOIN arr AS x", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT s, arr FROM t ARRAY JOIN arr", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT s, arr FROM t LEFT ARRAY JOIN arr", true); + + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT s, x, o.name FROM t LEFT ARRAY JOIN arr AS x INNER JOIN other_table o ON t.id = o.t_id", + true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "WITH exploded AS (SELECT s, x FROM t LEFT ARRAY JOIN arr AS x) SELECT * FROM exploded", + true); + + PlainSelect selectLeft = + (PlainSelect) CCJSqlParserUtil.parse("SELECT s, x FROM t LEFT ARRAY JOIN arr AS x"); + Join joinLeft = selectLeft.getJoins().get(0); + Assertions.assertTrue(joinLeft.isArray(), "Should be an array join"); + Assertions.assertTrue(joinLeft.isLeft(), "Should be a left join"); + + PlainSelect selectInner = + (PlainSelect) CCJSqlParserUtil.parse("SELECT s, x FROM t ARRAY JOIN arr AS x"); + Join joinInner = selectInner.getJoins().get(0); + Assertions.assertTrue(joinInner.isArray(), "Should be an array join"); + Assertions.assertFalse(joinInner.isLeft(), "Should not be a left join"); + } + }