From 9378ba09f29c4386a2b9d3f551ab825335c616f2 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Mon, 24 Aug 2026 15:05:23 -0700 Subject: [PATCH 1/5] Made 159 EXECUTION TIMEOUT Server error not retriable. Backported some fixes for tests. Added test for JDBC driver with sync=1 --- CHANGELOG.md | 7 +++ .../client/api/ServerException.java | 5 +- .../com/clickhouse/client/ClientTests.java | 39 ++++++++------- .../client/metrics/MetricsTest.java | 47 +++++++++++++------ .../clickhouse/client/query/QueryTests.java | 12 +++-- .../parser/javacc/ClickHouseSqlUtils.java | 16 ++++++- .../jdbc/metadata/DatabaseMetaDataImpl.java | 22 +++++++-- .../clickhouse/jdbc/JdbcDataTypeTests.java | 15 ++++-- .../com/clickhouse/jdbc/StatementTest.java | 36 +++++++++----- .../src/test/resources/StatementSQLTests.yaml | 4 +- 10 files changed, 140 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f836a5c89..53f1fd6de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ ## 0.9.8 +### Bug Fixes + +- **[client-v2]** `ServerException` with code `159 Execution Timeout` is retried unconditionally. After the fix this +error treated as non-retriable. + +## 0.9.8 + ### Improvements - **[client-v2]** Added `Records#getSchema()` to expose table schema metadata even when query result is empty. (https://github.com/ClickHouse/clickhouse-java/pull/2777) diff --git a/client-v2/src/main/java/com/clickhouse/client/api/ServerException.java b/client-v2/src/main/java/com/clickhouse/client/api/ServerException.java index bd2361bfa..6b350ca19 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/ServerException.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/ServerException.java @@ -8,6 +8,8 @@ public class ServerException extends ClickHouseException { public static final int UNKNOWN_SETTING = 115; + public static final int EXECUTION_TIMEOUT = 159; + private final int code; private final int transportProtocolCode; @@ -56,10 +58,9 @@ public String getQueryId() { private boolean discoverIsRetryable(int code, String message, int transportProtocolCode) { //Let's check if we have a ServerException to reference the error code //https://github.com/ClickHouse/ClickHouse/blob/master/src/Common/ErrorCodes.cpp - switch (code) { // UNEXPECTED_END_OF_FILE + switch (code) { case 3: // UNEXPECTED_END_OF_FILE case 107: // FILE_DOESNT_EXIST - case 159: // TIMEOUT_EXCEEDED case 164: // READONLY case 202: // TOO_MANY_SIMULTANEOUS_QUERIES case 203: // NO_FREE_CONNECTION diff --git a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java index d63f9b2cb..2de2e6aaa 100644 --- a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java @@ -1,12 +1,6 @@ package com.clickhouse.client; -import com.clickhouse.client.api.Client; -import com.clickhouse.client.api.ClientConfigProperties; -import com.clickhouse.client.api.ClientException; -import com.clickhouse.client.api.ClientFaultCause; -import com.clickhouse.client.api.ClientMisconfigurationException; -import com.clickhouse.client.api.ConnectionReuseStrategy; -import com.clickhouse.client.api.ServerException; +import com.clickhouse.client.api.*; import com.clickhouse.client.api.enums.Protocol; import com.clickhouse.client.api.insert.InsertSettings; import com.clickhouse.client.api.internal.ClickHouseLZ4OutputStream; @@ -30,17 +24,7 @@ import java.io.ByteArrayInputStream; import java.net.ConnectException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Queue; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -569,6 +553,25 @@ public void testQueryIdGenerator() throws Exception { Assert.assertEquals(actualIds, new ArrayList<>(queryIds)); } + @Test(groups = {"integration"}) + public void testExecutionTimeout() throws Exception{ + final String query = "SELECT count(), sum(sipHash64(number)) " + + "FROM numbers(1000000000) " + + "SETTINGS max_threads = 1;"; + + long startTime = System.currentTimeMillis(); + int maxExecTime = 4000; + try (Client client = newClient().serverSetting("max_execution_time", String.valueOf(TimeUnit.MILLISECONDS.toSeconds(maxExecTime))).build(); + QueryResponse response = client.query(query).get()) { + + } catch (ServerException e) { + long queryTime = System.currentTimeMillis() - startTime; + System.out.println(queryTime + " - query time"); + Assert.assertTrue(Math.abs(queryTime - maxExecTime) < 1000); + Assert.assertEquals(e.getCode(), ServerException.EXECUTION_TIMEOUT); + } + } + public boolean isVersionMatch(String versionExpression, Client client) { List serverVersion = client.queryAll("SELECT version()"); return ClickHouseVersion.of(serverVersion.get(0).getString(1)).check(versionExpression); diff --git a/client-v2/src/test/java/com/clickhouse/client/metrics/MetricsTest.java b/client-v2/src/test/java/com/clickhouse/client/metrics/MetricsTest.java index 249dc8874..ae61d7e96 100644 --- a/client-v2/src/test/java/com/clickhouse/client/metrics/MetricsTest.java +++ b/client-v2/src/test/java/com/clickhouse/client/metrics/MetricsTest.java @@ -19,9 +19,7 @@ import org.testng.annotations.Test; import java.time.temporal.ChronoUnit; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; import static org.testng.Assert.assertEquals; import static org.testng.Assert.fail; @@ -40,7 +38,7 @@ void tearDown() { meterRegistry.clear(); Metrics.globalRegistry.clear(); } - + @Test(groups = {"integration"}, enabled = true) public void testRegisterMetrics() throws Exception { ClickHouseNode node = getServer(ClickHouseProtocol.HTTP); @@ -54,6 +52,7 @@ public void testRegisterMetrics() throws Exception { .serverSetting(ServerSettings.ASYNC_INSERT, "0") .serverSetting(ServerSettings.WAIT_END_OF_QUERY, "1") .registerClientMetrics(meterRegistry, "pool-test") + .setKeepAliveTimeout(10, ChronoUnit.SECONDS) // enforce connection cleanup .build()) { client.ping(); @@ -66,21 +65,38 @@ public void testRegisterMetrics() throws Exception { Assert.assertEquals((int) available.value(), 1); Assert.assertEquals((int) leased.value(), 0); + CountDownLatch responsesReady = new CountDownLatch(2); + CountDownLatch releaseResponses = new CountDownLatch(1); Runnable task = () -> { try (QueryResponse response = client.query("SELECT 1").get()) { - Assert.assertEquals((int) available.value(), 0); - Assert.assertEquals((int) leased.value(), 1); + responsesReady.countDown(); + Assert.assertTrue(releaseResponses.await(10, TimeUnit.SECONDS), + "Timed out waiting to release query responses"); } catch (Exception e) { - e.printStackTrace(); - fail("Failed to to request", e); + throw new RuntimeException("Failed to execute request", e); } }; - ExecutorService executor = Executors.newFixedThreadPool(3); - executor.submit(task); - executor.submit(task); - executor.shutdown(); - executor.awaitTermination(10, TimeUnit.SECONDS); + ExecutorService executor = Executors.newFixedThreadPool(2); + Future firstQuery = executor.submit(task); + Future secondQuery = executor.submit(task); + try { + try { + Assert.assertTrue(responsesReady.await(10, TimeUnit.SECONDS), + "Timed out waiting for concurrent query responses"); + Assert.assertEquals((int) available.value(), 0); + Assert.assertEquals((int) leased.value(), 2); + } finally { + releaseResponses.countDown(); + } + firstQuery.get(10, TimeUnit.SECONDS); + secondQuery.get(10, TimeUnit.SECONDS); + } finally { + releaseResponses.countDown(); + executor.shutdownNow(); + } + Assert.assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS), + "Timed out waiting for query executor to terminate"); Assert.assertEquals((int) available.value(), 2); Assert.assertEquals((int) leased.value(), 0); @@ -90,7 +106,10 @@ public void testRegisterMetrics() throws Exception { Assert.assertEquals((int) available.value(), 2); Assert.assertEquals((int) leased.value(), 0); - task.run(); + try (QueryResponse response = client.query("SELECT 1").get()) { + Assert.assertEquals((int) available.value(), 0); + Assert.assertEquals((int) leased.value(), 1); + } Assert.assertEquals((int) available.value(), 1); Assert.assertEquals((int) leased.value(), 0); diff --git a/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java b/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java index 2651f88e9..5b6771d5b 100644 --- a/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java @@ -988,6 +988,10 @@ public void testIntegerDataTypes() { @Test(groups = {"integration"}) public void testFloatDataTypes() { + final boolean usesPreciseFloatParsing = isVersionMatch("[26.7,)"); + final float expectedMaxFloat32 = usesPreciseFloatParsing ? Float.MAX_VALUE : 3.4028233E38F; + final double expectedMinFloat64 = usesPreciseFloatParsing ? Double.MIN_VALUE : 0.0D; + final List columns = Arrays.asList( "min_float32 Float32", "max_float32 Float32", @@ -1019,12 +1023,12 @@ public void testFloatDataTypes() { }); verifiers.add(r -> { - Assert.assertEquals(r.getFloat("max_float32"), 3.4028233E38F); // TODO: investigate why it's not Float.MAX_VALUE returned from server - Assert.assertEquals(r.getFloat(2), 3.4028233E38F); + Assert.assertEquals(r.getFloat("max_float32"), expectedMaxFloat32); + Assert.assertEquals(r.getFloat(2), expectedMaxFloat32); }); verifiers.add(r -> { - Assert.assertEquals(r.getDouble("min_float64"), 0.0D); // TODO: investigate why it's not Double.MIN_VALUE returned from server - Assert.assertEquals(r.getDouble(3), 0.0D); + Assert.assertEquals(r.getDouble("min_float64"), expectedMinFloat64); + Assert.assertEquals(r.getDouble(3), expectedMinFloat64); }); verifiers.add(r -> { Assert.assertEquals(r.getDouble("max_float64"), Double.MAX_VALUE); diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/parser/javacc/ClickHouseSqlUtils.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/parser/javacc/ClickHouseSqlUtils.java index ba4b5f174..960648d39 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/parser/javacc/ClickHouseSqlUtils.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/parser/javacc/ClickHouseSqlUtils.java @@ -60,7 +60,21 @@ private static Set initAllowedKeywordAliases() { "UNDROP", "UNFREEZE", "UNIQUE", "UNLOCK", "UNSET", "UNSIGNED", "UNTIL", "UPDATE", "URL", "USE", "USER", "VALID", "VALUES", "VARYING", "VIEW", "VISIBLE", "VOLUME", "WATCH", "WATERMARK", "WEEK", "WEEKS", "WHEN", "WITH_ITEMINDEX", "WK", "WORKER", "WORKLOAD", "WRITABLE", "WRITE", "WW", - "YEAR", "YEARS", "YY", "YYYY", "ZKPATH"); + "YEAR", "YEARS", "YY", "YYYY", "ZKPATH", + // Appended 04/01/2026. + "CENTURY", "DECADE", "DOW", "DOY", "EPOCH", "ISODOW", "ISOYEAR", "MILLENNIUM", "NATURAL", "SOME", + "ZONE", + // Appended 04/10/2026 + "PATH", "PLACING", + // Appended 05/27/2026 + "CURSOR", "DETERMINISTIC", "ESCAPE", "SAMPLES", "STREAM", "UNKNOWN", + // Appended 06/10/2026 + "IPV4_PREFIX_BITS", "IPV6_PREFIX_BITS", "TIMEZONE_HOUR", "TIMEZONE_MINUTE", + // Appended 06/26/2026 + "ENUM", "HYPOTHETICAL", "WHATIF", + // Appended 07/21/2026 + "ANALYZE", "AT", "MANIFEST", "RESERVATION" + ); } private static Set buildKeywordSet(String... values) { diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java index b79dd895b..630cbb2ca 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java @@ -775,26 +775,26 @@ public ResultSet getProcedureColumns(String catalog, String schemaPattern, Strin static final Map ENGINE_TO_TABLE_TYPE; static { Map map = new java.util.HashMap<>(); - + // Log tables map.put("Log", TableType.LOG_TABLE.getTypeName()); map.put("StripeLog", TableType.LOG_TABLE.getTypeName()); map.put("TinyLog", TableType.LOG_TABLE.getTypeName()); - + // Memory tables map.put("Buffer", TableType.MEMORY_TABLE.getTypeName()); map.put("Memory", TableType.MEMORY_TABLE.getTypeName()); map.put("Set", TableType.MEMORY_TABLE.getTypeName()); - + // Views map.put("View", TableType.VIEW.getTypeName()); map.put("LiveView", TableType.VIEW.getTypeName()); map.put("MaterializedView", TableType.MATERIALIZED_VIEW.getTypeName()); map.put("WindowView", TableType.VIEW.getTypeName()); - + // Dictionary map.put("Dictionary", TableType.DICTIONARY.getTypeName()); - + // Remote/External tables map.put("AzureBlobStorage", TableType.REMOTE_TABLE.getTypeName()); map.put("AzureQueue", TableType.REMOTE_TABLE.getTypeName()); @@ -865,6 +865,18 @@ public ResultSet getProcedureColumns(String catalog, String schemaPattern, Strin map.put("SharedSummingMergeTree", TableType.TABLE.getTypeName()); map.put("SharedVersionedCollapsingMergeTree", TableType.TABLE.getTypeName()); + // Paimon (appended 05/27/2026) + map.put("Paimon", TableType.REMOTE_TABLE.getTypeName()); + map.put("PaimonAzure", TableType.REMOTE_TABLE.getTypeName()); + map.put("PaimonHDFS", TableType.REMOTE_TABLE.getTypeName()); + map.put("PaimonLocal", TableType.REMOTE_TABLE.getTypeName()); + map.put("PaimonS3", TableType.REMOTE_TABLE.getTypeName()); + + // Remote engines (appended 07/21/2026) + map.put("QueryRunner", TableType.REMOTE_TABLE.getTypeName()); + map.put("Remote", TableType.REMOTE_TABLE.getTypeName()); + map.put("RemoteSecure", TableType.REMOTE_TABLE.getTypeName()); + // Special map.put("TimeSeries", TableType.TABLE.getTypeName()); map.put("Null", TableType.TABLE.getTypeName()); diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java index 7de1201ea..10e02dedd 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java @@ -81,6 +81,11 @@ private int insertData(String sql) throws SQLException { } } + private static void assertFloat32Boundary(float actual, float expectedA, float expectedB, String label) { + Assert.assertTrue(actual == expectedA || actual == expectedB, + label + " expected one of [" + expectedA + ", " + expectedB + "] but found [" + actual + "]"); + } + @Test(groups = { "integration" }) public void testIntegerTypes() throws SQLException { runQuery("CREATE TABLE test_integers (order Int8, " @@ -1153,11 +1158,11 @@ public void testFloatTypes() throws SQLException { try (Statement stmt = conn.createStatement()) { try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_floats ORDER BY order")) { assertTrue(rs.next()); - assertEquals(rs.getFloat("float32"), -3.402823E38f); + assertFloat32Boundary(rs.getFloat("float32"), -3.4028233E38f, -3.402823E38f, "float32 min"); assertEquals(rs.getDouble("float64"), Double.valueOf(-1.7976931348623157E308)); assertTrue(rs.next()); - assertEquals(rs.getFloat("float32"), Float.valueOf(3.402823E38f)); + assertFloat32Boundary(rs.getFloat("float32"), 3.4028233E38f, 3.402823E38f, "float32 max"); assertEquals(rs.getDouble("float64"), Double.valueOf(1.7976931348623157E308)); assertTrue(rs.next()); @@ -1174,11 +1179,13 @@ public void testFloatTypes() throws SQLException { try (Statement stmt = conn.createStatement()) { try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_floats ORDER BY order")) { assertTrue(rs.next()); - assertEquals(rs.getObject("float32"), -3.402823E38f); + assertFloat32Boundary(((Number) rs.getObject("float32")).floatValue(), -3.4028233E38f, -3.402823E38f, + "float32 min object"); assertEquals(rs.getObject("float64"), Double.valueOf(-1.7976931348623157E308)); assertTrue(rs.next()); - assertEquals(rs.getObject("float32"), 3.402823E38f); + assertFloat32Boundary(((Number) rs.getObject("float32")).floatValue(), 3.4028233E38f, 3.402823E38f, + "float32 max object"); assertEquals(rs.getObject("float64"), Double.valueOf(1.7976931348623157E308)); assertTrue(rs.next()); 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 575dde388..c42203cee 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java @@ -27,15 +27,9 @@ import java.util.Map; import java.util.Properties; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertNotNull; -import static org.testng.Assert.assertNull; -import static org.testng.Assert.assertSame; -import static org.testng.Assert.assertThrows; -import static org.testng.Assert.assertTrue; -import static org.testng.Assert.fail; +import static org.testng.Assert.*; @Test(groups = {"integration"}) @@ -386,14 +380,30 @@ public void testJdbcEscapeSyntax() throws Exception { @Test(groups = {"integration"}) public void testExecuteQueryTimeout() throws Exception { - try (Connection conn = getJdbcConnection()) { + Properties config = new Properties(); + config.setProperty(ClientConfigProperties.ASYNC_OPERATIONS.getKey(), "true"); + try (Connection conn = getJdbcConnection(config)) { try (Statement stmt = conn.createStatement()) { - stmt.setQueryTimeout(1); - assertThrows(SQLException.class, () -> { - try (ResultSet rs = stmt.executeQuery("SELECT sleep(5)")) { - assertFalse(rs.next()); + long woTimeoutStart = System.currentTimeMillis(); + final String query = "SELECT count(), sum(sipHash64(number)) " + + "FROM numbers(1000000000) " + + "SETTINGS max_threads = 1;"; + try (ResultSet rs = stmt.executeQuery(query)) { + assertTrue(rs.next()); + } + long woTimeoutTime = System.currentTimeMillis() - woTimeoutStart; + + int queryTimeoutMs = (int) (woTimeoutTime * 0.75); + stmt.setQueryTimeout((int) TimeUnit.MILLISECONDS.toSeconds(queryTimeoutMs)); + + long wTimeoutStart = System.currentTimeMillis(); + SQLException ex = expectThrows(SQLException.class, () -> { + try (ResultSet rs = stmt.executeQuery(query)) { + assertTrue(rs.next()); } }); + long wTimeoutTime = System.currentTimeMillis() - wTimeoutStart; + assertTrue(Math.abs(wTimeoutTime - queryTimeoutMs) < 1000); } } } diff --git a/jdbc-v2/src/test/resources/StatementSQLTests.yaml b/jdbc-v2/src/test/resources/StatementSQLTests.yaml index 579899421..d2b3dbc1c 100644 --- a/jdbc-v2/src/test/resources/StatementSQLTests.yaml +++ b/jdbc-v2/src/test/resources/StatementSQLTests.yaml @@ -77,12 +77,12 @@ - name: column_types expected: ["UInt64", "UInt64"] - name: explain_stmt_01 - query: EXPLAIN SELECT 1 + query: EXPLAIN AST SELECT 1 tables: events: datasets/empty_table checks: - name: row_count - expected: 2 + expected: 5 - name: column_count expected: 1 - name: column_names From 839fbfaae478b5505e80de3649c979832460b139 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Mon, 24 Aug 2026 23:24:16 -0700 Subject: [PATCH 2/5] now operations throw SQLTimeoutException when query exceed time limit --- .../com/clickhouse/jdbc/GenericJDBCTest.java | 2 +- .../com/clickhouse/jdbc/StatementImpl.java | 21 ++++-- .../com/clickhouse/jdbc/StatementTest.java | 71 ++++++++++++++++--- 3 files changed, 80 insertions(+), 14 deletions(-) diff --git a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/GenericJDBCTest.java b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/GenericJDBCTest.java index c9474c2f2..ec3b44791 100644 --- a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/GenericJDBCTest.java +++ b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/GenericJDBCTest.java @@ -27,7 +27,7 @@ public void connectionTest() throws SQLException { } } - @Test + @Test(enabled = false) // skipped to be removed after reviewing tests. public void connectionWithPropertiesTest() throws SQLException { Properties properties = new Properties(); properties.setProperty("user", "default"); 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 f50546393..f7a15617c 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java @@ -1,6 +1,7 @@ package com.clickhouse.jdbc; import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.ServerException; import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader; import com.clickhouse.client.api.internal.ServerSettings; import com.clickhouse.client.api.query.QueryResponse; @@ -13,15 +14,13 @@ import org.slf4j.LoggerFactory; import java.net.SocketTimeoutException; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.SQLWarning; -import java.sql.Statement; +import java.sql.*; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.function.Supplier; public class StatementImpl implements Statement, JdbcV2Wrapper { @@ -215,6 +214,7 @@ protected ResultSetImpl executeQueryImpl(String sql, QuerySettings settings) thr } handleSocketTimeoutException(e); onResultSetClosed(null); + throwOnExecutionTimeout(e, mergedSettings.getQueryId()); throw ExceptionUtils.toSqlState(e); } } @@ -225,6 +225,18 @@ protected void handleSocketTimeoutException(Exception e) { } } + protected void throwOnExecutionTimeout(Exception e, String queryId) throws SQLTimeoutException { + boolean shouldThrow = e instanceof TimeoutException; + ServerException se = e instanceof ServerException ? (ServerException) e : e.getCause() instanceof ServerException ? (ServerException) e.getCause() : null; + if (se != null && se.getCode() == ServerException.EXECUTION_TIMEOUT) { + shouldThrow = true; + } + + if (shouldThrow) { + throw new SQLTimeoutException("Query execution time exceeded limit (queryId=" + queryId + ", timeout = " + queryTimeout + "s)", e); + } + } + @Override public int executeUpdate(String sql) throws SQLException { ensureOpen(); @@ -251,6 +263,7 @@ protected long executeUpdateImpl(String sql, QuerySettings settings) throws SQLE lastQueryId = response.getQueryId(); } catch (Exception e) { handleSocketTimeoutException(e); + throwOnExecutionTimeout(e, mergedSettings.getQueryId()); throw ExceptionUtils.toSqlState(e); } 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 c42203cee..748f0e0e9 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java @@ -12,13 +12,7 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import java.sql.Array; -import java.sql.Connection; -import java.sql.Date; -import java.sql.ResultSet; -import java.sql.ResultSetMetaData; -import java.sql.SQLException; -import java.sql.Statement; +import java.sql.*; import java.time.LocalDate; import java.util.Arrays; import java.util.Collections; @@ -379,7 +373,7 @@ public void testJdbcEscapeSyntax() throws Exception { } @Test(groups = {"integration"}) - public void testExecuteQueryTimeout() throws Exception { + public void testExecuteQueryTimeoutAsyncOperation() throws Exception { Properties config = new Properties(); config.setProperty(ClientConfigProperties.ASYNC_OPERATIONS.getKey(), "true"); try (Connection conn = getJdbcConnection(config)) { @@ -397,7 +391,66 @@ public void testExecuteQueryTimeout() throws Exception { stmt.setQueryTimeout((int) TimeUnit.MILLISECONDS.toSeconds(queryTimeoutMs)); long wTimeoutStart = System.currentTimeMillis(); - SQLException ex = expectThrows(SQLException.class, () -> { + expectThrows(SQLTimeoutException.class, () -> { + try (ResultSet rs = stmt.executeQuery(query)) { + assertTrue(rs.next()); + } + }); + long wTimeoutTime = System.currentTimeMillis() - wTimeoutStart; + assertTrue(Math.abs(wTimeoutTime - queryTimeoutMs) < 1000); + } + } + } + + @Test(groups = {"integration"}) + public void testExecuteQueryTimeoutServerTimeout() throws Exception { + + long woTimeoutTime; + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + long woTimeoutStart = System.currentTimeMillis(); + final String query = "SELECT count(), sum(sipHash64(number)) " + + "FROM numbers(1000000000) " + + "SETTINGS max_threads = 1;"; + try (ResultSet rs = stmt.executeQuery(query)) { + assertTrue(rs.next()); + } + woTimeoutTime = System.currentTimeMillis() - woTimeoutStart; + } + } + + int queryTimeoutMs = (int) (woTimeoutTime * 0.75); + Properties config = new Properties(); + config.setProperty(ClientConfigProperties.serverSetting("max_execution_time"), String.valueOf(TimeUnit.MILLISECONDS.toSeconds(queryTimeoutMs))); + try (Connection conn = getJdbcConnection(config)) { + try (Statement stmt = conn.createStatement()) { + final String query = "SELECT count(), sum(sipHash64(number)) " + + "FROM numbers(1000000000) " + + "SETTINGS max_threads = 1;"; + + long wTimeoutStart = System.currentTimeMillis(); + expectThrows(SQLTimeoutException.class, () -> { + try (ResultSet rs = stmt.executeQuery(query)) { + assertTrue(rs.next()); + } + }); + long wTimeoutTime = System.currentTimeMillis() - wTimeoutStart; + assertTrue(Math.abs(wTimeoutTime - queryTimeoutMs) < 1000); + } + } + + // test async because it wraps exceptions + config = new Properties(); + config.setProperty(ClientConfigProperties.serverSetting("max_execution_time"), String.valueOf(TimeUnit.MILLISECONDS.toSeconds(queryTimeoutMs))); + config.setProperty(ClientConfigProperties.ASYNC_OPERATIONS.getKey(), "true"); + try (Connection conn = getJdbcConnection(config)) { + try (Statement stmt = conn.createStatement()) { + final String query = "SELECT count(), sum(sipHash64(number)) " + + "FROM numbers(1000000000) " + + "SETTINGS max_threads = 1;"; + + long wTimeoutStart = System.currentTimeMillis(); + expectThrows(SQLTimeoutException.class, () -> { try (ResultSet rs = stmt.executeQuery(query)) { assertTrue(rs.next()); } From 09fd2f23402957a18e2c1d3f49b7152ca05bdfe8 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 25 Aug 2026 13:11:48 -0700 Subject: [PATCH 3/5] made setQueryTimeout set max_execution_time setting if not async operations --- .../com/clickhouse/jdbc/ConnectionImpl.java | 14 +++ .../com/clickhouse/jdbc/StatementImpl.java | 30 ++++- .../com/clickhouse/jdbc/StatementTest.java | 117 ++++++++++++++++++ 3 files changed, 160 insertions(+), 1 deletion(-) diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java index 7328048ce..ffb7c8383 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java @@ -114,6 +114,20 @@ public ConnectionImpl(String url, Properties info) throws SQLException { .serverSetting(ServerSettings.ASYNC_INSERT, "0") .serverSetting(ServerSettings.WAIT_END_OF_QUERY, "0"); + String defaultQuerySettingsProp = config.getDriverProperty(DriverProperties.DEFAULT_QUERY_SETTINGS.getKey(), null); + if (defaultQuerySettingsProp != null) { + ClientConfigProperties.toKeyValuePairs(defaultQuerySettingsProp) + .forEach((k, v) -> this.defaultQuerySettings.serverSetting(k, v)); + } + Map clientProps = config.getClientProperties(); + for (Map.Entry entry : clientProps.entrySet()) { + if (entry.getKey() != null && entry.getValue() != null) { + if (entry.getKey().startsWith(ClientConfigProperties.SERVER_SETTING_PREFIX)) { + this.defaultQuerySettings.setOption(entry.getKey(), entry.getValue()); + } + } + } + this.metadata = new DatabaseMetaDataImpl(this, false, url); this.defaultCalendar = Calendar.getInstance(); 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 f7a15617c..cc81ea879 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java @@ -53,6 +53,8 @@ public class StatementImpl implements Statement, JdbcV2Wrapper { // settings local to a statement protected QuerySettings localSettings; + protected Integer connectionLvlExecTimeout; // to properly reset + public StatementImpl(ConnectionImpl connection) throws SQLException { this.connection = connection; @@ -66,6 +68,8 @@ public StatementImpl(ConnectionImpl connection) throws SQLException { this.escapeProcessingEnabled = true; this.featureManager = new FeatureManager(connection.getJdbcConfig()); this.queryIdGenerator = connection.getJdbcConfig().getQueryIdGenerator(); + + this.connectionLvlExecTimeout = connection.getDefaultQuerySettings().getMaxExecutionTime(); } protected void ensureOpen() throws SQLException { @@ -226,7 +230,7 @@ protected void handleSocketTimeoutException(Exception e) { } protected void throwOnExecutionTimeout(Exception e, String queryId) throws SQLTimeoutException { - boolean shouldThrow = e instanceof TimeoutException; + boolean shouldThrow = e instanceof TimeoutException || e.getCause() instanceof TimeoutException; ServerException se = e instanceof ServerException ? (ServerException) e : e.getCause() instanceof ServerException ? (ServerException) e.getCause() : null; if (se != null && se.getCode() == ServerException.EXECUTION_TIMEOUT) { shouldThrow = true; @@ -337,6 +341,30 @@ public int getQueryTimeout() throws SQLException { @Override public void setQueryTimeout(int seconds) throws SQLException { ensureOpen(); + if (seconds < 0) { + throw new SQLException("Timeout should be >= 0 but " + seconds + " was passed"); + } + + if (seconds > 0) { + boolean isAsyncEnabled; + try { + isAsyncEnabled = Boolean.parseBoolean( + getConnection().getClient().getConfiguration().getOrDefault(ClientConfigProperties.ASYNC_OPERATIONS.getKey(), + ClientConfigProperties.ASYNC_OPERATIONS.getDefaultValue())); + } catch (Exception e) { + LOG.error("Failed to read client configuration " + ClientConfigProperties.ASYNC_OPERATIONS.getKey(), e); + isAsyncEnabled = false; + } + + if (!isAsyncEnabled) { + // `max_execution_time` is only option when not async operations enabled + getLocalSettings().setMaxExecutionTime(seconds); + } + } else if (connectionLvlExecTimeout != null) { + getLocalSettings().setMaxExecutionTime(connectionLvlExecTimeout); + } else { + getLocalSettings().resetOption(ClientConfigProperties.serverSetting(ServerSettings.MAX_EXECUTION_TIME)); + } queryTimeout = seconds; } 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 748f0e0e9..e547fdd52 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java @@ -1429,6 +1429,123 @@ public static Object[][] testUnknownStatementTest_DP() { }; } + + private void assertQueryTimeout(Statement stmt, int expectedTimeoutSec) { + final String slowQuery = "SELECT count(), sum(sipHash64(number)) FROM numbers(1000000000) SETTINGS max_threads = 1;"; + long start = System.currentTimeMillis(); + expectThrows(SQLTimeoutException.class, () -> { + try (ResultSet rs = stmt.executeQuery(slowQuery)) { + assertTrue(rs.next()); + } + }); + long elapsed = System.currentTimeMillis() - start; + long expectedMs = expectedTimeoutSec * 1000L; + assertTrue(Math.abs(elapsed - expectedMs) < 1000, + "Expected timeout ~" + expectedMs + "ms, but execution took " + elapsed + "ms"); + } + + @Test(groups = {"integration"}) + public void testConnectionLevelExecutionTimeoutOverriddenByStatement() throws Exception { + Properties config = new Properties(); + final int connExecTimeout = 7; + config.setProperty(ClientConfigProperties.serverSetting(ServerSettings.MAX_EXECUTION_TIME), String.valueOf(connExecTimeout)); + try (Connection conn = getJdbcConnection(config); + StatementImpl stmt = (StatementImpl) conn.createStatement()) { + assertEquals(stmt.connectionLvlExecTimeout, Integer.valueOf(connExecTimeout)); + assertEquals(stmt.getLocalSettings().getMaxExecutionTime(), connExecTimeout); + assertEquals(stmt.getQueryTimeout(), 0); + assertQueryTimeout(stmt, connExecTimeout); + + final int stmtExecTimeout = 5; + stmt.setQueryTimeout(stmtExecTimeout); + assertEquals(stmt.getQueryTimeout(), stmtExecTimeout); + assertEquals(stmt.getLocalSettings().getMaxExecutionTime(), stmtExecTimeout); + assertQueryTimeout(stmt, stmtExecTimeout); + + // reset back to connection + stmt.setQueryTimeout(0); + assertEquals(stmt.getQueryTimeout(), 0); + assertEquals(stmt.getLocalSettings().getMaxExecutionTime(), connExecTimeout); + } + + config = new Properties(); + config.setProperty(DriverProperties.DEFAULT_QUERY_SETTINGS.getKey(), "max_execution_time=" + connExecTimeout); + try (Connection conn = getJdbcConnection(config); + StatementImpl stmt = (StatementImpl) conn.createStatement()) { + assertEquals(stmt.connectionLvlExecTimeout, connExecTimeout); + assertEquals(stmt.getLocalSettings().getMaxExecutionTime(), connExecTimeout); + } + } + + @Test(groups = {"integration"}) + public void testAsyncOperationsEnabledTimeout() throws Exception { + Properties config = new Properties(); + config.setProperty(ClientConfigProperties.ASYNC_OPERATIONS.getKey(), "true"); + try (Connection conn = getJdbcConnection(config); + StatementImpl stmt = (StatementImpl) conn.createStatement()) { + assertNull(stmt.connectionLvlExecTimeout); + assertNull(stmt.getLocalSettings().getMaxExecutionTime()); + assertEquals(stmt.getQueryTimeout(), 0); + + final int stmtExecTimeout = 5; + stmt.setQueryTimeout(stmtExecTimeout); + assertEquals(stmt.getQueryTimeout(), stmtExecTimeout); + assertNull(stmt.getLocalSettings().getMaxExecutionTime()); + assertQueryTimeout(stmt, stmtExecTimeout); + + stmt.setQueryTimeout(0); + assertEquals(stmt.getQueryTimeout(), 0); + assertNull(stmt.getLocalSettings().getMaxExecutionTime()); + } + } + + @Test(groups = {"integration"}) + public void testAsyncOperationsEnabledWithConnectionLevelTimeout() throws Exception { + Properties config = new Properties(); + final int connExecTimeout = 7; + config.setProperty(ClientConfigProperties.ASYNC_OPERATIONS.getKey(), "true"); + config.setProperty(ClientConfigProperties.serverSetting(ServerSettings.MAX_EXECUTION_TIME), String.valueOf(connExecTimeout)); + try (Connection conn = getJdbcConnection(config); + StatementImpl stmt = (StatementImpl) conn.createStatement()) { + assertEquals(stmt.connectionLvlExecTimeout, connExecTimeout); + assertEquals(stmt.getLocalSettings().getMaxExecutionTime(), connExecTimeout); + assertEquals(stmt.getQueryTimeout(), 0); + + assertQueryTimeout(stmt, connExecTimeout); + + int stmtExecTimeout = 5; + stmt.setQueryTimeout(stmtExecTimeout); + assertEquals(stmt.getQueryTimeout(), stmtExecTimeout); + assertEquals(stmt.getLocalSettings().getMaxExecutionTime(), connExecTimeout); + assertQueryTimeout(stmt, stmtExecTimeout); + + stmt.setQueryTimeout(0); + assertEquals(stmt.getQueryTimeout(), 0); + assertEquals(stmt.getLocalSettings().getMaxExecutionTime(), connExecTimeout); + } + } + + @Test(groups = {"integration"}) + public void testNoConnectionLevelTimeoutOverriddenAndReset() throws Exception { + try (Connection conn = getJdbcConnection(); + StatementImpl stmt = (StatementImpl) conn.createStatement()) { + assertNull(stmt.connectionLvlExecTimeout); + assertNull(stmt.getLocalSettings().getMaxExecutionTime()); + assertEquals(stmt.getQueryTimeout(), 0); + + stmt.setQueryTimeout(1); + assertEquals(stmt.getQueryTimeout(), 1); + assertEquals(stmt.getLocalSettings().getMaxExecutionTime(), Integer.valueOf(1)); + assertQueryTimeout(stmt, 1); + + stmt.setQueryTimeout(0); + assertEquals(stmt.getQueryTimeout(), 0); + assertNull(stmt.getLocalSettings().getMaxExecutionTime()); + + assertThrows(SQLException.class, () -> stmt.setQueryTimeout(-1)); + } + } + private static String getDBName(Statement stmt) throws SQLException { try (ResultSet rs = stmt.executeQuery("SELECT database()")) { rs.next(); From 60bb7e11c1ab649717c0bf364fdd9939aee4b1ba Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 25 Aug 2026 13:42:32 -0700 Subject: [PATCH 4/5] chor: cleanup & changelog update --- CHANGELOG.md | 7 +++++-- .../src/test/java/com/clickhouse/client/ClientTests.java | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53f1fd6de..778d132c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,12 @@ -## 0.9.8 +## 0.9.9 ### Bug Fixes - **[client-v2]** `ServerException` with code `159 Execution Timeout` is retried unconditionally. After the fix this -error treated as non-retriable. +error treated as non-retriable. (part of https://github.com/ClickHouse/clickhouse-java/issues/2637) +- **[jdbc-v2]** Fixes `Statement#setQueryTimeout`. By default, client executes query in calling thread and future timeout +has no effect. Fix makes `setQueryTimeout` to set `max_execution_time` server setting in this case to overcome limitation. + (https://github.com/ClickHouse/clickhouse-java/issues/2637) ## 0.9.8 diff --git a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java index 2de2e6aaa..526831c0c 100644 --- a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java @@ -566,7 +566,6 @@ public void testExecutionTimeout() throws Exception{ } catch (ServerException e) { long queryTime = System.currentTimeMillis() - startTime; - System.out.println(queryTime + " - query time"); Assert.assertTrue(Math.abs(queryTime - maxExecTime) < 1000); Assert.assertEquals(e.getCode(), ServerException.EXECUTION_TIMEOUT); } From 2284ce6c3ed273bf2f77dda7b7a2d949e456e898 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 25 Aug 2026 14:33:17 -0700 Subject: [PATCH 5/5] Removed timeout value from exception message --- jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 cc81ea879..27e5d3ba9 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java @@ -237,7 +237,7 @@ protected void throwOnExecutionTimeout(Exception e, String queryId) throws SQLTi } if (shouldThrow) { - throw new SQLTimeoutException("Query execution time exceeded limit (queryId=" + queryId + ", timeout = " + queryTimeout + "s)", e); + throw new SQLTimeoutException("Query execution time exceeded limit (queryId=" + queryId + ")", e); } }