From 942a2c8eec9cff145177f27302a72d058d95126c Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Wed, 4 Jun 2025 10:32:22 -0700 Subject: [PATCH 1/5] added create user statement --- .../jdbc/internal/ClickHouseLexer.g4 | 26 +++++++++++ .../jdbc/internal/ClickHouseParser.g4 | 46 ++++++++++++++++++- .../com/clickhouse/jdbc/StatementImpl.java | 4 +- .../jdbc/internal/ParsedStatement.java | 2 + .../clickhouse/jdbc/internal/SqlParser.java | 18 +++++++- .../com/clickhouse/jdbc/StatementTest.java | 26 +++++++++-- 6 files changed, 114 insertions(+), 8 deletions(-) diff --git a/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseLexer.g4 b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseLexer.g4 index ac3bbbea6..ce51eb41b 100644 --- a/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseLexer.g4 +++ b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseLexer.g4 @@ -27,11 +27,14 @@ ATTACH : A T T A C H; BETWEEN : B E T W E E N; BOTH : B O T H; BY : B Y; +BCRYPT_PASSWORD : B C R Y P T '_' P A S S W O R D; +BCRYPT_HASH : B C R Y P T '_' H A S H; CASE : C A S E; CAST : C A S T; CHECK : C H E C K; CLEAR : C L E A R; CLUSTER : C L U S T E R; +CN : C N; CODEC : C O D E C; COLLATE : C O L L A T E; COLUMN : C O L U M N; @@ -59,6 +62,8 @@ DICTIONARY : D I C T I O N A R Y; DISK : D I S K; DISTINCT : D I S T I N C T; DISTRIBUTED : D I S T R I B U T E D; +DOUBLE_SHA1_PASSWORD : D O U B L E '_' S H A '1' '_' P A S S W O R D; +DOUBLE_SHA1_HASH : D O U B L E '_' S H A '1' '_' H A S H; DROP : D R O P; ELSE : E L S E; END : E N D; @@ -82,11 +87,15 @@ FULL : F U L L; FUNCTION : F U N C T I O N; GLOBAL : G L O B A L; GRANULARITY : G R A N U L A R I T Y; +GRANTEES : G R A N T E E S; GROUP : G R O U P; HAVING : H A V I N G; HIERARCHICAL : H I E R A R C H I C A L; +HTTP : H T T P; +HOST : H O S T; HOUR : H O U R; ID : I D; +IDENTIFIED : I D E N T I F I E D; IF : I F; ILIKE : I L I K E; IN : I N; @@ -97,13 +106,16 @@ INNER : I N N E R; INSERT : I N S E R T; INTERVAL : I N T E R V A L; INTO : I N T O; +IP : I P; IS : I S; IS_OBJECT_ID : I S UNDERSCORE O B J E C T UNDERSCORE I D; JOIN : J O I N; KEY : K E Y; +KERBEROS : K E R B E R O S; KILL : K I L L; LAST : L A S T; LAYOUT : L A Y O U T; +LDAP : L D A P; LEADING : L E A D I N G; LEFT : L E F T; LIFETIME : L I F E T I M E; @@ -123,7 +135,9 @@ MONTH : M O N T H; MOVE : M O V E; MUTATION : M U T A T I O N; NAN_SQL : N A N; // conflicts with macro NAN +NAME : N A M E; NO : N O; +NO_PASSWORD : N O '_' P A S S W O R D; NONE : N O N E; NOT : N O T; NULL_SQL : N U L L; // conflicts with macro NULL @@ -142,8 +156,11 @@ PRECEDING : P R E C E D I N G; PREWHERE : P R E W H E R E; PRIMARY : P R I M A R Y; PROJECTION : P R O J E C T I O N; +PLAINTEXT_PASSWORD : P L A I N T E X T '_' P A S S W O R D; QUARTER : Q U A R T E R; RANGE : R A N G E; +REALM : R E A L M; +REGEXP : R E G E X P; RELOAD : R E L O A D; REMOVE : R E M O V E; RENAME : R E N A M E; @@ -156,13 +173,21 @@ ROLLUP : R O L L U P; ROW : R O W; ROWS : R O W S; SAMPLE : S A M P L E; +SCHEMA : S C H E M A; +SCRAM_SHA256_PASSWORD : S C R A M '_' S H A '2' '5' '6' '_' P A S S W O R D; +SCRAM_SHA256_HASH : S C R A M '_' S H A '2' '5' '6' '_' H A S H; SECOND : S E C O N D; SELECT : S E L E C T; SEMI : S E M I; SENDS : S E N D S; +SERVER : S E R V E R; +SSL_CERTIFICATE : S S L '_' C E R T I F I C A T E; +SSH_KEY : S S H '_' K E Y; SET : S E T; SETTINGS : S E T T I N G S; SHOW : S H O W; +SHA256_PASSWORD : S H A '2' '5' '6' '_' P A S S W O R D; +SHA256_HASH : S H A '2' '5' '6' '_' H A S H; SOURCE : S O U R C E; START : S T A R T; STOP : S T O P; @@ -190,6 +215,7 @@ UNBOUNDED : U N B O U N D E D; UNION : U N I O N; UPDATE : U P D A T E; USE : U S E; +USER : U S E R; USING : U S I N G; UUID : U U I D; VALUES : V A L U E S; diff --git a/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseParser.g4 b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseParser.g4 index 896ffb7bf..030764997 100644 --- a/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseParser.g4 +++ b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseParser.g4 @@ -142,6 +142,50 @@ createStmt engineClause? subqueryClause? # CreateTableStmt | (ATTACH | CREATE) (OR REPLACE)? VIEW (IF NOT EXISTS)? tableIdentifier uuidClause? clusterClause? tableSchemaClause? subqueryClause # CreateViewStmt + | CREATE USER ((IF NOT EXISTS) | (OR REPLACE))? userIdentifier (COMMA userIdentifier)* clusterClause? + userIdentifiedClause? + userCreateHostClause? + validUntilClause? + (DEFAULT ROLE identifier (COMMA identifier)*)? + (DEFAULT DATABASE identifier | NONE)? + + settingsClause? #CreateUserStmt + ; + +userIdentifier + : (IDENTIFIER | STRING_LITERAL) + ; + +userIdentifiedClause + : IDENTIFIED BY literal + | IDENTIFIED WITH userIdentifiedWithClause validUntilClause? (COMMA userIdentifiedWithClause VALID UNTIL literal)* + | NOT IDENTIFIED + ; + +userIdentifiedWithClause + : (PLAINTEXT_PASSWORD | SHA256_PASSWORD | SHA256_HASH | DOUBLE_SHA1_PASSWORD | DOUBLE_SHA1_HASH | SCRAM_SHA256_PASSWORD | SCRAM_SHA256_HASH | BCRYPT_PASSWORD | BCRYPT_HASH) BY literal + | NO_PASSWORD + | LDAP SERVER literal + | KERBEROS (REALM literal)? + | SSL_CERTIFICATE CN literal + | SSH_KEY BY KEY literal TYPE literal + | HTTP SERVER literal (SCHEMA literal)? + ; + +userCreateHostClause + : HOST (userCreateHostDef (COMMA userCreateHostDef)*) | ANY | NONE + ; + +userCreateHostDef + : LOCAL | NAME literal | REGEXP literal | IP literal | LIKE literal + ; + +userCreateGranteesClause + : GRANTEES (identifier | STRING_LITERAL | ANY | NONE ) (COMMA (identifier | STRING_LITERAL | ANY | NONE ))* + (EXCEPT (identifier | STRING_LITERAL) (COMMA (identifier | STRING_LITERAL ))*) + ; +validUntilClause + : VALID UNTIL interval ; dictionarySchemaClause @@ -950,4 +994,4 @@ identifierOrNull enumValue : STRING_LITERAL EQ_SINGLE numberLiteral - ; \ No newline at end of file + ; diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java index cff7530ef..f79a7da07 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java @@ -129,7 +129,7 @@ protected ResultSetImpl executeQueryImpl(String sql, QuerySettings settings) thr try { lastStatementSql = parseJdbcEscapeSyntax(sql); - LOG.debug("SQL Query: {}", lastStatementSql); + LOG.trace("SQL Query: {}", lastStatementSql); // this is not secure for create statements because of passwords QueryResponse response; if (queryTimeout == 0) { response = connection.client.query(lastStatementSql, mergedSettings).get(); @@ -179,7 +179,7 @@ protected int executeUpdateImpl(String sql, QuerySettings settings) throws SQLEx } lastStatementSql = parseJdbcEscapeSyntax(sql); - LOG.debug("SQL Query: {}", lastStatementSql); + LOG.trace("SQL Query: {}", lastStatementSql); int updateCount = 0; try (QueryResponse response = queryTimeout == 0 ? connection.client.query(lastStatementSql, mergedSettings).get() : connection.client.query(lastStatementSql, mergedSettings).get(queryTimeout, TimeUnit.SECONDS)) { diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedStatement.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedStatement.java index 709fda0ca..f12fb8ccb 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedStatement.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedStatement.java @@ -103,4 +103,6 @@ public void enterSetRoleStmt(ClickHouseParser.SetRoleStmtContext ctx) { setRoles(roles); } } + + } diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParser.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParser.java index 620ff5b4d..a0ed91b2a 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParser.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParser.java @@ -1,25 +1,34 @@ package com.clickhouse.jdbc.internal; +import org.antlr.v4.runtime.ANTLRErrorListener; +import org.antlr.v4.runtime.BaseErrorListener; import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; import org.antlr.v4.runtime.tree.IterativeParseTreeWalker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.regex.Matcher; import java.util.regex.Pattern; public class SqlParser { + private static Logger LOG = LoggerFactory.getLogger(SqlParser.class); + public ParsedStatement parsedStatement(String sql) { CharStream charStream = CharStreams.fromString(sql); ClickHouseLexer lexer = new ClickHouseLexer(charStream); ClickHouseParser parser = new ClickHouseParser(new CommonTokenStream(lexer)); + parser.removeErrorListeners(); + parser.addErrorListener(new ParserErrorListener()); ClickHouseParser.QueryStmtContext parseTree = parser.queryStmt(); ParsedStatement parserListener = new ParsedStatement(); IterativeParseTreeWalker.DEFAULT.walk(parserListener, parseTree); - return parserListener; } @@ -54,4 +63,11 @@ public static String escapeQuotes(String str) { .replace("'", "\\'") .replace("\"", "\\\""); } + + private static class ParserErrorListener extends BaseErrorListener { + @Override + public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) { + LOG.warn("SQL syntax error at line: " + line + ", pos: " + charPositionInLine + ", " + msg); + } + } } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java index 058df6eb4..b8ca45afd 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java @@ -341,7 +341,7 @@ public void testExecuteQueryTimeout() throws Exception { @Test(groups = { "integration" }) - private void testSettingRole() throws SQLException { + public void testSettingRole() throws SQLException { if (earlierThan(24, 4)) {//Min version is 24.4 return; } @@ -550,8 +550,8 @@ public void testSwitchDatabase() throws Exception { } } } - - + + @Test(groups = { "integration" }) public void testNewLineSQLParsing() throws Exception { try (Connection conn = getJdbcConnection()) { @@ -616,7 +616,7 @@ public void testNewLineSQLParsing() throws Exception { } } - + @Test(groups = { "integration" }) public void testNullableFixedStringType() throws Exception { try (Connection conn = getJdbcConnection()) { @@ -707,4 +707,22 @@ public void testExecuteWithMaxRows() throws Exception { } } + @Test(groups = {"integration"}) + public void testDDLStatements() throws Exception { + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()){ + Assert.assertFalse(stmt.execute("CREATE USER IF NOT EXISTS 'user011' IDENTIFIED BY 'password'")); + + try (ResultSet rs = stmt.executeQuery("SHOW USERS")) { + boolean found = false; + while (rs.next()) { + if (rs.getString("name").equals("user011")) { + found = true; + } + } + Assert.assertTrue(found); + } + } + } + } } From af9e682309635ff0121305d82960d82b5f0f1cfd Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Wed, 4 Jun 2025 11:05:02 -0700 Subject: [PATCH 2/5] added version check for recursive CTE test --- .../test/java/com/clickhouse/jdbc/PreparedStatementTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java index 002fb5fb4..3b151fcfb 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java @@ -360,6 +360,10 @@ void testMultipleWithClauses() throws Exception { @Test(groups = { "integration" }) void testRecursiveWithClause() throws Exception { + if (ClickHouseVersion.of(getServerVersion()).check("(,24.3]")) { + return; // recursive CTEs were introduces in 24.4 + } + try (Connection conn = getJdbcConnection(); PreparedStatement stmt = conn.prepareStatement( "WITH RECURSIVE numbers AS (" + From 0b55563268141a570a933b338d0d2db855145c4a Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Wed, 4 Jun 2025 11:18:11 -0700 Subject: [PATCH 3/5] disabled DDL tests for cloud --- .../test/java/com/clickhouse/jdbc/PreparedStatementTest.java | 2 +- jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java index 3b151fcfb..cf4ab9493 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java @@ -363,7 +363,7 @@ void testRecursiveWithClause() throws Exception { if (ClickHouseVersion.of(getServerVersion()).check("(,24.3]")) { return; // recursive CTEs were introduces in 24.4 } - + try (Connection conn = getJdbcConnection(); PreparedStatement stmt = conn.prepareStatement( "WITH RECURSIVE numbers AS (" + diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java index b8ca45afd..6f5f66678 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java @@ -709,6 +709,9 @@ public void testExecuteWithMaxRows() throws Exception { @Test(groups = {"integration"}) public void testDDLStatements() throws Exception { + if (isCloud()) { + return; // skip because we do not want to create extra on cloud instance + } try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()){ Assert.assertFalse(stmt.execute("CREATE USER IF NOT EXISTS 'user011' IDENTIFIED BY 'password'")); From d5e9c9d84d94821e90acc62b97f8927032b94aad Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Thu, 5 Jun 2025 12:44:50 -0700 Subject: [PATCH 4/5] fixed IDENTIFIED BY KEY grammer. added more tests --- .../jdbc/internal/ClickHouseParser.g4 | 2 +- .../jdbc/internal/SqlParserTest.java | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseParser.g4 b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseParser.g4 index e6b5b7740..e94537c15 100644 --- a/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseParser.g4 +++ b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseParser.g4 @@ -168,7 +168,7 @@ userIdentifiedWithClause | LDAP SERVER literal | KERBEROS (REALM literal)? | SSL_CERTIFICATE CN literal - | SSH_KEY BY KEY literal TYPE literal + | SSH_KEY BY KEY literal TYPE literal (COMMA KEY literal TYPE literal)* | HTTP SERVER literal (SCHEMA literal)? ; diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/SqlParserTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/SqlParserTest.java index 83adc500d..992a5643e 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/SqlParserTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/SqlParserTest.java @@ -2,6 +2,7 @@ import org.testng.Assert; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import static org.testng.Assert.assertEquals; @@ -210,4 +211,34 @@ public void testStmtWithFunction() { ParsedPreparedStatement stmt = parser.parsePreparedStatement(sql); Assert.assertEquals(stmt.getArgCount(), 4); } + + @Test(dataProvider = "testCreateStmtDP") + public void testCreateStatement(String sql) { + SqlParser parser = new SqlParser(); + ParsedPreparedStatement stmt = parser.parsePreparedStatement(sql); + Assert.assertFalse(stmt.isHasErrors()); + } + + @DataProvider + public static Object[][] testCreateStmtDP() { + return new Object[][] { + {"CREATE USER 'user01' IDENTIFIED WITH no_password"}, + {"CREATE USER 'user01' IDENTIFIED WITH plaintext_password BY 'qwerty'"}, + {"CREATE USER 'user01' IDENTIFIED WITH sha256_password BY 'qwerty' or IDENTIFIED BY 'password'"}, + {"CREATE USER 'user01' IDENTIFIED WITH sha256_hash BY 'hash' SALT 'salt'"}, + {"CREATE USER 'user01' IDENTIFIED WITH sha256_hash BY 'hash'"}, + {"CREATE USER 'user01' IDENTIFIED WITH double_sha1_password BY 'qwerty'"}, + {"CREATE USER 'user01' IDENTIFIED WITH double_sha1_hash BY 'hash'"}, + {"CREATE USER 'user01' IDENTIFIED WITH bcrypt_password BY 'qwerty'"}, + {"CREATE USER 'user01' IDENTIFIED WITH bcrypt_hash BY 'hash'"}, + {"CREATE USER 'user01' IDENTIFIED WITH ldap SERVER 'server_name'"}, + {"CREATE USER 'user01' IDENTIFIED WITH kerberos"}, + {"CREATE USER 'user01' IDENTIFIED WITH kerberos REALM 'realm'"}, + {"CREATE USER 'user01' IDENTIFIED WITH ssl_certificate CN 'mysite.com:user'"}, + {"CREATE USER 'user01' IDENTIFIED WITH ssh_key BY KEY 'public_key' TYPE 'ssh-rsa', KEY 'another_public_key' TYPE 'ssh-ed25519'"}, + {"CREATE USER 'user01' IDENTIFIED WITH http SERVER 'http_server' SCHEME 'basic'"}, + {"CREATE USER 'user01' IDENTIFIED WITH http SERVER 'http_server'"}, + {"CREATE USER 'user01' IDENTIFIED BY 'qwerty'"}, + }; + } } \ No newline at end of file From 5ca334b1db3ad012f681bd31f2a911ca4f7ca635 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Thu, 5 Jun 2025 12:47:00 -0700 Subject: [PATCH 5/5] fixed minor issues --- .../src/main/java/com/clickhouse/jdbc/internal/SqlParser.java | 3 +-- .../test/java/com/clickhouse/jdbc/PreparedStatementTest.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParser.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParser.java index a0ed91b2a..47ca141c3 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParser.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParser.java @@ -1,6 +1,5 @@ package com.clickhouse.jdbc.internal; -import org.antlr.v4.runtime.ANTLRErrorListener; import org.antlr.v4.runtime.BaseErrorListener; import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.CharStreams; @@ -16,7 +15,7 @@ public class SqlParser { - private static Logger LOG = LoggerFactory.getLogger(SqlParser.class); + private static final Logger LOG = LoggerFactory.getLogger(SqlParser.class); public ParsedStatement parsedStatement(String sql) { diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java index be0cfd57b..6f7a252c9 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java @@ -363,7 +363,7 @@ void testMultipleWithClauses() throws Exception { @Test(groups = { "integration" }) void testRecursiveWithClause() throws Exception { if (ClickHouseVersion.of(getServerVersion()).check("(,24.3]")) { - return; // recursive CTEs were introduces in 24.4 + return; // recursive CTEs were introduced in 24.4 } try (Connection conn = getJdbcConnection();