From cd3773a9fa9a364e61940a3c87c69db98f7e30ca Mon Sep 17 00:00:00 2001 From: watashi-00 Date: Mon, 17 Aug 2026 19:44:17 -0300 Subject: [PATCH 1/4] feat(Join): add array join support to Join class --- .../sf/jsqlparser/statement/select/Join.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) 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()) { From 046b365ccb3e6e30c23355217c354c5c24e8e753 Mon Sep 17 00:00:00 2001 From: watashi-00 Date: Mon, 17 Aug 2026 21:46:11 -0300 Subject: [PATCH 2/4] feat(parser): add support for ClickHouse ARRAY JOIN syntax and deparsing --- .../util/deparser/SelectDeParser.java | 4 ++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 37 +++++++++++-------- .../statement/select/SelectTest.java | 25 +++++++++++++ 3 files changed, 51 insertions(+), 15 deletions(-) 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..ed97d6806 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -829,9 +829,12 @@ 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 + && (next2.image == null || !next2.image.equalsIgnoreCase("ARRAY")); + case K_RIGHT: return nextKind != K_JOIN && nextKind != K_OUTER - && nextKind != K_SEMI; + && nextKind != K_SEMI && nextKind != K_ARRAY_LITERAL + && (next2.image == null || !next2.image.equalsIgnoreCase("ARRAY")); case K_ALL: return nextKind != K_JOIN; case K_ANY: return nextKind != OPENING_BRACKET; case K_SOME: return nextKind != OPENING_BRACKET; @@ -993,23 +996,11 @@ 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 @@ -1026,8 +1017,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 +6497,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..2d74a6fa6 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,29 @@ 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 FROM t ARRAY JOIN [1, 2, 3] AS x", 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"); + } + } From 97ff292a3c1a7e091db0e82c578a26f3a1b35647 Mon Sep 17 00:00:00 2001 From: watashi-00 Date: Mon, 17 Aug 2026 22:03:41 -0300 Subject: [PATCH 3/4] fix(parser): correct array literal parsing in JSqlParserCC.jjt --- .../jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt | 11 +++++------ .../sf/jsqlparser/statement/select/SelectTest.java | 1 - 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index ed97d6806..da45e5887 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -829,12 +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_ARRAY_LITERAL - && (next2.image == null || !next2.image.equalsIgnoreCase("ARRAY")); - + && nextKind != K_SEMI && nextKind != K_ARRAY_LITERAL; case K_RIGHT: return nextKind != K_JOIN && nextKind != K_OUTER - && nextKind != K_SEMI && nextKind != K_ARRAY_LITERAL - && (next2.image == null || !next2.image.equalsIgnoreCase("ARRAY")); + && 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; @@ -1006,7 +1005,7 @@ public class CCJSqlParser extends AbstractJSqlParser { // 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: 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 2d74a6fa6..aed77faed 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java @@ -6535,7 +6535,6 @@ void testClickHouseArrayJoin() throws Exception { 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 FROM t ARRAY JOIN [1, 2, 3] AS x", 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( From f5c9ec8aaa8d5dc28c4c989f90a4997e0f36d5a0 Mon Sep 17 00:00:00 2001 From: watashi-00 Date: Mon, 17 Aug 2026 22:32:19 -0300 Subject: [PATCH 4/4] style: apply spotless code formatting --- .../jsqlparser/statement/select/SelectTest.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) 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 aed77faed..c664acd8a 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java @@ -6530,22 +6530,27 @@ void testIssue2445FunctionWithBracketParameters() throws JSQLParserException { @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 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); + "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); + "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"); + 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"); + 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");