diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef68d17e6..5a2a110de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -73,6 +73,17 @@
### New Features
+- **[client-v2, jdbc-v2]** Added support for an application-supplied `javax.net.ssl.SSLContext`. In client-v2,
+ `Client.Builder.setSSLContext(SSLContext)` hands the client a fully pre-built context that is used as is; in
+ jdbc-v2 the same context may be passed as a live object in the connection `Properties` under the `ssl_context`
+ key (added with `Properties.put`, since it is not a string). Trust/key material options cannot be combined with
+ a custom context and are rejected; `ssl_mode` still applies but only to server hostname verification. This
+ supports in-memory TLS material that must never be written to disk, including behind connection pools that only
+ expose `java.util.Properties`.
+ - Examples for client-v2 https://github.com/ClickHouse/clickhouse-java/blob/main/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/SSLExamples.java
+ - Examples for jdbc-v2 https://github.com/ClickHouse/clickhouse-java/blob/main/examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/SSLExamples.java
+ (https://github.com/ClickHouse/clickhouse-java/pull/2918, https://github.com/ClickHouse/clickhouse-java/issues/2909)
+
- **[jdbc-v2, client-v2]** Implemented SSL modes configuration. Now it is possible to set `ssl_mode` to `DISABLED`,
`TRUST`, `VERIFY_CA` and `STRICT`. Note for V1 users: `NONE` is supported only by JDBC driver and mapped to `TRUST`.
Please migrate to the new naming.
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 3bf94c529..60c01034f 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
@@ -80,6 +80,8 @@
import java.util.function.Supplier;
import java.util.stream.Collectors;
+import javax.net.ssl.SSLContext;
+
/**
*
Client is the starting point for all interactions with ClickHouse.
*
@@ -147,8 +149,12 @@ public class Client implements AutoCloseable {
private Client(Collection endpoints, Map configuration,
ExecutorService sharedOperationExecutor, ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy,
- Object metricsRegistry, Supplier queryIdGenerator, CredentialsManager cManager) {
+ Object metricsRegistry, Supplier queryIdGenerator, CredentialsManager cManager,
+ SSLContext sslContext) {
Map parsedConfiguration = new ConcurrentHashMap<>(ClientConfigProperties.parseConfigMap(configuration));
+ if (sslContext != null) {
+ parsedConfiguration.put(ClientConfigProperties.SSL_CONTEXT.getKey(), sslContext);
+ }
this.credentialsManager = cManager;
this.session = Session.extractFrom(parsedConfiguration);
this.configuration = new ConcurrentHashMap<>(parsedConfiguration);
@@ -270,6 +276,18 @@ public static class Builder {
private ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy;
private Object metricRegistry = null;
private Supplier queryIdGenerator;
+ private SSLContext sslContext = null;
+
+ // Trust/key material options that feed a context the client would otherwise build; none of them
+ // may be combined with an application-supplied SSLContext (see build()).
+ private static final ClientConfigProperties[] SSL_MATERIAL_PROPERTIES = {
+ ClientConfigProperties.SSL_TRUST_STORE,
+ ClientConfigProperties.SSL_KEYSTORE_TYPE,
+ ClientConfigProperties.SSL_KEY_STORE_PASSWORD,
+ ClientConfigProperties.SSL_KEY,
+ ClientConfigProperties.CA_CERTIFICATE,
+ ClientConfigProperties.SSL_CERTIFICATE,
+ };
public Builder() {
this.endpoints = new HashSet<>();
@@ -344,6 +362,11 @@ private Builder addEndpoint(Endpoint endpoint) {
* @param value - configuration option value
*/
public Builder setOption(String key, String value) {
+ if (key.equals(ClientConfigProperties.SSL_CONTEXT.getKey())) {
+ throw new ClientMisconfigurationException("'" + ClientConfigProperties.SSL_CONTEXT.getKey()
+ + "' cannot be set as a string; supply a javax.net.ssl.SSLContext object via "
+ + "Client.Builder.setSSLContext(...)");
+ }
this.configuration.put(key, value);
if (key.equals(ClientConfigProperties.PRODUCT_NAME.getKey())) {
setClientName(value);
@@ -785,6 +808,20 @@ public Builder setSSLMode(SSLMode sslMode) {
return this;
}
+ /**
+ * Supplies a pre-built {@link SSLContext}. When set, it is used as is instead of a context built
+ * from the configured trust/key material (which then cannot be set alongside it). {@link SSLMode}
+ * still applies, but only to server hostname verification: {@link SSLMode#STRICT} (default) enforces
+ * it while {@link SSLMode#TRUST} and {@link SSLMode#VERIFY_CA} skip it.
+ *
+ * @param sslContext a fully configured SSL context; {@code null} clears any previously set context
+ * @return same instance of the builder
+ */
+ public Builder setSSLContext(SSLContext sslContext) {
+ this.sslContext = sslContext;
+ return this;
+ }
+
/**
* Configure client to use server timezone for date/datetime columns. Default is true.
* If this options is selected then server timezone should be set as well.
@@ -1165,14 +1202,28 @@ public Client build() {
CredentialsManager cManager = new CredentialsManager(this.configuration);
- if (configuration.containsKey(ClientConfigProperties.SSL_TRUST_STORE.getKey()) &&
+ // A textual 'ssl_context' can never be a live context (also rejected in setOption).
+ if (configuration.containsKey(ClientConfigProperties.SSL_CONTEXT.getKey())) {
+ throw new ClientMisconfigurationException("'" + ClientConfigProperties.SSL_CONTEXT.getKey()
+ + "' cannot be set as a string; supply a javax.net.ssl.SSLContext object via "
+ + "Client.Builder.setSSLContext(...)");
+ }
+
+ if (this.sslContext != null) {
+ // A custom SSLContext replaces any context the client would build, so trust/key material
+ // cannot be set alongside it. SSL_MODE (hostname verification) is still allowed.
+ for (ClientConfigProperties material : SSL_MATERIAL_PROPERTIES) {
+ if (configuration.containsKey(material.getKey())) {
+ throw new ClientMisconfigurationException("'" + material.getKey() + "' cannot be combined"
+ + " with a custom SSLContext; the supplied context is used as is. Only 'ssl_mode'"
+ + " (hostname verification) may be set alongside it.");
+ }
+ }
+ } else if (configuration.containsKey(ClientConfigProperties.SSL_TRUST_STORE.getKey()) &&
configuration.containsKey(ClientConfigProperties.SSL_CERTIFICATE.getKey())) {
throw new ClientMisconfigurationException("Trust store and certificates cannot be used together");
}
- // A trust store and a CA certificate are not rejected here: for VERIFY_CA/STRICT the trust
- // store takes precedence and the CA certificate is ignored with a warning (see createSSLContext).
-
// Resolve ssl_mode case-insensitively and normalize it to the canonical enum name so that
// downstream parsing is consistent and an unknown value is reported as a misconfiguration
// here instead of failing later with a generic enum-parsing error.
@@ -1229,7 +1280,8 @@ public Client build() {
}
return new Client(this.endpoints, this.configuration, this.sharedOperationExecutor,
- this.columnToMethodMatchingStrategy, this.metricRegistry, this.queryIdGenerator, cManager);
+ this.columnToMethodMatchingStrategy, this.metricRegistry, this.queryIdGenerator, cManager,
+ this.sslContext);
}
}
diff --git a/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java b/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java
index 9a38232ba..8281654cf 100644
--- a/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java
+++ b/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java
@@ -8,6 +8,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import javax.net.ssl.SSLContext;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -118,6 +120,8 @@ public enum ClientConfigProperties {
SSL_MODE("ssl_mode", SSLMode.class, SSLMode.STRICT.name()),
+ SSL_CONTEXT("ssl_context", SSLContext.class),
+
RETRY_ON_FAILURE("retry", Integer.class, "3"),
INPUT_OUTPUT_FORMAT("format", ClickHouseFormat.class),
diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java
index ab4b0153c..f610f2155 100644
--- a/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java
+++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java
@@ -154,7 +154,7 @@ public HttpAPIClientHelper(Map configuration, Object metricsRegi
}
/**
- * Creates or returns default SSL context.
+ * Creates an SSL context from the configured trust/key material.
*
* @return SSLContext
*/
@@ -280,7 +280,21 @@ private HttpClientConnectionManager poolConnectionManager(LayeredConnectionSocke
public CloseableHttpClient createHttpClient(boolean initSslContext, Map configuration) {
// Top Level builders
HttpClientBuilder clientBuilder = HttpClientBuilder.create();
- SSLContext sslContext = initSslContext ? createSSLContext(configuration) : null;
+ // An application-supplied SSLContext is used as is; otherwise one is built from the configured
+ // trust/key material. Server hostname verification below still applies via the SSL mode.
+ SSLContext sslContext = null;
+ if (initSslContext) {
+ Object customSSLContext = configuration.get(ClientConfigProperties.SSL_CONTEXT.getKey());
+ if (customSSLContext == null) {
+ sslContext = createSSLContext(configuration);
+ } else if (customSSLContext instanceof SSLContext) {
+ sslContext = (SSLContext) customSSLContext;
+ } else {
+ throw new ClientMisconfigurationException("'" + ClientConfigProperties.SSL_CONTEXT.getKey()
+ + "' must be a javax.net.ssl.SSLContext instance but was "
+ + customSSLContext.getClass().getName() + "; supply it via Client.Builder.setSSLContext(...)");
+ }
+ }
LayeredConnectionSocketFactory sslConnectionSocketFactory;
if (sslContext != null) {
String socketSNI = (String)configuration.get(ClientConfigProperties.SSL_SOCKET_SNI.getKey());
diff --git a/client-v2/src/test/java/com/clickhouse/client/api/ClientBuilderTest.java b/client-v2/src/test/java/com/clickhouse/client/api/ClientBuilderTest.java
index baccdc767..bf303ebba 100644
--- a/client-v2/src/test/java/com/clickhouse/client/api/ClientBuilderTest.java
+++ b/client-v2/src/test/java/com/clickhouse/client/api/ClientBuilderTest.java
@@ -1,12 +1,16 @@
package com.clickhouse.client.api;
import com.clickhouse.client.api.enums.SSLMode;
+import com.clickhouse.client.api.internal.HttpAPIClientHelper;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
+import javax.net.ssl.SSLContext;
import java.lang.reflect.Field;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
public class ClientBuilderTest {
@@ -86,6 +90,144 @@ public void testSslModeInvalidValueRejected() {
.build());
}
+ @Test
+ public void testStringSSLContextRejectedBySetOption() {
+ Assert.expectThrows(ClientMisconfigurationException.class,
+ () -> new Client.Builder().setOption(ClientConfigProperties.SSL_CONTEXT.getKey(), "not-a-context"));
+ }
+
+ @DataProvider(name = "sslMaterialWithCustomContext_DP")
+ public static Object[][] sslMaterialWithCustomContext_DP() {
+ return new Object[][] {
+ { ClientConfigProperties.SSL_TRUST_STORE.getKey(), "/path/to/truststore.jks" },
+ { ClientConfigProperties.SSL_KEYSTORE_TYPE.getKey(), "JKS" },
+ { ClientConfigProperties.SSL_KEY_STORE_PASSWORD.getKey(), "secret" },
+ { ClientConfigProperties.SSL_KEY.getKey(), "/path/to/client.key" },
+ { ClientConfigProperties.CA_CERTIFICATE.getKey(), "/path/to/ca.crt" },
+ { ClientConfigProperties.SSL_CERTIFICATE.getKey(), "/path/to/client.crt" },
+ };
+ }
+
+ @Test(dataProvider = "sslMaterialWithCustomContext_DP")
+ public void testCustomSSLContextRejectsOtherSslMaterial(String materialKey, String materialValue) throws Exception {
+ SSLContext customContext = SSLContext.getInstance("TLS");
+ customContext.init(null, null, null);
+
+ Assert.expectThrows(ClientMisconfigurationException.class, () -> new Client.Builder()
+ .addEndpoint("https://localhost:8443")
+ .setUsername("default")
+ .setPassword("")
+ .setOption(materialKey, materialValue)
+ .setSSLContext(customContext)
+ .build());
+ }
+
+ @Test
+ public void testCustomSSLContextAllowsSslMode() throws Exception {
+ SSLContext customContext = SSLContext.getInstance("TLS");
+ customContext.init(null, null, null);
+
+ try (Client client = new Client.Builder()
+ .addEndpoint("https://localhost:8443")
+ .setUsername("default")
+ .setPassword("")
+ .setSSLMode(SSLMode.VERIFY_CA)
+ .setSSLContext(customContext)
+ .build()) {
+ Assert.assertSame(extractConfiguration(client).get(ClientConfigProperties.SSL_CONTEXT.getKey()),
+ customContext);
+ Assert.assertEquals(client.getConfiguration().get(ClientConfigProperties.SSL_MODE.getKey()),
+ SSLMode.VERIFY_CA.name());
+ }
+ }
+
+ @Test
+ public void testTrustStoreAndClientCertificateConflictRejectedWithoutCustomContext() {
+ // Contrast: without a custom SSLContext the trust-store/certificate conflict is still rejected.
+ Assert.expectThrows(ClientMisconfigurationException.class, () -> new Client.Builder()
+ .addEndpoint("https://localhost:8443")
+ .setUsername("default")
+ .setPassword("")
+ .setSSLTrustStore("/path/to/truststore.jks")
+ .setClientCertificate("client.crt")
+ .build());
+ }
+
+ @Test
+ public void testSetSSLContextStoredInConfiguration() throws Exception {
+ SSLContext customContext = SSLContext.getInstance("TLS");
+ customContext.init(null, null, null);
+
+ try (Client client = new Client.Builder()
+ .addEndpoint("https://localhost:8443")
+ .setUsername("default")
+ .setPassword("")
+ .setSSLContext(customContext)
+ .build()) {
+ Assert.assertSame(extractConfiguration(client).get(ClientConfigProperties.SSL_CONTEXT.getKey()),
+ customContext, "The application-supplied SSLContext should be stored in the configuration");
+ }
+
+ // Without setSSLContext the key must be absent so the client builds its own context.
+ try (Client client = new Client.Builder()
+ .addEndpoint("https://localhost:8443")
+ .setUsername("default")
+ .setPassword("")
+ .build()) {
+ Assert.assertNull(extractConfiguration(client).get(ClientConfigProperties.SSL_CONTEXT.getKey()),
+ "No SSLContext should be stored when none is supplied");
+ }
+ }
+
+ @Test
+ public void testCreateSSLContextIgnoresCustomContext() throws Exception {
+ SSLContext customContext = SSLContext.getInstance("TLS");
+ customContext.init(null, null, null);
+
+ try (Client client = new Client.Builder()
+ .addEndpoint("https://localhost:8443")
+ .setUsername("default")
+ .setPassword("")
+ .build()) {
+ HttpAPIClientHelper helper = extractHttpClientHelper(client);
+ Map configWithCustom = new HashMap<>(extractConfiguration(client));
+ configWithCustom.put(ClientConfigProperties.SSL_CONTEXT.getKey(), customContext);
+ // createSSLContext only builds from trust/key material; selecting a custom context is
+ // createHttpClient's responsibility, so this must not return the supplied context.
+ Assert.assertNotSame(helper.createSSLContext(configWithCustom), customContext);
+ }
+ }
+
+ @Test
+ public void testCreateHttpClientRejectsNonSSLContextValue() throws Exception {
+ try (Client client = new Client.Builder()
+ .addEndpoint("https://localhost:8443")
+ .setUsername("default")
+ .setPassword("")
+ .build()) {
+ HttpAPIClientHelper helper = extractHttpClientHelper(client);
+ Map configWithBadContext = new HashMap<>(extractConfiguration(client));
+ configWithBadContext.put(ClientConfigProperties.SSL_CONTEXT.getKey(), "not-a-context");
+ // A non-SSLContext value under 'ssl_context' is a misconfiguration; createHttpClient must
+ // reject it instead of silently building a context from trust/key material.
+ Assert.expectThrows(ClientMisconfigurationException.class,
+ () -> helper.createHttpClient(true, configWithBadContext));
+ }
+ }
+
+ private static HttpAPIClientHelper extractHttpClientHelper(Client client) throws Exception {
+ Field helperField = Client.class.getDeclaredField("httpClientHelper");
+ helperField.setAccessible(true);
+ return (HttpAPIClientHelper) helperField.get(client);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map extractConfiguration(Client client) throws Exception {
+ Field configField = Client.class.getDeclaredField("configuration");
+ configField.setAccessible(true);
+ return (Map) configField.get(client);
+ }
+
private static String extractFirstEndpointUri(Client client) throws Exception {
Field endpointsField = Client.class.getDeclaredField("endpoints");
endpointsField.setAccessible(true);
diff --git a/docs/features.md b/docs/features.md
index 11a0e4ac4..4237a80cd 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -7,6 +7,7 @@ This document lists stable, user-visible behavior in `client-v2` and `jdbc-v2` t
- HTTP and HTTPS connectivity: Connects to ClickHouse over HTTP(S), supports endpoint paths, and exposes a basic `ping` health check.
- TLS configuration: Supports trust stores, client certificates/keys, SSL certificate authentication, and SNI for HTTPS connections. Trust material (root CA and client certificate/key) can be supplied either as a file path or directly as PEM content.
- SSL verification modes: `Client.Builder.setSSLMode(SSLMode)` (or the `ssl_mode` property) controls how strictly the server identity is verified on secure connections: `DISABLED` (SSL not used; plain protocols only), `TRUST` (accept any server certificate and skip hostname verification; a configured trust store or CA certificate is ignored with a warning, while a client certificate/key is still applied for mTLS if configured), `VERIFY_CA` (validate the certificate chain but skip hostname verification), and `STRICT` (full chain and hostname verification, default).
+- Custom SSL context: `Client.Builder.setSSLContext(SSLContext)` supplies a fully pre-built `javax.net.ssl.SSLContext`. When set, the client uses it as is instead of building one from the trust/key material (trust store, CA certificate, client certificate/key); those options cannot be combined with a custom context and are rejected as invalid configuration. `ssl_mode` then only controls server hostname verification. This is primarily for material assembled in memory that must never be written to disk.
- Authentication modes: Supports username/password credentials, ClickHouse auth headers, bearer tokens, and optional HTTP Basic authentication.
- Runtime credential updates: Existing `Client` instances can update username/password or bearer-token credentials for subsequent requests without rebuilding the client.
- Proxy support: Can send requests through configured HTTP proxies, including proxy credentials.
@@ -44,6 +45,7 @@ Compatibility-sensitive traits:
- `Geometry` write inference is dimension-based rather than fully type-specific: point, ring/line string, polygon/multi-line string, and multi-polygon are selected from array depth, so writing `Geometry` cannot currently distinguish `Ring` from `LineString` or `Polygon` from `MultiLineString`.
- 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.
- SSL mode behavior is compatibility-sensitive: the default is `STRICT`. `ssl_mode` does not enable or disable encryption - the endpoint scheme decides that. `DISABLED` is only valid with a plain `http://` endpoint; combining it with an `https://` endpoint throws `ClientMisconfigurationException`. `TRUST` accepts any server certificate and skips hostname verification; a configured trust store or CA certificate has no effect in this mode and is ignored (a warning is logged), while a client certificate/key is still applied for mTLS. For `VERIFY_CA` and `STRICT`, a trust store and a CA certificate cannot both take effect: when both are configured the trust store is used and the CA certificate is ignored (a warning is logged). A trust store and a client certificate (`sslcert`) still cannot be configured together and throw `ClientMisconfigurationException`. `ssl_mode` values are matched case-insensitively and normalized to the canonical enum name (`DISABLED`, `TRUST`, `VERIFY_CA`, `STRICT`) when the client is built; an unrecognized value throws `ClientMisconfigurationException`.
+- Custom SSL context precedence is compatibility-sensitive: when an application-supplied `SSLContext` is set (`Client.Builder.setSSLContext(SSLContext)`), it is used as is. `ssl_mode` still applies but only to server hostname verification (`TRUST`/`VERIFY_CA` skip it, `STRICT` enforces it). Because the supplied context replaces any context the client would build, the trust/key material options (trust store, key-store type/password, client key, CA certificate, client certificate) cannot be combined with it and are rejected with `ClientMisconfigurationException`; only `ssl_mode` may be set alongside a custom context. The supplied context is a live object and is never parsed from or represented as a string: a textual `ssl_context` value (from a string option or URL query parameter) is rejected with `ClientMisconfigurationException` rather than silently ignored.
- Certificate-as-content support is compatibility-sensitive: any certificate or key value containing a PEM begin marker (`-----BEGIN`) is treated as inline PEM content, otherwise it is treated as a file path (also searched in the home directory and on the classpath).
- JSONEachRow reading depends on the selected parser factory and request format settings: parser materialization determines Java value types, the reader infers minimal schema from the first row, and JSON number server settings are applied only when `QuerySettings` resolves to `ClickHouseFormat.JSONEachRow` and `json_disable_number_quoting` is enabled.
- JSONEachRow schema inference is intentionally best-effort: scalar values use Java-to-ClickHouse type mappings, while JSON arrays and objects are identified structurally as `Array` and `Map`. For arrays, maps, and some nested or ambiguous values, the inferred type may not include the most specific element, key, value, or nested ClickHouse type.
@@ -54,6 +56,7 @@ Compatibility-sensitive traits:
- JDBC driver registration: Registers through the standard JDBC service mechanism and is available through `DriverManager`.
- JDBC URL parsing: Accepts `jdbc:clickhouse:` and `jdbc:ch:` URLs with host, port, optional HTTP path, optional database, and query parameters.
- SSL URL support: Supports HTTPS connections through URL and property configuration, including default protocol and port handling. The `ssl_mode` property selects the verification strictness (`disabled`, `trust`, `verify_ca`, `strict`); values are case-insensitive and the traditional JDBC value `none` is accepted as an alias for `trust`. Root CA and client certificate/key may be supplied as a file path or as inline PEM content.
+- Custom SSL context via properties: A fully pre-built `javax.net.ssl.SSLContext` may be passed as a live object in the connection `Properties` under the `ssl_context` key (added with `Properties.put`, since it is not a string). It is forwarded to the underlying `client-v2` transport and used as is; trust/key material properties (e.g. `sslrootcert`, `sslcert`) cannot be combined with it and are rejected, while `ssl_mode` then only controls hostname verification. This supports diskless, in-memory TLS material behind connection pools that only expose `java.util.Properties`.
- Driver and client properties: Separates JDBC-specific properties from passthrough client options used by the underlying `client-v2` transport.
- DataSource support: Provides a JDBC `DataSource` implementation backed by the same driver configuration model.
- Connection lifecycle: Supports connection close, validity checks, ping-based health checks, and network timeout management.
@@ -93,4 +96,5 @@ Compatibility-sensitive traits:
- Date and timestamp setters with `Calendar` are timezone-sensitive by design. Preserving the current day-shift and instant-preserving behavior is important for compatibility.
- `setObject()` temporal behavior is specific and should not drift: `LocalDateTime` and `Instant` are rendered through `fromUnixTimestamp64Nano(...)`, while `Timestamp` and `Date` use quoted textual forms.
- JDBC `ssl_mode` handling is compatibility-sensitive: values are case-insensitive, `none` is aliased to `trust` (the no-verification mode), and an unrecognized value throws `SQLException` during connection configuration. The normalized canonical mode name is forwarded to the underlying `client-v2` transport.
+- Connection `Properties` values must be strings, with one scoped exception: the `ssl_context` key may carry a live `javax.net.ssl.SSLContext` object. Any other non-string property value still throws `IllegalArgumentException` during connection configuration. A string `ssl_context` (supplied via `setProperty` or a URL query parameter) is rejected with `SQLException`, since a string cannot represent a live context.
- INSERT result semantics depend on server-side `async_insert` and `wait_for_async_insert`. The driver does not override these settings, so it follows whatever the server profile or user configuration sets. When `async_insert=1` and `wait_for_async_insert=0`, `Statement.executeUpdate(...)` and `PreparedStatement.executeUpdate(...)` may return `0` (or an under-counted value), and parsing/data errors in the INSERT body may not be reported synchronously as a `SQLException`. Set `async_insert=0` (or `wait_for_async_insert=1`) per connection or statement to restore synchronous row counts and error reporting.
diff --git a/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/SSLExamples.java b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/SSLExamples.java
index 520b82a03..c21d9fb2c 100644
--- a/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/SSLExamples.java
+++ b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/SSLExamples.java
@@ -5,10 +5,16 @@
import com.clickhouse.client.api.query.GenericRecord;
import lombok.extern.slf4j.Slf4j;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
+import java.security.KeyStore;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
import java.util.List;
/**
@@ -26,6 +32,11 @@
*
Connecting to a server with a self-signed certificate without any trust material -
* {@link SSLMode#TRUST} accepts any server certificate and skips hostname verification.
* Use it only for testing or in fully trusted environments.
+ *
Supplying a fully pre-built {@link javax.net.ssl.SSLContext} with
+ * {@link Client.Builder#setSSLContext(javax.net.ssl.SSLContext)} - useful when the trust/key
+ * material is assembled entirely in memory (e.g. fetched and decrypted from a secret store) and
+ * must never be written to disk. The client uses the context as is; {@link SSLMode} then only
+ * controls hostname verification.
*
*
*
More SSL examples (mTLS, trust stores, SNI) will be added to this class later.
@@ -70,6 +81,7 @@ public static void main(String[] args) {
if (rootCert != null) {
connectWithCustomRootCertificate(endpoint, database, user, password, rootCert);
connectWithRootCertificateAsString(endpoint, database, user, password, rootCert);
+ connectWithCustomSSLContext(endpoint, database, user, password, rootCert);
} else {
log.info("chRootCert is not set - skipping the custom CA certificate examples. "
+ "Pass the path to the CA certificate (PEM) that signed the server certificate to run them.");
@@ -88,6 +100,8 @@ public static void main(String[] args) {
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
connectWithRootCertificateAsString(server.getEndpoint(), database,
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
+ connectWithCustomSSLContext(server.getEndpoint(), database,
+ SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
} catch (Exception e) {
log.error("Failed to run the SSL example against a local Docker server", e);
}
@@ -192,6 +206,60 @@ static void connectWithRootCertificateAsString(String endpoint, String database,
}
}
+ /**
+ * Connects using a fully pre-built {@link SSLContext} supplied with
+ * {@link Client.Builder#setSSLContext(SSLContext)}.
+ *
+ *
This mirrors an enterprise use-case where certificates and keys are held only in memory
+ * (for example fetched and decrypted from a secret store) and must never be written to disk.
+ * The whole {@link SSLContext} is assembled by the application and handed to the client, which
+ * uses it as is; the CA certificate, trust store and client certificate/key builder options cannot
+ * be combined with it and are rejected. {@link SSLMode} still applies to server hostname verification
+ * only; here the default {@link SSLMode#STRICT} keeps full verification because the in-memory trust
+ * material validates the whole certificate chain.
+ */
+ static void connectWithCustomSSLContext(String endpoint, String database, String user, String password,
+ String rootCertPath) {
+ final SSLContext sslContext;
+ try {
+ // Build the trust material entirely in memory. In a real application the PEM bytes would come
+ // from a secret manager; here we read the file generated for this example to keep it runnable.
+ byte[] caPem = Files.readAllBytes(Paths.get(rootCertPath));
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ X509Certificate caCert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(caPem));
+
+ KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ trustStore.load(null, null);
+ trustStore.setCertificateEntry("ca", caCert);
+
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init(trustStore);
+
+ sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, tmf.getTrustManagers(), null);
+ } catch (Exception e) {
+ log.error("Failed to build the in-memory SSLContext from {}", rootCertPath, e);
+ return;
+ }
+
+ log.info("Connecting to {} using an application-supplied in-memory SSLContext", endpoint);
+ try (Client client = new Client.Builder()
+ .addEndpoint(endpoint)
+ .setUsername(user)
+ .setPassword(password)
+ .setDefaultDatabase(database)
+ // The client uses this context as is; trust/key builder options cannot be combined with it.
+ .setSSLContext(sslContext)
+ .build()) {
+
+ List rows = client.queryAll("SELECT currentUser() AS user, version() AS version");
+ log.info("Connected securely (custom SSLContext) as '{}' to ClickHouse {}",
+ rows.get(0).getString("user"), rows.get(0).getString("version"));
+ } catch (Exception e) {
+ log.error("Secure connection with a custom SSLContext failed", e);
+ }
+ }
+
private static String trimToNull(String value) {
if (value == null) {
return null;
diff --git a/examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/SSLExamples.java b/examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/SSLExamples.java
index f6a1cef1f..37586ebfc 100644
--- a/examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/SSLExamples.java
+++ b/examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/SSLExamples.java
@@ -4,10 +4,17 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
@@ -31,6 +38,11 @@
* the {@code ssl_mode=trust} connection property accepts any server certificate and skips
* hostname verification ({@code ssl_mode=none} is accepted as an alias). Use it only for
* testing or in fully trusted environments.
+ *
Passing a fully pre-built {@link javax.net.ssl.SSLContext} as a live object in the
+ * connection {@link Properties} under the {@code ssl_context} key - useful when the trust/key
+ * material is assembled entirely in memory (e.g. fetched and decrypted from a secret store) and
+ * must never be written to disk, which is common behind connection pools such as HikariCP. The
+ * driver uses the context as is; {@code ssl_mode} then only controls hostname verification.
*
*
*
More SSL examples (mTLS, trust stores, SNI) will be added to this class later.
@@ -72,6 +84,7 @@ public static void main(String[] args) {
if (rootCert != null) {
connectWithCustomRootCertificate(url, user, password, rootCert);
connectWithRootCertificateAsString(url, user, password, rootCert);
+ connectWithCustomSSLContext(url, user, password, rootCert);
} else {
log.info("chRootCert is not set - skipping the custom CA certificate examples. "
+ "Pass the path to the CA certificate (PEM) that signed the server certificate to run them.");
@@ -93,6 +106,8 @@ public static void main(String[] args) {
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
connectWithRootCertificateAsString(server.getJdbcUrl(),
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
+ connectWithCustomSSLContext(server.getJdbcUrl(),
+ SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
} catch (Exception e) {
log.error("Failed to run the SSL example against a local Docker server", e);
Runtime.getRuntime().exit(-1);
@@ -196,6 +211,64 @@ static void connectWithRootCertificateAsString(String url, String user, String p
}
}
+ /**
+ * Connects by passing a fully pre-built {@link SSLContext} as a live object in the connection
+ * {@link Properties} under the {@code ssl_context} key.
+ *
+ *
This mirrors an enterprise use-case where certificates and keys are held only in memory
+ * (for example fetched and decrypted from a secret store) and must never be written to disk, and
+ * where the application is confined to the {@link java.sql.DriverManager}/{@link Properties} API
+ * behind a connection pool such as HikariCP. Because an {@link SSLContext} cannot be represented
+ * as a string, it is added with {@link Properties#put(Object, Object)} (not
+ * {@link Properties#setProperty(String, String)}). The driver uses the context as is - the
+ * {@code sslrootcert}/{@code sslcert} properties cannot be combined with it and are rejected - and
+ * {@code ssl_mode} (default {@code strict}) then only controls hostname verification.
+ */
+ static void connectWithCustomSSLContext(String url, String user, String password, String rootCertPath)
+ throws SQLException, IOException {
+ // In a real application the PEM content would typically come from an env variable or a secret
+ // manager; here we read the file generated for this example to keep it runnable.
+ byte[] caPem = Files.readAllBytes(Paths.get(rootCertPath));
+
+ final SSLContext sslContext;
+ try {
+ // Assemble the trust material and the SSLContext entirely in memory.
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ X509Certificate caCert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(caPem));
+
+ KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ trustStore.load(null, null);
+ trustStore.setCertificateEntry("ca", caCert);
+
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init(trustStore);
+
+ sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, tmf.getTrustManagers(), null);
+ } catch (GeneralSecurityException | IOException e) {
+ log.error("Failed to build the in-memory SSLContext from {}", rootCertPath, e);
+ return;
+ }
+
+ log.info("Connecting to {} using an application-supplied in-memory SSLContext", url);
+
+ Properties properties = new Properties();
+ properties.setProperty(ClientConfigProperties.USER.getKey(), user); // user
+ properties.setProperty(ClientConfigProperties.PASSWORD.getKey(), password); // password
+ properties.setProperty("ssl", "true"); // enable TLS even if the URL has no https scheme
+ // The SSLContext is a live object, so it is added with put(...) rather than setProperty(...).
+ properties.put(ClientConfigProperties.SSL_CONTEXT.getKey(), sslContext); // ssl_context
+
+ try (Connection connection = DriverManager.getConnection(url, properties);
+ Statement stmt = connection.createStatement();
+ ResultSet rs = stmt.executeQuery("SELECT currentUser() AS user, version() AS version")) {
+ if (rs.next()) {
+ log.info("Connected securely (custom SSLContext) as '{}' to ClickHouse {}",
+ rs.getString("user"), rs.getString("version"));
+ }
+ }
+ }
+
private static String trimToNull(String value) {
if (value == null) {
return null;
diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java
index 99669b981..2e4a85903 100644
--- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java
+++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java
@@ -31,6 +31,8 @@
import java.util.regex.Pattern;
import java.util.stream.Collectors;
+import javax.net.ssl.SSLContext;
+
public class JdbcConfiguration {
public static final Logger LOG = LoggerFactory.getLogger(JdbcConfiguration.class);
@@ -74,6 +76,13 @@ public Map getClientProperties() {
return ImmutableMap.copyOf(clientProperties);
}
+ // A pre-built SSLContext can be passed only as a live object in the connection Properties, so it is
+ // kept apart from the string-only clientProperties and forwarded to the underlying client directly.
+ private SSLContext sslContext;
+ public SSLContext getSslContext() {
+ return sslContext;
+ }
+
private final Map driverProperties;
private final String connectionUrl;
@@ -347,6 +356,11 @@ private void buildFinalProperties(Map urlProperties, Properties
for (Map.Entry