From 5634dfed5a3bbefa2f4f1f7d31025502198a15e8 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Mon, 30 Mar 2026 10:45:01 -0700 Subject: [PATCH 1/4] Implemented core API for sessions --- .../com/clickhouse/client/api/Client.java | 48 +++++++ .../client/api/command/CommandSettings.java | 17 +++ .../client/api/http/ClickHouseHttpProto.java | 15 ++ .../client/api/insert/InsertSettings.java | 36 +++++ .../client/api/internal/CommonSettings.java | 32 +++++ .../client/api/query/QuerySettings.java | 36 +++++ .../clickhouse/client/HttpTransportTests.java | 131 ++++++++++++++++++ .../com/clickhouse/client/SettingsTests.java | 22 +++ 8 files changed, 337 insertions(+) diff --git a/client-v2/src/main/java/com/clickhouse/client/api/Client.java b/client-v2/src/main/java/com/clickhouse/client/api/Client.java index 73ec21155..5c0d52222 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/Client.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/Client.java @@ -955,6 +955,29 @@ public Builder serverSetting(String name, Collection values) { return this; } + /** + * Sets ClickHouse session id to be sent with each request. + */ + public Builder setSessionId(String sessionId) { + ValidationUtils.checkNonBlank(sessionId, ClickHouseHttpProto.QPARAM_SESSION_ID); + return serverSetting(ClickHouseHttpProto.QPARAM_SESSION_ID, sessionId); + } + + /** + * Sets ClickHouse session check flag to be sent with each request. + */ + public Builder setSessionCheck(boolean sessionCheck) { + return serverSetting(ClickHouseHttpProto.QPARAM_SESSION_CHECK, sessionCheck ? "1" : "0"); + } + + /** + * Sets ClickHouse session timeout in seconds to be sent with each request. + */ + public Builder setSessionTimeout(int timeoutInSeconds) { + ValidationUtils.checkPositive(timeoutInSeconds, ClickHouseHttpProto.QPARAM_SESSION_TIMEOUT); + return serverSetting(ClickHouseHttpProto.QPARAM_SESSION_TIMEOUT, String.valueOf(timeoutInSeconds)); + } + /** * Sets column to method matching strategy. It is used while registering POJO serializers and deserializers. * Default is {@link DefaultColumnToMethodMatchingStrategy}. @@ -2138,6 +2161,31 @@ public void updateClientName(String name) { this.configuration.put(ClientConfigProperties.CLIENT_NAME.getKey(), name); } + /** + * Updates ClickHouse session id for all subsequent requests created by this client. + */ + public void updateSessionId(String sessionId) { + ValidationUtils.checkNonBlank(sessionId, ClickHouseHttpProto.QPARAM_SESSION_ID); + this.configuration.put(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_ID), sessionId); + } + + /** + * Updates ClickHouse session check flag for all subsequent requests created by this client. + */ + public void updateSessionCheck(boolean sessionCheck) { + this.configuration.put(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_CHECK), + sessionCheck ? "1" : "0"); + } + + /** + * Updates ClickHouse session timeout (seconds) for all subsequent requests created by this client. + */ + public void updateSessionTimeout(int timeoutInSeconds) { + ValidationUtils.checkPositive(timeoutInSeconds, ClickHouseHttpProto.QPARAM_SESSION_TIMEOUT); + this.configuration.put(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_TIMEOUT), + String.valueOf(timeoutInSeconds)); + } + public static final String clientVersion = ClickHouseClientOption.readVersionFromResource("client-v2-version.properties"); public static final String CLIENT_USER_AGENT = "clickhouse-java-v2/"; diff --git a/client-v2/src/main/java/com/clickhouse/client/api/command/CommandSettings.java b/client-v2/src/main/java/com/clickhouse/client/api/command/CommandSettings.java index 552c424d5..48755bc14 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/command/CommandSettings.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/command/CommandSettings.java @@ -3,4 +3,21 @@ import com.clickhouse.client.api.query.QuerySettings; public class CommandSettings extends QuerySettings { + @Override + public CommandSettings setSessionId(String sessionId) { + super.setSessionId(sessionId); + return this; + } + + @Override + public CommandSettings setSessionCheck(boolean sessionCheck) { + super.setSessionCheck(sessionCheck); + return this; + } + + @Override + public CommandSettings setSessionTimeout(int timeoutInSeconds) { + super.setSessionTimeout(timeoutInSeconds); + return this; + } } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/http/ClickHouseHttpProto.java b/client-v2/src/main/java/com/clickhouse/client/api/http/ClickHouseHttpProto.java index 8075ede61..aa41329e8 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/http/ClickHouseHttpProto.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/http/ClickHouseHttpProto.java @@ -57,6 +57,21 @@ public class ClickHouseHttpProto { */ public static final String QPARAM_QUERY_ID = "query_id"; + /** + * Query parameter to specify a session id. + */ + public static final String QPARAM_SESSION_ID = "session_id"; + + /** + * Query parameter to check session status (1/0). + */ + public static final String QPARAM_SESSION_CHECK = "session_check"; + + /** + * Query parameter to specify session timeout in seconds. + */ + public static final String QPARAM_SESSION_TIMEOUT = "session_timeout"; + public static final String QPARAM_ROLE = "role"; /** diff --git a/client-v2/src/main/java/com/clickhouse/client/api/insert/InsertSettings.java b/client-v2/src/main/java/com/clickhouse/client/api/insert/InsertSettings.java index 29e24d508..d136c6cc6 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/insert/InsertSettings.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/insert/InsertSettings.java @@ -91,6 +91,42 @@ public InsertSettings setQueryId(String queryId) { return this; } + /** + * Sets ClickHouse session id for this operation. + */ + public InsertSettings setSessionId(String sessionId) { + settings.setSessionId(sessionId); + return this; + } + + public String getSessionId() { + return settings.getSessionId(); + } + + /** + * Sets ClickHouse session check flag for this operation. + */ + public InsertSettings setSessionCheck(boolean sessionCheck) { + settings.setSessionCheck(sessionCheck); + return this; + } + + public Boolean getSessionCheck() { + return settings.getSessionCheck(); + } + + /** + * Sets ClickHouse session timeout (seconds) for this operation. + */ + public InsertSettings setSessionTimeout(int timeoutInSeconds) { + settings.setSessionTimeout(timeoutInSeconds); + return this; + } + + public Integer getSessionTimeout() { + return settings.getSessionTimeout(); + } + public int getInputStreamCopyBufferSize() { return this.inputStreamCopyBufferSize; } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/CommonSettings.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/CommonSettings.java index f6365538d..14eb8a166 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/internal/CommonSettings.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/CommonSettings.java @@ -2,6 +2,7 @@ import com.clickhouse.client.api.Client; import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.http.ClickHouseHttpProto; import java.time.Duration; import java.time.temporal.ChronoUnit; @@ -90,6 +91,37 @@ public CommonSettings setQueryId(String queryId) { return this; } + public CommonSettings setSessionId(String sessionId) { + ValidationUtils.checkNonBlank(sessionId, ClickHouseHttpProto.QPARAM_SESSION_ID); + serverSetting(ClickHouseHttpProto.QPARAM_SESSION_ID, sessionId); + return this; + } + + public String getSessionId() { + return (String) settings.get(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_ID)); + } + + public CommonSettings setSessionCheck(boolean sessionCheck) { + serverSetting(ClickHouseHttpProto.QPARAM_SESSION_CHECK, sessionCheck ? "1" : "0"); + return this; + } + + public Boolean getSessionCheck() { + String value = (String) settings.get(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_CHECK)); + return value == null ? null : ("1".equals(value) || Boolean.parseBoolean(value)); + } + + public CommonSettings setSessionTimeout(int timeoutInSeconds) { + ValidationUtils.checkPositive(timeoutInSeconds, ClickHouseHttpProto.QPARAM_SESSION_TIMEOUT); + serverSetting(ClickHouseHttpProto.QPARAM_SESSION_TIMEOUT, String.valueOf(timeoutInSeconds)); + return this; + } + + public Integer getSessionTimeout() { + String value = (String) settings.get(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_TIMEOUT)); + return value == null ? null : Integer.valueOf(value); + } + /** * Operation id. Used internally to register new operation. * Should not be called directly. diff --git a/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java b/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java index 8c19a107c..4fc4b734f 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java @@ -85,6 +85,42 @@ public String getQueryId() { return settings.getQueryId(); } + /** + * Sets ClickHouse session id for this operation. + */ + public QuerySettings setSessionId(String sessionId) { + settings.setSessionId(sessionId); + return this; + } + + public String getSessionId() { + return settings.getSessionId(); + } + + /** + * Sets ClickHouse session check flag for this operation. + */ + public QuerySettings setSessionCheck(boolean sessionCheck) { + settings.setSessionCheck(sessionCheck); + return this; + } + + public Boolean getSessionCheck() { + return settings.getSessionCheck(); + } + + /** + * Sets ClickHouse session timeout (seconds) for this operation. + */ + public QuerySettings setSessionTimeout(int timeoutInSeconds) { + settings.setSessionTimeout(timeoutInSeconds); + return this; + } + + public Integer getSessionTimeout() { + return settings.getSessionTimeout(); + } + /** * Read buffer is used for reading data from a server. Size is in bytes. * Minimal value is {@value MINIMAL_READ_BUFFER_SIZE} bytes. diff --git a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java index fc2d13a86..8e5790a2e 100644 --- a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java @@ -11,6 +11,7 @@ import com.clickhouse.client.api.command.CommandSettings; import com.clickhouse.client.api.enums.Protocol; import com.clickhouse.client.api.enums.ProxyType; +import com.clickhouse.client.api.insert.InsertSettings; import com.clickhouse.client.api.insert.InsertResponse; import com.clickhouse.client.api.internal.DataTypeConverter; import com.clickhouse.client.api.internal.ServerSettings; @@ -603,6 +604,83 @@ public void testServerSettings() { } } + @Test(groups = { "integration" }) + public void testSessionSettingsClientAndOperationLevels() { + if (isCloud()) { + return; // mocked server + } + + int serverPort = new Random().nextInt(1000) + 10000; + WireMockServer mockServer = new WireMockServer(WireMockConfiguration + .options().port(serverPort).notifier(new ConsoleNotifier(false))); + mockServer.start(); + + try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost", mockServer.port(), false) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .setSessionId("client-session") + .setSessionCheck(false) + .setSessionTimeout(60) + .compressClientRequest(false) + .build()) { + mockServer.addStubMapping(WireMock.post(WireMock.anyUrl()) + .withQueryParam("session_id", WireMock.equalTo("query-session")) + .withQueryParam("session_check", WireMock.equalTo("1")) + .withQueryParam("session_timeout", WireMock.equalTo("15")) + .willReturn(WireMock.aResponse() + .withHeader("X-ClickHouse-Summary", + "{ \"read_bytes\": \"10\", \"read_rows\": \"1\"}")).build()); + + QuerySettings querySettings = new QuerySettings() + .setSessionId("query-session") + .setSessionCheck(true) + .setSessionTimeout(15); + try (QueryResponse response = client.query("SELECT 1", querySettings).get(1, TimeUnit.SECONDS)) { + Assert.assertEquals(response.getReadBytes(), 10); + } + + mockServer.resetRequests(); + mockServer.resetMappings(); + mockServer.addStubMapping(WireMock.post(WireMock.anyUrl()) + .withQueryParam("session_id", WireMock.equalTo("client-session")) + .withQueryParam("session_check", WireMock.equalTo("0")) + .withQueryParam("session_timeout", WireMock.equalTo("60")) + .willReturn(WireMock.aResponse() + .withHeader("X-ClickHouse-Summary", + "{ \"read_bytes\": \"11\", \"read_rows\": \"1\"}")).build()); + + try (CommandResponse response = client.execute("SELECT 1").get(1, TimeUnit.SECONDS)) { + Assert.assertEquals(response.getReadBytes(), 11); + } + + mockServer.resetRequests(); + mockServer.resetMappings(); + mockServer.addStubMapping(WireMock.post(WireMock.anyUrl()) + .withQueryParam("session_id", WireMock.equalTo("insert-session")) + .withQueryParam("session_check", WireMock.equalTo("0")) + .withQueryParam("session_timeout", WireMock.equalTo("90")) + .willReturn(WireMock.aResponse() + .withHeader("X-ClickHouse-Summary", + "{ \"read_bytes\": \"12\", \"read_rows\": \"1\"}")).build()); + + InsertSettings insertSettings = new InsertSettings() + .setSessionId("insert-session") + .setSessionCheck(false) + .setSessionTimeout(90); + try (InsertResponse response = client.insert( + "test_table", + new ByteArrayInputStream("1\n".getBytes(StandardCharsets.UTF_8)), + ClickHouseFormat.CSV, + insertSettings).get(1, TimeUnit.SECONDS)) { + Assert.assertEquals(response.getReadBytes(), 12); + } + } catch (Exception e) { + Assert.fail("Unexpected exception", e); + } finally { + mockServer.stop(); + } + } + static { System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "DEBUG"); } @@ -1003,6 +1081,59 @@ public void testBearerTokenAuth() throws Exception { } } + @Test(groups = { "integration" }) + public void testUpdateSessionSettingsAtRuntime() throws Exception { + if (isCloud()) { + return; // mocked server + } + + int serverPort = new Random().nextInt(1000) + 10000; + WireMockServer mockServer = new WireMockServer(WireMockConfiguration + .options().port(serverPort).notifier(new ConsoleNotifier(false))); + mockServer.start(); + + try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost", mockServer.port(), false) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .setSessionId("session-initial") + .setSessionCheck(false) + .setSessionTimeout(60) + .compressServerResponse(false) + .build()) { + + mockServer.addStubMapping(WireMock.post(WireMock.anyUrl()) + .withQueryParam("session_id", WireMock.equalTo("session-initial")) + .withQueryParam("session_check", WireMock.equalTo("0")) + .withQueryParam("session_timeout", WireMock.equalTo("60")) + .willReturn(WireMock.aResponse() + .withHeader("X-ClickHouse-Summary", + "{ \"read_bytes\": \"10\", \"read_rows\": \"1\"}")).build()); + + try (CommandResponse response = client.execute("SELECT 1").get(1, TimeUnit.SECONDS)) { + Assert.assertEquals(response.getReadBytes(), 10); + } + + mockServer.resetAll(); + mockServer.addStubMapping(WireMock.post(WireMock.anyUrl()) + .withQueryParam("session_id", WireMock.equalTo("session-updated")) + .withQueryParam("session_check", WireMock.equalTo("1")) + .withQueryParam("session_timeout", WireMock.equalTo("30")) + .willReturn(WireMock.aResponse() + .withHeader("X-ClickHouse-Summary", + "{ \"read_bytes\": \"11\", \"read_rows\": \"1\"}")).build()); + + client.updateSessionId("session-updated"); + client.updateSessionCheck(true); + client.updateSessionTimeout(30); + + try (CommandResponse response = client.execute("SELECT 1").get(1, TimeUnit.SECONDS)) { + Assert.assertEquals(response.getReadBytes(), 11); + } + } finally { + mockServer.stop(); + } + } + @Test(groups = { "integration" }) public void testJWTWithCloud() throws Exception { if (!isCloud()) { diff --git a/client-v2/src/test/java/com/clickhouse/client/SettingsTests.java b/client-v2/src/test/java/com/clickhouse/client/SettingsTests.java index 688da4a83..c0d51ed1e 100644 --- a/client-v2/src/test/java/com/clickhouse/client/SettingsTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/SettingsTests.java @@ -114,6 +114,17 @@ void testQuerySettingsSpecific() throws Exception { settings.setNetworkTimeout(10, ChronoUnit.SECONDS); Assert.assertEquals(settings.getNetworkTimeout(), TimeUnit.SECONDS.toMillis(10)); } + + { + final QuerySettings settings = new QuerySettings(); + settings.setSessionId("session-1"); + settings.setSessionCheck(true); + settings.setSessionTimeout(30); + Assert.assertEquals(settings.getSessionId(), "session-1"); + Assert.assertTrue(settings.getSessionCheck()); + Assert.assertEquals(settings.getSessionTimeout().intValue(), 30); + Assert.assertThrows(IllegalArgumentException.class, () -> settings.setSessionTimeout(0)); + } } @Test @@ -175,5 +186,16 @@ public void testInsertSettingsSpecific() throws Exception { settings.setNetworkTimeout(10, ChronoUnit.SECONDS); Assert.assertEquals(settings.getNetworkTimeout(), TimeUnit.SECONDS.toMillis(10)); } + + { + final InsertSettings settings = new InsertSettings(); + settings.setSessionId("session-2"); + settings.setSessionCheck(false); + settings.setSessionTimeout(45); + Assert.assertEquals(settings.getSessionId(), "session-2"); + Assert.assertFalse(settings.getSessionCheck()); + Assert.assertEquals(settings.getSessionTimeout().intValue(), 45); + Assert.assertThrows(IllegalArgumentException.class, () -> settings.setSessionTimeout(-1)); + } } } From 22ab21b8689cf019edd17370d5f1c8e3bd73f28d Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Wed, 1 Apr 2026 15:16:56 -0700 Subject: [PATCH 2/4] added timezone method and more tests --- .../com/clickhouse/client/api/Client.java | 23 ++++++++- .../client/api/command/CommandSettings.java | 6 +++ .../client/api/http/ClickHouseHttpProto.java | 5 ++ .../client/api/insert/InsertSettings.java | 12 +++++ .../client/api/internal/CommonSettings.java | 10 ++++ .../client/api/query/QuerySettings.java | 12 +++++ .../com/clickhouse/client/ClientTests.java | 51 +++++++++++++++++++ .../clickhouse/client/HttpTransportTests.java | 20 +++++--- .../com/clickhouse/client/SettingsTests.java | 6 +++ docs/features.md | 2 +- 10 files changed, 138 insertions(+), 9 deletions(-) diff --git a/client-v2/src/main/java/com/clickhouse/client/api/Client.java b/client-v2/src/main/java/com/clickhouse/client/api/Client.java index 5c0d52222..4444a8aeb 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/Client.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/Client.java @@ -145,7 +145,7 @@ public class Client implements AutoCloseable { private Client(Collection endpoints, Map configuration, ExecutorService sharedOperationExecutor, ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy, Object metricsRegistry, Supplier queryIdGenerator) { - this.configuration = ClientConfigProperties.parseConfigMap(configuration); + this.configuration = Collections.synchronizedMap(ClientConfigProperties.parseConfigMap(configuration)); this.readOnlyConfig = Collections.unmodifiableMap(configuration); this.metricsRegistry = metricsRegistry; this.queryIdGenerator = queryIdGenerator; @@ -978,6 +978,14 @@ public Builder setSessionTimeout(int timeoutInSeconds) { return serverSetting(ClickHouseHttpProto.QPARAM_SESSION_TIMEOUT, String.valueOf(timeoutInSeconds)); } + /** + * Sets ClickHouse session timezone to be sent with each request. + */ + public Builder setSessionTimezone(String timezone) { + ValidationUtils.checkNonBlank(timezone, ClickHouseHttpProto.QPARAM_SESSION_TIMEZONE); + return serverSetting(ClickHouseHttpProto.QPARAM_SESSION_TIMEZONE, timezone); + } + /** * Sets column to method matching strategy. It is used while registering POJO serializers and deserializers. * Default is {@link DefaultColumnToMethodMatchingStrategy}. @@ -2186,6 +2194,15 @@ public void updateSessionTimeout(int timeoutInSeconds) { String.valueOf(timeoutInSeconds)); } + /** + * Updates ClickHouse session timezone for all subsequent requests created by this client. + */ + public void updateSessionTimezone(String timezone) { + ValidationUtils.checkNonBlank(timezone, ClickHouseHttpProto.QPARAM_SESSION_TIMEZONE); + this.configuration.put(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_TIMEZONE), + timezone); + } + public static final String clientVersion = ClickHouseClientOption.readVersionFromResource("client-v2-version.properties"); public static final String CLIENT_USER_AGENT = "clickhouse-java-v2/"; @@ -2219,7 +2236,9 @@ private Endpoint getNextAliveNode() { */ private Map buildRequestSettings(Map opSettings) { Map requestSettings = new HashMap<>(); - requestSettings.putAll(configuration); + synchronized (configuration) { + requestSettings.putAll(configuration); + } requestSettings.putAll(opSettings); return requestSettings; } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/command/CommandSettings.java b/client-v2/src/main/java/com/clickhouse/client/api/command/CommandSettings.java index 48755bc14..042bc5a1d 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/command/CommandSettings.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/command/CommandSettings.java @@ -20,4 +20,10 @@ public CommandSettings setSessionTimeout(int timeoutInSeconds) { super.setSessionTimeout(timeoutInSeconds); return this; } + + @Override + public CommandSettings setSessionTimezone(String timezone) { + super.setSessionTimezone(timezone); + return this; + } } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/http/ClickHouseHttpProto.java b/client-v2/src/main/java/com/clickhouse/client/api/http/ClickHouseHttpProto.java index aa41329e8..7b76dde91 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/http/ClickHouseHttpProto.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/http/ClickHouseHttpProto.java @@ -72,6 +72,11 @@ public class ClickHouseHttpProto { */ public static final String QPARAM_SESSION_TIMEOUT = "session_timeout"; + /** + * Query parameter to specify session timezone. + */ + public static final String QPARAM_SESSION_TIMEZONE = "session_timezone"; + public static final String QPARAM_ROLE = "role"; /** diff --git a/client-v2/src/main/java/com/clickhouse/client/api/insert/InsertSettings.java b/client-v2/src/main/java/com/clickhouse/client/api/insert/InsertSettings.java index d136c6cc6..6d5ab49bb 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/insert/InsertSettings.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/insert/InsertSettings.java @@ -127,6 +127,18 @@ public Integer getSessionTimeout() { return settings.getSessionTimeout(); } + /** + * Sets ClickHouse session timezone for this operation. + */ + public InsertSettings setSessionTimezone(String timezone) { + settings.setSessionTimezone(timezone); + return this; + } + + public String getSessionTimezone() { + return settings.getSessionTimezone(); + } + public int getInputStreamCopyBufferSize() { return this.inputStreamCopyBufferSize; } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/CommonSettings.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/CommonSettings.java index 14eb8a166..5aafa9191 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/internal/CommonSettings.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/CommonSettings.java @@ -122,6 +122,16 @@ public Integer getSessionTimeout() { return value == null ? null : Integer.valueOf(value); } + public CommonSettings setSessionTimezone(String timezone) { + ValidationUtils.checkNonBlank(timezone, ClickHouseHttpProto.QPARAM_SESSION_TIMEZONE); + serverSetting(ClickHouseHttpProto.QPARAM_SESSION_TIMEZONE, timezone); + return this; + } + + public String getSessionTimezone() { + return (String) settings.get(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_TIMEZONE)); + } + /** * Operation id. Used internally to register new operation. * Should not be called directly. diff --git a/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java b/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java index 4fc4b734f..5fa5150f8 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java @@ -121,6 +121,18 @@ public Integer getSessionTimeout() { return settings.getSessionTimeout(); } + /** + * Sets ClickHouse session timezone for this operation. + */ + public QuerySettings setSessionTimezone(String timezone) { + settings.setSessionTimezone(timezone); + return this; + } + + public String getSessionTimezone() { + return settings.getSessionTimezone(); + } + /** * Read buffer is used for reading data from a server. Size is in bytes. * Minimal value is {@value MINIMAL_READ_BUFFER_SIZE} bytes. 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..d24b9ac42 100644 --- a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java @@ -7,6 +7,7 @@ import com.clickhouse.client.api.ClientMisconfigurationException; import com.clickhouse.client.api.ConnectionReuseStrategy; import com.clickhouse.client.api.ServerException; +import com.clickhouse.client.api.command.CommandSettings; import com.clickhouse.client.api.enums.Protocol; import com.clickhouse.client.api.insert.InsertSettings; import com.clickhouse.client.api.internal.ClickHouseLZ4OutputStream; @@ -144,6 +145,46 @@ public void testRawSettings() { } } + @Test(groups = {"integration"}) + public void testTemporaryTablesAreBoundToSession() throws Exception { + if (isCloud()) { + return; // HTTP sessions require server affinity + } + + String session1 = "session_1_" + UUID.randomUUID().toString().replace("-", ""); + String session2 = "session_2_" + UUID.randomUUID().toString().replace("-", ""); + String table1 = "tmp_session_1_" + UUID.randomUUID().toString().replace("-", ""); + String table2 = "tmp_session_2_" + UUID.randomUUID().toString().replace("-", ""); + + CommandSettings session1CommandSettings = new CommandSettings() + .setSessionId(session1) + .setSessionTimeout(60); + CommandSettings session2CommandSettings = new CommandSettings() + .setSessionId(session2) + .setSessionTimeout(60); + QuerySettings session1QuerySettings = new QuerySettings() + .setSessionId(session1) + .setSessionTimeout(60); + QuerySettings session2QuerySettings = new QuerySettings() + .setSessionId(session2) + .setSessionTimeout(60); + + try (Client client = newClient().build()) { + client.execute("CREATE TEMPORARY TABLE " + table1 + " (value UInt8)", session1CommandSettings).get().close(); + client.execute("INSERT INTO " + table1 + " VALUES (1)", session1CommandSettings).get().close(); + client.execute("CREATE TEMPORARY TABLE " + table2 + " (value UInt8)", session2CommandSettings).get().close(); + client.execute("INSERT INTO " + table2 + " VALUES (2)", session2CommandSettings).get().close(); + + Assert.assertEquals(client.queryAll("SELECT value FROM " + table1, session1QuerySettings).get(0).getInteger(1), + Integer.valueOf(1)); + Assert.assertEquals(client.queryAll("SELECT value FROM " + table2, session2QuerySettings).get(0).getInteger(1), + Integer.valueOf(2)); + + assertTableNotFoundInSession(client, table2, session1QuerySettings); + assertTableNotFoundInSession(client, table1, session2QuerySettings); + } + } + @Test(groups = {"integration"}) public void testCustomSettings() { if (isCloud()) { @@ -574,6 +615,16 @@ public boolean isVersionMatch(String versionExpression, Client client) { return ClickHouseVersion.of(serverVersion.get(0).getString(1)).check(versionExpression); } + private void assertTableNotFoundInSession(Client client, String tableName, QuerySettings settings) { + try { + client.queryAll("SELECT * FROM " + tableName, settings); + Assert.fail("Expected table to be unavailable in session: " + tableName); + } catch (ClientException e) { + Assert.assertTrue(e.getCause() instanceof ServerException, "Expected ServerException but got " + e.getCause()); + Assert.assertEquals(((ServerException) e.getCause()).getCode(), ServerException.TABLE_NOT_FOUND); + } + } + protected Client.Builder newClient() { ClickHouseNode node = getServer(ClickHouseProtocol.HTTP); boolean isSecure = isCloud(); diff --git a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java index 8e5790a2e..6c5423fb8 100644 --- a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java @@ -610,9 +610,8 @@ public void testSessionSettingsClientAndOperationLevels() { return; // mocked server } - int serverPort = new Random().nextInt(1000) + 10000; WireMockServer mockServer = new WireMockServer(WireMockConfiguration - .options().port(serverPort).notifier(new ConsoleNotifier(false))); + .options().dynamicPort().notifier(new ConsoleNotifier(false))); mockServer.start(); try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost", mockServer.port(), false) @@ -621,12 +620,14 @@ public void testSessionSettingsClientAndOperationLevels() { .setSessionId("client-session") .setSessionCheck(false) .setSessionTimeout(60) + .setSessionTimezone("Europe/London") .compressClientRequest(false) .build()) { mockServer.addStubMapping(WireMock.post(WireMock.anyUrl()) .withQueryParam("session_id", WireMock.equalTo("query-session")) .withQueryParam("session_check", WireMock.equalTo("1")) .withQueryParam("session_timeout", WireMock.equalTo("15")) + .withQueryParam("session_timezone", WireMock.equalTo("Asia/Tokyo")) .willReturn(WireMock.aResponse() .withHeader("X-ClickHouse-Summary", "{ \"read_bytes\": \"10\", \"read_rows\": \"1\"}")).build()); @@ -634,7 +635,8 @@ public void testSessionSettingsClientAndOperationLevels() { QuerySettings querySettings = new QuerySettings() .setSessionId("query-session") .setSessionCheck(true) - .setSessionTimeout(15); + .setSessionTimeout(15) + .setSessionTimezone("Asia/Tokyo"); try (QueryResponse response = client.query("SELECT 1", querySettings).get(1, TimeUnit.SECONDS)) { Assert.assertEquals(response.getReadBytes(), 10); } @@ -645,6 +647,7 @@ public void testSessionSettingsClientAndOperationLevels() { .withQueryParam("session_id", WireMock.equalTo("client-session")) .withQueryParam("session_check", WireMock.equalTo("0")) .withQueryParam("session_timeout", WireMock.equalTo("60")) + .withQueryParam("session_timezone", WireMock.equalTo("Europe/London")) .willReturn(WireMock.aResponse() .withHeader("X-ClickHouse-Summary", "{ \"read_bytes\": \"11\", \"read_rows\": \"1\"}")).build()); @@ -659,6 +662,7 @@ public void testSessionSettingsClientAndOperationLevels() { .withQueryParam("session_id", WireMock.equalTo("insert-session")) .withQueryParam("session_check", WireMock.equalTo("0")) .withQueryParam("session_timeout", WireMock.equalTo("90")) + .withQueryParam("session_timezone", WireMock.equalTo("America/Denver")) .willReturn(WireMock.aResponse() .withHeader("X-ClickHouse-Summary", "{ \"read_bytes\": \"12\", \"read_rows\": \"1\"}")).build()); @@ -666,7 +670,8 @@ public void testSessionSettingsClientAndOperationLevels() { InsertSettings insertSettings = new InsertSettings() .setSessionId("insert-session") .setSessionCheck(false) - .setSessionTimeout(90); + .setSessionTimeout(90) + .setSessionTimezone("America/Denver"); try (InsertResponse response = client.insert( "test_table", new ByteArrayInputStream("1\n".getBytes(StandardCharsets.UTF_8)), @@ -1087,9 +1092,8 @@ public void testUpdateSessionSettingsAtRuntime() throws Exception { return; // mocked server } - int serverPort = new Random().nextInt(1000) + 10000; WireMockServer mockServer = new WireMockServer(WireMockConfiguration - .options().port(serverPort).notifier(new ConsoleNotifier(false))); + .options().dynamicPort().notifier(new ConsoleNotifier(false))); mockServer.start(); try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost", mockServer.port(), false) @@ -1098,6 +1102,7 @@ public void testUpdateSessionSettingsAtRuntime() throws Exception { .setSessionId("session-initial") .setSessionCheck(false) .setSessionTimeout(60) + .setSessionTimezone("UTC") .compressServerResponse(false) .build()) { @@ -1105,6 +1110,7 @@ public void testUpdateSessionSettingsAtRuntime() throws Exception { .withQueryParam("session_id", WireMock.equalTo("session-initial")) .withQueryParam("session_check", WireMock.equalTo("0")) .withQueryParam("session_timeout", WireMock.equalTo("60")) + .withQueryParam("session_timezone", WireMock.equalTo("UTC")) .willReturn(WireMock.aResponse() .withHeader("X-ClickHouse-Summary", "{ \"read_bytes\": \"10\", \"read_rows\": \"1\"}")).build()); @@ -1118,6 +1124,7 @@ public void testUpdateSessionSettingsAtRuntime() throws Exception { .withQueryParam("session_id", WireMock.equalTo("session-updated")) .withQueryParam("session_check", WireMock.equalTo("1")) .withQueryParam("session_timeout", WireMock.equalTo("30")) + .withQueryParam("session_timezone", WireMock.equalTo("Asia/Novosibirsk")) .willReturn(WireMock.aResponse() .withHeader("X-ClickHouse-Summary", "{ \"read_bytes\": \"11\", \"read_rows\": \"1\"}")).build()); @@ -1125,6 +1132,7 @@ public void testUpdateSessionSettingsAtRuntime() throws Exception { client.updateSessionId("session-updated"); client.updateSessionCheck(true); client.updateSessionTimeout(30); + client.updateSessionTimezone("Asia/Novosibirsk"); try (CommandResponse response = client.execute("SELECT 1").get(1, TimeUnit.SECONDS)) { Assert.assertEquals(response.getReadBytes(), 11); diff --git a/client-v2/src/test/java/com/clickhouse/client/SettingsTests.java b/client-v2/src/test/java/com/clickhouse/client/SettingsTests.java index c0d51ed1e..c781d5245 100644 --- a/client-v2/src/test/java/com/clickhouse/client/SettingsTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/SettingsTests.java @@ -120,10 +120,13 @@ void testQuerySettingsSpecific() throws Exception { settings.setSessionId("session-1"); settings.setSessionCheck(true); settings.setSessionTimeout(30); + settings.setSessionTimezone("Asia/Tokyo"); Assert.assertEquals(settings.getSessionId(), "session-1"); Assert.assertTrue(settings.getSessionCheck()); Assert.assertEquals(settings.getSessionTimeout().intValue(), 30); + Assert.assertEquals(settings.getSessionTimezone(), "Asia/Tokyo"); Assert.assertThrows(IllegalArgumentException.class, () -> settings.setSessionTimeout(0)); + Assert.assertThrows(IllegalArgumentException.class, () -> settings.setSessionTimezone("")); } } @@ -192,10 +195,13 @@ public void testInsertSettingsSpecific() throws Exception { settings.setSessionId("session-2"); settings.setSessionCheck(false); settings.setSessionTimeout(45); + settings.setSessionTimezone("Europe/Paris"); Assert.assertEquals(settings.getSessionId(), "session-2"); Assert.assertFalse(settings.getSessionCheck()); Assert.assertEquals(settings.getSessionTimeout().intValue(), 45); + Assert.assertEquals(settings.getSessionTimezone(), "Europe/Paris"); Assert.assertThrows(IllegalArgumentException.class, () -> settings.setSessionTimeout(-1)); + Assert.assertThrows(IllegalArgumentException.class, () -> settings.setSessionTimezone("")); } } } diff --git a/docs/features.md b/docs/features.md index 858ff603d..0832924da 100644 --- a/docs/features.md +++ b/docs/features.md @@ -10,7 +10,7 @@ This document lists stable, user-visible behavior in `client-v2` and `jdbc-v2` t - Proxy support: Can send requests through configured HTTP proxies, including proxy credentials. - Connection and socket tuning: Exposes pool sizing, keep-alive, reuse strategy, connect/request/socket timeouts, and low-level socket options. - Query execution: Executes SQL asynchronously and returns streaming query responses with response metadata and metrics. -- Query settings: Supports per-query database selection, output format, execution limits, roles, log comments, headers, server settings, and network timeout overrides. +- Query settings: Supports per-query database selection, output format, execution limits, roles, log comments, headers, session settings, server settings, and network timeout overrides. - Parameterized SQL: Accepts named query parameters and can send them through supported HTTP request encodings. - Result materialization helpers: Provides streaming `Records`, generic row access, and convenience APIs that materialize all rows into generic records or typed POJOs. - Binary format readers: Reads ClickHouse binary result formats including `Native`, `RowBinary`, `RowBinaryWithNames`, and `RowBinaryWithNamesAndTypes`. From 1d6855070317e75c4f70bb9204d78785ef9b6a0f Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Thu, 2 Apr 2026 12:40:33 -0700 Subject: [PATCH 3/4] Formalized session and separate object to hold session specific config --- .../com/clickhouse/client/api/Client.java | 39 ++----- .../com/clickhouse/client/api/Session.java | 100 ++++++++++++++++++ .../client/api/command/CommandSettings.java | 7 ++ .../client/api/insert/InsertSettings.java | 6 ++ .../client/api/internal/CommonSettings.java | 7 ++ .../client/api/query/QuerySettings.java | 6 ++ .../com/clickhouse/client/ClientTests.java | 29 +++++ .../clickhouse/client/HttpTransportTests.java | 28 +++-- .../com/clickhouse/client/SettingsTests.java | 29 +++++ 9 files changed, 210 insertions(+), 41 deletions(-) create mode 100644 client-v2/src/main/java/com/clickhouse/client/api/Session.java diff --git a/client-v2/src/main/java/com/clickhouse/client/api/Client.java b/client-v2/src/main/java/com/clickhouse/client/api/Client.java index 4444a8aeb..15883e1fa 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/Client.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/Client.java @@ -117,6 +117,7 @@ public class Client implements AutoCloseable { private final List endpoints; private final Map configuration; + private final Session session; private final Map readOnlyConfig; @@ -145,7 +146,9 @@ public class Client implements AutoCloseable { private Client(Collection endpoints, Map configuration, ExecutorService sharedOperationExecutor, ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy, Object metricsRegistry, Supplier queryIdGenerator) { - this.configuration = Collections.synchronizedMap(ClientConfigProperties.parseConfigMap(configuration)); + Map parsedConfiguration = ClientConfigProperties.parseConfigMap(configuration); + this.session = Session.extractFrom(parsedConfiguration); + this.configuration = new ConcurrentHashMap<>(parsedConfiguration); this.readOnlyConfig = Collections.unmodifiableMap(configuration); this.metricsRegistry = metricsRegistry; this.queryIdGenerator = queryIdGenerator; @@ -2174,33 +2177,7 @@ public void updateClientName(String name) { */ public void updateSessionId(String sessionId) { ValidationUtils.checkNonBlank(sessionId, ClickHouseHttpProto.QPARAM_SESSION_ID); - this.configuration.put(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_ID), sessionId); - } - - /** - * Updates ClickHouse session check flag for all subsequent requests created by this client. - */ - public void updateSessionCheck(boolean sessionCheck) { - this.configuration.put(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_CHECK), - sessionCheck ? "1" : "0"); - } - - /** - * Updates ClickHouse session timeout (seconds) for all subsequent requests created by this client. - */ - public void updateSessionTimeout(int timeoutInSeconds) { - ValidationUtils.checkPositive(timeoutInSeconds, ClickHouseHttpProto.QPARAM_SESSION_TIMEOUT); - this.configuration.put(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_TIMEOUT), - String.valueOf(timeoutInSeconds)); - } - - /** - * Updates ClickHouse session timezone for all subsequent requests created by this client. - */ - public void updateSessionTimezone(String timezone) { - ValidationUtils.checkNonBlank(timezone, ClickHouseHttpProto.QPARAM_SESSION_TIMEZONE); - this.configuration.put(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_TIMEZONE), - timezone); + this.session.updateSessionId(sessionId); } public static final String clientVersion = @@ -2235,10 +2212,8 @@ private Endpoint getNextAliveNode() { * @return request settings - merged client and operation settings */ private Map buildRequestSettings(Map opSettings) { - Map requestSettings = new HashMap<>(); - synchronized (configuration) { - requestSettings.putAll(configuration); - } + Map requestSettings = new HashMap<>(configuration); + session.applyTo(requestSettings); requestSettings.putAll(opSettings); return requestSettings; } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/Session.java b/client-v2/src/main/java/com/clickhouse/client/api/Session.java new file mode 100644 index 000000000..5aecd7e92 --- /dev/null +++ b/client-v2/src/main/java/com/clickhouse/client/api/Session.java @@ -0,0 +1,100 @@ +package com.clickhouse.client.api; + +import com.clickhouse.client.api.http.ClickHouseHttpProto; +import com.clickhouse.client.api.internal.ValidationUtils; + +import java.util.Map; + +/** + * Reusable ClickHouse session configuration that can be applied to clients or operation settings. + */ +public class Session { + private String sessionId; + private Boolean sessionCheck; + private Integer sessionTimeout; + private String sessionTimezone; + + static Session extractFrom(Map configuration) { + Session session = new Session(); + + String sessionId = (String) configuration.remove(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_ID)); + if (sessionId != null) { + session.setSessionId(sessionId); + } + + String sessionCheck = (String) configuration.remove(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_CHECK)); + if (sessionCheck != null) { + session.setSessionCheck("1".equals(sessionCheck) || Boolean.parseBoolean(sessionCheck)); + } + + String sessionTimeout = (String) configuration.remove(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_TIMEOUT)); + if (sessionTimeout != null) { + session.setSessionTimeout(Integer.parseInt(sessionTimeout)); + } + + String sessionTimezone = (String) configuration.remove(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_TIMEZONE)); + if (sessionTimezone != null) { + session.setSessionTimezone(sessionTimezone); + } + + return session; + } + + public synchronized Session setSessionId(String sessionId) { + ValidationUtils.checkNonBlank(sessionId, ClickHouseHttpProto.QPARAM_SESSION_ID); + this.sessionId = sessionId; + return this; + } + + public synchronized String getSessionId() { + return sessionId; + } + + public synchronized Session setSessionCheck(boolean sessionCheck) { + this.sessionCheck = sessionCheck; + return this; + } + + public synchronized Boolean getSessionCheck() { + return sessionCheck; + } + + public synchronized Session setSessionTimeout(int timeoutInSeconds) { + ValidationUtils.checkPositive(timeoutInSeconds, ClickHouseHttpProto.QPARAM_SESSION_TIMEOUT); + this.sessionTimeout = timeoutInSeconds; + return this; + } + + public synchronized Integer getSessionTimeout() { + return sessionTimeout; + } + + public synchronized Session setSessionTimezone(String timezone) { + ValidationUtils.checkNonBlank(timezone, ClickHouseHttpProto.QPARAM_SESSION_TIMEZONE); + this.sessionTimezone = timezone; + return this; + } + + public synchronized String getSessionTimezone() { + return sessionTimezone; + } + + public synchronized void updateSessionId(String sessionId) { + setSessionId(sessionId); + } + + public synchronized void applyTo(Map requestSettings) { + putIfSet(requestSettings, ClickHouseHttpProto.QPARAM_SESSION_ID, sessionId); + putIfSet(requestSettings, ClickHouseHttpProto.QPARAM_SESSION_CHECK, + sessionCheck == null ? null : (sessionCheck ? "1" : "0")); + putIfSet(requestSettings, ClickHouseHttpProto.QPARAM_SESSION_TIMEOUT, + sessionTimeout == null ? null : String.valueOf(sessionTimeout)); + putIfSet(requestSettings, ClickHouseHttpProto.QPARAM_SESSION_TIMEZONE, sessionTimezone); + } + + private static void putIfSet(Map settings, String key, String value) { + if (value != null) { + settings.put(ClientConfigProperties.serverSetting(key), value); + } + } +} diff --git a/client-v2/src/main/java/com/clickhouse/client/api/command/CommandSettings.java b/client-v2/src/main/java/com/clickhouse/client/api/command/CommandSettings.java index 042bc5a1d..399a50f26 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/command/CommandSettings.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/command/CommandSettings.java @@ -1,5 +1,6 @@ package com.clickhouse.client.api.command; +import com.clickhouse.client.api.Session; import com.clickhouse.client.api.query.QuerySettings; public class CommandSettings extends QuerySettings { @@ -26,4 +27,10 @@ public CommandSettings setSessionTimezone(String timezone) { super.setSessionTimezone(timezone); return this; } + + @Override + public CommandSettings use(Session session) { + super.use(session); + return this; + } } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/insert/InsertSettings.java b/client-v2/src/main/java/com/clickhouse/client/api/insert/InsertSettings.java index 6d5ab49bb..840e33889 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/insert/InsertSettings.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/insert/InsertSettings.java @@ -2,6 +2,7 @@ import com.clickhouse.client.api.Client; import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.Session; import com.clickhouse.client.api.internal.CommonSettings; import org.apache.hc.core5.http.HttpHeaders; @@ -139,6 +140,11 @@ public String getSessionTimezone() { return settings.getSessionTimezone(); } + public InsertSettings use(Session session) { + settings.use(session); + return this; + } + public int getInputStreamCopyBufferSize() { return this.inputStreamCopyBufferSize; } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/CommonSettings.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/CommonSettings.java index 5aafa9191..aa2403c73 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/internal/CommonSettings.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/CommonSettings.java @@ -2,6 +2,7 @@ import com.clickhouse.client.api.Client; import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.Session; import com.clickhouse.client.api.http.ClickHouseHttpProto; import java.time.Duration; @@ -132,6 +133,12 @@ public String getSessionTimezone() { return (String) settings.get(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_TIMEZONE)); } + public CommonSettings use(Session session) { + ValidationUtils.checkNotNull(session, "session"); + session.applyTo(settings); + return this; + } + /** * Operation id. Used internally to register new operation. * Should not be called directly. diff --git a/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java b/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java index 5fa5150f8..95babb1ea 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java @@ -3,6 +3,7 @@ import com.clickhouse.client.api.Client; import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.Session; import com.clickhouse.client.api.internal.CommonSettings; import com.clickhouse.client.api.internal.ServerSettings; import com.clickhouse.client.api.internal.ValidationUtils; @@ -133,6 +134,11 @@ public String getSessionTimezone() { return settings.getSessionTimezone(); } + public QuerySettings use(Session session) { + settings.use(session); + return this; + } + /** * Read buffer is used for reading data from a server. Size is in bytes. * Minimal value is {@value MINIMAL_READ_BUFFER_SIZE} bytes. 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 d24b9ac42..7ff66b199 100644 --- a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java @@ -185,6 +185,28 @@ public void testTemporaryTablesAreBoundToSession() throws Exception { } } + @Test(groups = {"integration"}) + public void testSessionCheckFailsForUnknownSession() { + if (isCloud()) { + return; // HTTP sessions require server affinity + } + + QuerySettings settings = new QuerySettings() + .setSessionId("missing_session_" + UUID.randomUUID().toString().replace("-", "")) + .setSessionCheck(true) + .setSessionTimeout(60); + + try (Client client = newClient().build()) { + client.queryAll("SELECT 1", settings); + Assert.fail("Expected session check to fail for an unknown session"); + } catch (ClientException e) { + Assert.assertTrue(e.getCause() instanceof ServerException, "Expected ServerException but got " + e.getCause()); + ServerException serverException = (ServerException) e.getCause(); + Assert.assertEquals(serverException.getCode(), 372); + Assert.assertTrue(serverException.getMessage().toLowerCase().contains("session")); + } + } + @Test(groups = {"integration"}) public void testCustomSettings() { if (isCloud()) { @@ -574,6 +596,13 @@ public void testInvalidConfig() { } } + @Test(groups = {"integration"}) + public void testInvalidSessionConfig() { + Assert.assertThrows(IllegalArgumentException.class, () -> new Client.Builder().setSessionId("")); + Assert.assertThrows(IllegalArgumentException.class, () -> new Client.Builder().setSessionTimeout(0)); + Assert.assertThrows(IllegalArgumentException.class, () -> new Client.Builder().setSessionTimezone("")); + } + @Test(groups = {"integration"}) public void testQueryIdGenerator() throws Exception { final String queryId = UUID.randomUUID().toString(); diff --git a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java index d126bf357..e2ce64be1 100644 --- a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java @@ -3,6 +3,7 @@ import com.clickhouse.client.api.Client; import com.clickhouse.client.api.ClientConfigProperties; import com.clickhouse.client.api.ClientException; +import com.clickhouse.client.api.Session; import com.clickhouse.client.api.ClientFaultCause; import com.clickhouse.client.api.ConnectionInitiationException; import com.clickhouse.client.api.ConnectionReuseStrategy; @@ -632,11 +633,12 @@ public void testSessionSettingsClientAndOperationLevels() { .withHeader("X-ClickHouse-Summary", "{ \"read_bytes\": \"10\", \"read_rows\": \"1\"}")).build()); - QuerySettings querySettings = new QuerySettings() + Session querySession = new Session() .setSessionId("query-session") .setSessionCheck(true) .setSessionTimeout(15) .setSessionTimezone("Asia/Tokyo"); + QuerySettings querySettings = new QuerySettings().use(querySession); try (QueryResponse response = client.query("SELECT 1", querySettings).get(1, TimeUnit.SECONDS)) { Assert.assertEquals(response.getReadBytes(), 10); } @@ -667,11 +669,12 @@ public void testSessionSettingsClientAndOperationLevels() { .withHeader("X-ClickHouse-Summary", "{ \"read_bytes\": \"12\", \"read_rows\": \"1\"}")).build()); - InsertSettings insertSettings = new InsertSettings() + Session insertSession = new Session() .setSessionId("insert-session") .setSessionCheck(false) .setSessionTimeout(90) .setSessionTimezone("America/Denver"); + InsertSettings insertSettings = new InsertSettings().use(insertSession); try (InsertResponse response = client.insert( "test_table", new ByteArrayInputStream("1\n".getBytes(StandardCharsets.UTF_8)), @@ -1087,7 +1090,7 @@ public void testBearerTokenAuth() throws Exception { } @Test(groups = { "integration" }) - public void testUpdateSessionSettingsAtRuntime() throws Exception { + public void testUpdateSessionIdAtRuntime() throws Exception { if (isCloud()) { return; // mocked server } @@ -1122,17 +1125,14 @@ public void testUpdateSessionSettingsAtRuntime() throws Exception { mockServer.resetAll(); mockServer.addStubMapping(WireMock.post(WireMock.anyUrl()) .withQueryParam("session_id", WireMock.equalTo("session-updated")) - .withQueryParam("session_check", WireMock.equalTo("1")) - .withQueryParam("session_timeout", WireMock.equalTo("30")) - .withQueryParam("session_timezone", WireMock.equalTo("Asia/Novosibirsk")) + .withQueryParam("session_check", WireMock.equalTo("0")) + .withQueryParam("session_timeout", WireMock.equalTo("60")) + .withQueryParam("session_timezone", WireMock.equalTo("UTC")) .willReturn(WireMock.aResponse() .withHeader("X-ClickHouse-Summary", "{ \"read_bytes\": \"11\", \"read_rows\": \"1\"}")).build()); client.updateSessionId("session-updated"); - client.updateSessionCheck(true); - client.updateSessionTimeout(30); - client.updateSessionTimezone("Asia/Novosibirsk"); try (CommandResponse response = client.execute("SELECT 1").get(1, TimeUnit.SECONDS)) { Assert.assertEquals(response.getReadBytes(), 11); @@ -1142,6 +1142,16 @@ public void testUpdateSessionSettingsAtRuntime() throws Exception { } } + @Test(groups = { "integration" }) + public void testUpdateSessionIdRejectInvalidValues() { + try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost", 8123, false) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .build()) { + Assert.assertThrows(IllegalArgumentException.class, () -> client.updateSessionId("")); + } + } + @Test(groups = { "integration" }) public void testBasicAuthWithNoPassword() throws Exception { WireMockServer mockServer = new WireMockServer(WireMockConfiguration diff --git a/client-v2/src/test/java/com/clickhouse/client/SettingsTests.java b/client-v2/src/test/java/com/clickhouse/client/SettingsTests.java index c781d5245..ca4614a67 100644 --- a/client-v2/src/test/java/com/clickhouse/client/SettingsTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/SettingsTests.java @@ -1,6 +1,7 @@ package com.clickhouse.client; import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.Session; import com.clickhouse.client.api.insert.InsertSettings; import com.clickhouse.client.api.internal.ServerSettings; import com.clickhouse.client.api.query.QuerySettings; @@ -125,9 +126,23 @@ void testQuerySettingsSpecific() throws Exception { Assert.assertTrue(settings.getSessionCheck()); Assert.assertEquals(settings.getSessionTimeout().intValue(), 30); Assert.assertEquals(settings.getSessionTimezone(), "Asia/Tokyo"); + Assert.assertThrows(IllegalArgumentException.class, () -> settings.setSessionId("")); Assert.assertThrows(IllegalArgumentException.class, () -> settings.setSessionTimeout(0)); Assert.assertThrows(IllegalArgumentException.class, () -> settings.setSessionTimezone("")); } + + { + final Session session = new Session() + .setSessionId("session-use-1") + .setSessionCheck(true) + .setSessionTimeout(45) + .setSessionTimezone("Europe/Berlin"); + final QuerySettings settings = new QuerySettings().use(session); + Assert.assertEquals(settings.getSessionId(), "session-use-1"); + Assert.assertTrue(settings.getSessionCheck()); + Assert.assertEquals(settings.getSessionTimeout().intValue(), 45); + Assert.assertEquals(settings.getSessionTimezone(), "Europe/Berlin"); + } } @Test @@ -200,8 +215,22 @@ public void testInsertSettingsSpecific() throws Exception { Assert.assertFalse(settings.getSessionCheck()); Assert.assertEquals(settings.getSessionTimeout().intValue(), 45); Assert.assertEquals(settings.getSessionTimezone(), "Europe/Paris"); + Assert.assertThrows(IllegalArgumentException.class, () -> settings.setSessionId("")); Assert.assertThrows(IllegalArgumentException.class, () -> settings.setSessionTimeout(-1)); Assert.assertThrows(IllegalArgumentException.class, () -> settings.setSessionTimezone("")); } + + { + final Session session = new Session() + .setSessionId("session-use-2") + .setSessionCheck(false) + .setSessionTimeout(50) + .setSessionTimezone("Europe/Paris"); + final InsertSettings settings = new InsertSettings().use(session); + Assert.assertEquals(settings.getSessionId(), "session-use-2"); + Assert.assertFalse(settings.getSessionCheck()); + Assert.assertEquals(settings.getSessionTimeout().intValue(), 50); + Assert.assertEquals(settings.getSessionTimezone(), "Europe/Paris"); + } } } From 6f1ca06531508a5c8803a4971350f347af19f548 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Thu, 2 Apr 2026 12:57:50 -0700 Subject: [PATCH 4/4] Added example and updated documentation --- .../com/clickhouse/client/api/Client.java | 17 ++ .../clickhouse/client/HttpTransportTests.java | 11 +- docs/features.md | 4 +- examples/client-v2/README.md | 42 +++- .../examples/client_v2/ExamplesSupport.java | 46 ++++ .../clickhouse/examples/client_v2/Main.java | 17 +- .../examples/client_v2/Sessions.java | 205 ++++++++++++++++++ 7 files changed, 321 insertions(+), 21 deletions(-) create mode 100644 examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/ExamplesSupport.java create mode 100644 examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/Sessions.java diff --git a/client-v2/src/main/java/com/clickhouse/client/api/Client.java b/client-v2/src/main/java/com/clickhouse/client/api/Client.java index 15883e1fa..7a008f5b8 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/Client.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/Client.java @@ -989,6 +989,23 @@ public Builder setSessionTimezone(String timezone) { return serverSetting(ClickHouseHttpProto.QPARAM_SESSION_TIMEZONE, timezone); } + public Builder use(Session session) { + ValidationUtils.checkNotNull(session, "session"); + if (session.getSessionId() != null) { + setSessionId(session.getSessionId()); + } + if (session.getSessionCheck() != null) { + setSessionCheck(session.getSessionCheck()); + } + if (session.getSessionTimeout() != null) { + setSessionTimeout(session.getSessionTimeout()); + } + if (session.getSessionTimezone() != null) { + setSessionTimezone(session.getSessionTimezone()); + } + return this; + } + /** * Sets column to method matching strategy. It is used while registering POJO serializers and deserializers. * Default is {@link DefaultColumnToMethodMatchingStrategy}. diff --git a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java index e2ce64be1..ad0d1d97b 100644 --- a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java @@ -615,13 +615,16 @@ public void testSessionSettingsClientAndOperationLevels() { .options().dynamicPort().notifier(new ConsoleNotifier(false))); mockServer.start(); - try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost", mockServer.port(), false) - .setUsername("default") - .setPassword(ClickHouseServerForTest.getPassword()) + Session clientSession = new Session() .setSessionId("client-session") .setSessionCheck(false) .setSessionTimeout(60) - .setSessionTimezone("Europe/London") + .setSessionTimezone("Europe/London"); + + try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost", mockServer.port(), false) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .use(clientSession) .compressClientRequest(false) .build()) { mockServer.addStubMapping(WireMock.post(WireMock.anyUrl()) diff --git a/docs/features.md b/docs/features.md index 0832924da..f274ca578 100644 --- a/docs/features.md +++ b/docs/features.md @@ -10,7 +10,7 @@ This document lists stable, user-visible behavior in `client-v2` and `jdbc-v2` t - Proxy support: Can send requests through configured HTTP proxies, including proxy credentials. - Connection and socket tuning: Exposes pool sizing, keep-alive, reuse strategy, connect/request/socket timeouts, and low-level socket options. - Query execution: Executes SQL asynchronously and returns streaming query responses with response metadata and metrics. -- Query settings: Supports per-query database selection, output format, execution limits, roles, log comments, headers, session settings, server settings, and network timeout overrides. +- Query settings: Supports per-query database selection, output format, execution limits, roles, log comments, headers, reusable `Session` objects, session settings, server settings, and network timeout overrides. - Parameterized SQL: Accepts named query parameters and can send them through supported HTTP request encodings. - Result materialization helpers: Provides streaming `Records`, generic row access, and convenience APIs that materialize all rows into generic records or typed POJOs. - Binary format readers: Reads ClickHouse binary result formats including `Native`, `RowBinary`, `RowBinaryWithNames`, and `RowBinaryWithNamesAndTypes`. @@ -18,6 +18,7 @@ This document lists stable, user-visible behavior in `client-v2` and `jdbc-v2` t - Insert APIs: Supports inserting registered POJOs, raw streams, and callback-driven writers, with optional column lists and format selection. - Insert controls: Supports insert-specific settings such as deduplication token, query id, compression behavior, and request headers. - Command execution: Executes DDL or other non-result commands and exposes response summaries and operation metrics. +- Session handling: Supports client-wide and per-operation HTTP sessions, operation-level session overrides, runtime updates of client `session_id`, and server-side session validation through `session_check`. - Metadata discovery: Loads table schemas from table names or queries and allows schema registration for typed read/write operations. - Server information loading: Can refresh server version, current user, and server time zone information. - Compression support: Supports response compression, ClickHouse LZ4 request/response compression, HTTP content compression, and caller-supplied precompressed insert bodies. @@ -34,6 +35,7 @@ Compatibility-sensitive traits: - Identifier quoting behavior is stable API for helper callers: identifiers are double-quoted, embedded double quotes are doubled, and optional quoting keeps simple identifiers unchanged. - Instant formatting is type-sensitive and should not drift: `Date` formatting depends on an explicit timezone, `DateTime` is serialized as epoch seconds, and higher-precision timestamps preserve up to 9 fractional digits. - Timezone conversion helpers preserve nanoseconds and can intentionally shift local date or time when interpreted in a different timezone; this behavior is covered by tests and should not be normalized away. +- Session precedence is part of the contract: client session defaults apply to each request, operation settings may override them, and only the client `session_id` is mutable at runtime while other client session properties remain fixed for the lifetime of the client. ## `jdbc-v2` diff --git a/examples/client-v2/README.md b/examples/client-v2/README.md index 8c6918228..2d6251a6e 100644 --- a/examples/client-v2/README.md +++ b/examples/client-v2/README.md @@ -2,24 +2,54 @@ ## Overview -This example demonstrates how to use the client V2 to interact with the ClickHouse server. +This module contains runnable examples demonstrating how to use `client-v2` to interact with a ClickHouse server. ## How to Run Apache Maven or IDE with Maven support is required to run this example. -First we need to compile the example : +First compile the examples: ```shell mvn clean compile ``` -To run: +Both `Main` and `Sessions` are executable entry points and use the same connection properties. + +Run the general end-to-end example: +```shell +mvn exec:java -Dexec.mainClass="com.clickhouse.examples.client_v2.Main" +``` + +Run the sessions example: ```shell - mvn exec:java -Dexec.mainClass="com.clickhouse.examples.client_v2.Main" +mvn exec:java -Dexec.mainClass="com.clickhouse.examples.client_v2.Sessions" ``` -Addition options can be passed to the application: +Additional options can be passed to either application: - `-DchEndpoint` - Endpoint to connect in the format of URL (default: http://localhost:8123/) - `-DchUser` - ClickHouse user name (default: default) - `-DchPassword` - ClickHouse user password (default: empty) -- `-DchDatabase` - ClickHouse database name (default: default) \ No newline at end of file +- `-DchDatabase` - ClickHouse database name (default: default) + +Example with custom connection properties: +```shell +mvn exec:java \ + -Dexec.mainClass="com.clickhouse.examples.client_v2.Sessions" \ + -DchEndpoint="http://localhost:8123" \ + -DchUser="default" \ + -DchPassword="" \ + -DchDatabase="default" +``` + +## Executable Examples + +`com.clickhouse.examples.client_v2.Main` +- Shows the existing read and write flows for `client-v2`. + +`com.clickhouse.examples.client_v2.Sessions` +- Shows client-wide session configuration with `Client.Builder.use(session)`. +- Shows operation-wide session configuration with `settings.use(session)`. +- Shows using two different sessions with one client. +- Shows how to detect and handle session timeout using `session_check`. + +Note: HTTP sessions require server affinity. Use a single endpoint or sticky routing when requests go through a load balancer. \ No newline at end of file diff --git a/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/ExamplesSupport.java b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/ExamplesSupport.java new file mode 100644 index 000000000..fd5c10387 --- /dev/null +++ b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/ExamplesSupport.java @@ -0,0 +1,46 @@ +package com.clickhouse.examples.client_v2; + +import com.clickhouse.client.api.Client; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +final class ExamplesSupport { + private ExamplesSupport() { + } + + static ConnectionConfig loadConnectionConfig() { + return new ConnectionConfig( + System.getProperty("chEndpoint", "http://localhost:8123"), + System.getProperty("chUser", "default"), + System.getProperty("chPassword", ""), + System.getProperty("chDatabase", "default")); + } + + static boolean isServerAlive(ConnectionConfig config) { + try (Client client = new Client.Builder() + .addEndpoint(config.endpoint) + .setUsername(config.user) + .setPassword(config.password) + .setDefaultDatabase(config.database) + .build()) { + return client.ping(); + } catch (Exception e) { + log.error("Failed to ping ClickHouse server", e); + return false; + } + } + + static final class ConnectionConfig { + final String endpoint; + final String user; + final String password; + final String database; + + ConnectionConfig(String endpoint, String user, String password, String database) { + this.endpoint = endpoint; + this.user = user; + this.password = password; + this.database = database; + } + } +} diff --git a/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/Main.java b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/Main.java index 91da25ee8..f4ca4aec7 100644 --- a/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/Main.java +++ b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/Main.java @@ -11,15 +11,12 @@ public class Main { public static void main(String[] args) { - final String endpoint = System.getProperty("chEndpoint", "http://localhost:8123"); - final String user = System.getProperty("chUser", "default"); - final String password = System.getProperty("chPassword", ""); - final String database = System.getProperty("chDatabase", "default"); + ExamplesSupport.ConnectionConfig config = ExamplesSupport.loadConnectionConfig(); // Stream data from resources/sample_hacker_news_posts.json to ClickHouse - Stream2DbWriter writer = new Stream2DbWriter(endpoint, user, password, database); + Stream2DbWriter writer = new Stream2DbWriter(config.endpoint, config.user, config.password, config.database); - if (writer.isServerAlive()) { + if (ExamplesSupport.isServerAlive(config)) { log.info("ClickHouse server is alive"); } else { log.error("ClickHouse server is not alive"); @@ -36,13 +33,13 @@ public static void main(String[] args) { } // Read data back - SimpleReader reader = new SimpleReader(endpoint, user, password, database); + SimpleReader reader = new SimpleReader(config.endpoint, config.user, config.password, config.database); reader.readDataUsingBinaryFormat(); reader.readDataAll(); reader.readData(); // Read as Text format - TextFormatsReader textFormatsReader = new TextFormatsReader(endpoint, user, password, database); + TextFormatsReader textFormatsReader = new TextFormatsReader(config.endpoint, config.user, config.password, config.database); textFormatsReader.readAsJsonEachRow(); textFormatsReader.readAsJsonEachRowButGSon(); textFormatsReader.readJSONEachRowIntoArrayOfObject(); @@ -51,7 +48,7 @@ public static void main(String[] args) { textFormatsReader.readAsTSV(); // Insert data using POJO - POJO2DbWriter pojoWriter = new POJO2DbWriter(endpoint, user, password, database); + POJO2DbWriter pojoWriter = new POJO2DbWriter(config.endpoint, config.user, config.password, config.database); pojoWriter.resetTable(); for (int i = 0; i < 10; i++) { pojoWriter.submit(new ArticleViewEvent(11132929d, LocalDateTime.now(), UUID.randomUUID().toString())); @@ -60,7 +57,7 @@ public static void main(String[] args) { pojoWriter.printLastEvents(); // Insert data using POJO with JSON - ExperimentalJSONExample jsonExample = new ExperimentalJSONExample(endpoint, user, password, database); + ExperimentalJSONExample jsonExample = new ExperimentalJSONExample(config.endpoint, config.user, config.password, config.database); jsonExample.writeData(); jsonExample.readData(); diff --git a/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/Sessions.java b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/Sessions.java new file mode 100644 index 000000000..842d31dfc --- /dev/null +++ b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/Sessions.java @@ -0,0 +1,205 @@ +package com.clickhouse.examples.client_v2; + +import com.clickhouse.client.api.Client; +import com.clickhouse.client.api.ClientException; +import com.clickhouse.client.api.ServerException; +import com.clickhouse.client.api.Session; +import com.clickhouse.client.api.command.CommandResponse; +import com.clickhouse.client.api.command.CommandSettings; +import com.clickhouse.client.api.query.QueryResponse; +import com.clickhouse.client.api.query.QuerySettings; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; + +/** + * Example class showing different ways to work with ClickHouse HTTP sessions. + * + *

HTTP sessions require that requests for the same session id reach the same ClickHouse server, + * so use a single endpoint or sticky routing if a load balancer is involved.

+ */ +@Slf4j +public class Sessions { + private static final long REQUEST_TIMEOUT_SECONDS = 10; + + private final String endpoint; + private final String user; + private final String password; + private final String database; + + public Sessions(String endpoint, String user, String password, String database) { + this.endpoint = endpoint; + this.user = user; + this.password = password; + this.database = database; + } + + public static void main(String[] args) { + ExamplesSupport.ConnectionConfig config = ExamplesSupport.loadConnectionConfig(); + if (!ExamplesSupport.isServerAlive(config)) { + log.error("ClickHouse server is not alive"); + Runtime.getRuntime().exit(-503); + } + + Sessions sessions = new Sessions(config.endpoint, config.user, config.password, config.database); + sessions.clientWideSession(); + sessions.operationWideSession(); + sessions.twoSessions(); + sessions.handleSessionTimeout(); + + log.info("Session examples completed"); + } + + /** + * Configure a session on the client so every operation automatically reuses it. + */ + public void clientWideSession() { + Session clientSession = new Session() + .setSessionId("client_session_" + randomSuffix()) + .setSessionTimeout(60) + .setSessionTimezone("UTC"); + String tableName = "tmp_client_session_" + randomSuffix(); + + try (Client client = newClientBuilder().use(clientSession).build()) { + execute(client, "CREATE TEMPORARY TABLE " + tableName + " (value UInt8)"); + execute(client, "INSERT INTO " + tableName + " VALUES (1)"); + + long value = querySingleLong(client, "SELECT value FROM " + tableName, null); + log.info("Client-wide session keeps the temporary table alive across operations. value={}", value); + } catch (Exception e) { + log.error("Failed to demonstrate client-wide session", e); + } + } + + /** + * Apply a session only to the operations that should use it. + */ + public void operationWideSession() { + Session session = new Session() + .setSessionId("operation_session_" + randomSuffix()) + .setSessionTimeout(60) + .setSessionTimezone("UTC"); + CommandSettings commandSettings = new CommandSettings().use(session); + QuerySettings querySettings = new QuerySettings().use(session); + String tableName = "tmp_operation_session_" + randomSuffix(); + + try (Client client = newClientBuilder().build()) { + execute(client, "CREATE TEMPORARY TABLE " + tableName + " (value UInt8)", commandSettings); + execute(client, "INSERT INTO " + tableName + " VALUES (2)", commandSettings); + + long value = querySingleLong(client, "SELECT value FROM " + tableName, querySettings); + log.info("Operation-wide session keeps state only for requests that use it. value={}", value); + } catch (Exception e) { + log.error("Failed to demonstrate operation-wide session", e); + } + } + + /** + * Reuse the same client with two independent sessions. + */ + public void twoSessions() { + Session sessionA = new Session() + .setSessionId("session_a_" + randomSuffix()) + .setSessionTimeout(60); + Session sessionB = new Session() + .setSessionId("session_b_" + randomSuffix()) + .setSessionTimeout(60); + + CommandSettings commandsA = new CommandSettings().use(sessionA); + QuerySettings queriesA = new QuerySettings().use(sessionA); + CommandSettings commandsB = new CommandSettings().use(sessionB); + QuerySettings queriesB = new QuerySettings().use(sessionB); + String tableName = "tmp_shared_name"; + + try (Client client = newClientBuilder().build()) { + execute(client, "CREATE TEMPORARY TABLE " + tableName + " (value UInt8)", commandsA); + execute(client, "INSERT INTO " + tableName + " VALUES (10)", commandsA); + + execute(client, "CREATE TEMPORARY TABLE " + tableName + " (value UInt8)", commandsB); + execute(client, "INSERT INTO " + tableName + " VALUES (20)", commandsB); + + long valueA = querySingleLong(client, "SELECT value FROM " + tableName, queriesA); + long valueB = querySingleLong(client, "SELECT value FROM " + tableName, queriesB); + + log.info("Two sessions can keep isolated temporary tables with the same name. sessionA={}, sessionB={}", + valueA, valueB); + } catch (Exception e) { + log.error("Failed to demonstrate two independent sessions", e); + } + } + + /** + * Show one way to detect that a session has expired on the server. + */ + public void handleSessionTimeout() { + Session session = new Session() + .setSessionId("timeout_session_" + randomSuffix()) + .setSessionTimeout(3); + + try (Client client = newClientBuilder().build()) { + try (QueryResponse response = client.query("SELECT 1", new QuerySettings().use(session)) + .get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + log.info("Started session {} with queryId {}", session.getSessionId(), response.getQueryId()); + } + + long waitSeconds = session.getSessionTimeout() + 1L; + log.info("Waiting {} seconds for session {} to expire", waitSeconds, session.getSessionId()); + TimeUnit.SECONDS.sleep(waitSeconds); + + QuerySettings sessionCheck = new QuerySettings().use(session).setSessionCheck(true); + try (QueryResponse response = client.query("SELECT 1", sessionCheck) + .get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + log.warn("Session {} is still active after query {}. Increase the wait time if your server rounds session_timeout.", + session.getSessionId(), response.getQueryId()); + } catch (ClientException e) { + ServerException serverException = findServerException(e); + if (serverException == null) { + throw e; + } + log.info("Session {} expired as expected: {}", session.getSessionId(), serverException.getMessage()); + } + } catch (Exception e) { + log.error("Failed to demonstrate session timeout handling", e); + } + } + + private Client.Builder newClientBuilder() { + return new Client.Builder() + .addEndpoint(endpoint) + .setUsername(user) + .setPassword(password) + .setDefaultDatabase(database) + .compressServerResponse(true); + } + + private void execute(Client client, String sql) throws Exception { + execute(client, sql, null); + } + + private void execute(Client client, String sql, CommandSettings settings) throws Exception { + try (CommandResponse response = settings == null + ? client.execute(sql).get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS) + : client.execute(sql, settings).get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + log.info("Executed: {}, readBytes={}", sql, response.getReadBytes()); + } + } + + private long querySingleLong(Client client, String sql, QuerySettings settings) throws Exception { + return (settings == null ? client.queryAll(sql) : client.queryAll(sql, settings)).get(0).getLong(1); + } + + private ServerException findServerException(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + if (current instanceof ServerException) { + return (ServerException) current; + } + current = current.getCause(); + } + return null; + } + + private String randomSuffix() { + return Long.toHexString(System.nanoTime()); + } +}