Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,16 @@ of `NULL` was not set and read. (https://github.com/ClickHouse/clickhouse-java/i
and `getObject(column, Object.class)` and the no-type `getObject(column)` overloads now return a decoded `String`
instead of the internal holder.

## 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. (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

### Improvements
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

public static final int UNKNOWN_SETTING = 115;

public static final int EXECUTION_TIMEOUT = 159;

private final int code;

private final int transportProtocolCode;
Expand Down Expand Up @@ -56,10 +58,9 @@
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) {

Check failure on line 61 in client-v2/src/main/java/com/clickhouse/client/api/ServerException.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a default case to this switch.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AaDaYKqYc4VPrk66gl5o&open=AaDaYKqYc4VPrk66gl5o&pullRequest=3147
case 3: // UNEXPECTED_END_OF_FILE
case 107: // FILE_DOESNT_EXIST
case 159: // TIMEOUT_EXCEEDED
Comment thread
chernser marked this conversation as resolved.
case 164: // READONLY
case 202: // TOO_MANY_SIMULTANEOUS_QUERIES
case 203: // NO_FREE_CONNECTION
Expand Down
18 changes: 18 additions & 0 deletions client-v2/src/test/java/com/clickhouse/client/ClientTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,24 @@
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()) {

Check warning on line 721 in client-v2/src/test/java/com/clickhouse/client/ClientTests.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this block of code, fill it in, or add a comment explaining why it is empty.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AaDaYKuIc4VPrk66gl5p&open=AaDaYKuIc4VPrk66gl5p&pullRequest=3147

} catch (ServerException e) {
long queryTime = System.currentTimeMillis() - startTime;
Assert.assertTrue(Math.abs(queryTime - maxExecTime) < 1000);
Assert.assertEquals(e.getCode(), ServerException.EXECUTION_TIMEOUT);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timeout test can pass silently

Medium Severity

testExecutionTimeout only asserts inside catch (ServerException). If the query finishes without a timeout, or the failure is wrapped (for example ExecutionException), the test still passes and never checks EXECUTION_TIMEOUT or that code 159 is not retried.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b3769a7. Configure here.


@Test(groups = {"integration"})
public void testHostnameWithUnderscore() throws Exception {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,7 @@ public void testExecuteRequestThrowsConnectExceptionOn503() throws Exception {

@DataProvider(name = "serverExceptionRetryCases")
public static Object[][] serverExceptionRetryCases() {
// Server code 159 (TIMEOUT_EXCEEDED) is retryable; code 60 (TABLE_NOT_FOUND) is not.
ServerException retryable = new ServerException(159, "TIMEOUT_EXCEEDED", 500, "q1");
ServerException retryable = new ServerException(202, "TOO_MANY_SIMULTANEOUS_QUERIES", 500, "q1");
ServerException nonRetryable = new ServerException(60, "TABLE_NOT_FOUND", 404, "q2");
return new Object[][]{
// ServerException thrown directly (behaviour that already worked; pinned as contrast).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ void tearDown() {
meterRegistry.clear();
Metrics.globalRegistry.clear();
}

@Test(groups = {"integration"}, enabled = true)
public void testRegisterMetrics() throws Exception {
ClickHouseNode node = getServer(ClickHouseProtocol.HTTP);
Expand Down
14 changes: 14 additions & 0 deletions jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@

private final JsonParserFactory jsonParserFactory;

public ConnectionImpl(String url, Properties info) throws SQLException {

Check failure on line 79 in jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 20 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AaDaYKy1c4VPrk66gl5s&open=AaDaYKy1c4VPrk66gl5s&pullRequest=3147
try {
this.url = url;//Raw URL
this.config = new JdbcConfiguration(url, info);
Expand Down Expand Up @@ -122,6 +122,20 @@
this.schema = client.getDefaultDatabase();
this.defaultQuerySettings = new QuerySettings();

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<String, String> clientProps = config.getClientProperties();
for (Map.Entry<String, String> entry : clientProps.entrySet()) {
if (entry.getKey() != null && entry.getValue() != null) {
if (entry.getKey().startsWith(ClientConfigProperties.SERVER_SETTING_PREFIX)) {

Check warning on line 133 in jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Merge this if statement with the enclosing one.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AaDaYKy1c4VPrk66gl5r&open=AaDaYKy1c4VPrk66gl5r&pullRequest=3147
this.defaultQuerySettings.setOption(entry.getKey(), entry.getValue());
}
}
}

this.metadata = new DatabaseMetaDataImpl(this, false, url);
this.defaultCalendar = Calendar.getInstance();

Expand Down
49 changes: 45 additions & 4 deletions jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java
Original file line number Diff line number Diff line change
@@ -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.ClickHouseFormatReader;
import com.clickhouse.client.api.data_formats.JSONEachRowFormatReader;
import com.clickhouse.client.api.internal.ServerSettings;
Expand All @@ -15,17 +16,15 @@
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.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
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;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -66,6 +65,8 @@
// settings local to a statement
protected QuerySettings localSettings;

protected Integer connectionLvlExecTimeout; // to properly reset


public StatementImpl(ConnectionImpl connection) throws SQLException {
this.connection = connection;
Expand All @@ -79,6 +80,8 @@
this.escapeProcessingEnabled = true;
this.featureManager = new FeatureManager(connection.getJdbcConfig());
this.queryIdGenerator = connection.getJdbcConfig().getQueryIdGenerator();

this.connectionLvlExecTimeout = connection.getDefaultQuerySettings().getMaxExecutionTime();
}

protected void ensureOpen() throws SQLException {
Expand Down Expand Up @@ -360,6 +363,7 @@
}
handleSocketTimeoutException(e);
onResultSetClosed(null);
throwOnExecutionTimeout(e, mergedSettings.getQueryId());
throw ExceptionUtils.toSqlState(e);
}
}
Expand All @@ -370,6 +374,18 @@
}
}

protected void throwOnExecutionTimeout(Exception e, String queryId) throws SQLTimeoutException {
boolean shouldThrow = e instanceof TimeoutException || e.getCause() instanceof TimeoutException;
ServerException se = e instanceof ServerException ? (ServerException) e : e.getCause() instanceof ServerException ? (ServerException) e.getCause() : null;

Check warning on line 379 in jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AaDaYKx-c4VPrk66gl5q&open=AaDaYKx-c4VPrk66gl5q&pullRequest=3147
if (se != null && se.getCode() == ServerException.EXECUTION_TIMEOUT) {
shouldThrow = true;
}

if (shouldThrow) {
throw new SQLTimeoutException("Query execution time exceeded limit (queryId=" + queryId + ")", e);
}
}

@Override
public int executeUpdate(String sql) throws SQLException {
ensureOpen();
Expand All @@ -396,6 +412,7 @@
lastQueryId = response.getQueryId();
} catch (Exception e) {
handleSocketTimeoutException(e);
throwOnExecutionTimeout(e, mergedSettings.getQueryId());
throw ExceptionUtils.toSqlState(e);
}

Expand Down Expand Up @@ -469,6 +486,30 @@
@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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -794,26 +794,26 @@ public ResultSet getProcedureColumns(String catalog, String schemaPattern, Strin
static final Map<String, String> ENGINE_TO_TABLE_TYPE;
static {
Map<String, String> 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());
Expand Down Expand Up @@ -899,6 +899,18 @@ public ResultSet getProcedureColumns(String catalog, String schemaPattern, Strin
// Remote engines (appended 08/31/2026)
map.put("BigQuery", TableType.REMOTE_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());
Expand Down
Loading
Loading