Skip to content

Commit 579e32e

Browse files
Merge origin/main into polyglot/client-v2-ssl-cipher-suites
Resolve conflicts with #2918 (application-supplied SSLContext), which merged to main and touched the same client-v2 SSL surface. Union both SSL features (no behavior change to either): - Client.java: keep both Builder.setSSLCipherSuites(...) and setSSLContext(...) - ClientBuilderTest / JdbcConfigurationTest: keep both sides' test methods - docs/features.md: keep both the cipher-suite and custom-SSLContext bullets - examples client-v2 + jdbc SSLExamples: keep both example methods + call sites - CHANGELOG.md: keep both New Features entries (auto-merged, distinct releases) ClientConfigProperties (SSL_CIPHER_SUITES stays the last enum constant) and HttpAPIClientHelper.createHttpClient (custom-context selection + cipher-suite socket factory) auto-merged cleanly and now integrate both features.
2 parents f2f9acb + 565c59b commit 579e32e

14 files changed

Lines changed: 708 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@
1111

1212
### Bug Fixes
1313

14+
- **[client-v2]** Fixed binary array decoding for nullable element types so `Array(Nullable(Float64))` and similar columns now return boxed arrays such as `Double[]` instead of `Object[]`. This keeps null-supporting arrays aligned with their element type while preserving the existing `Object[]` fallback for Variant/Dynamic/Geometry arrays. (https://github.com/ClickHouse/clickhouse-java/issues/2846)
15+
16+
- **[client-v2]** Fixed `Float32`/`Float64` columns throwing `ClassCastException` when a value of a
17+
non-matching boxed numeric type was supplied through the `Object`-typed insert surface — for example a
18+
`Double` (the natural type of a Java literal like `1.5`) for a `Float32` column, or a `Float` for a
19+
`Float64` column. The `RowBinary` serializer now narrows any `Number` (and, like the `Int*` columns,
20+
a `String`/`Boolean`) through `Number#floatValue()`/`Number#doubleValue()`, so the float columns accept
21+
the same value types the integer columns already did. (https://github.com/ClickHouse/clickhouse-java/issues/2930)
22+
1423
- **[client-v2]** Fixed POJO insert error classification so transport write failures such as java.net.SocketException:
1524
Broken pipe (Write failed) are now surfaced as transfer/network errors instead of being wrapped as
1625
DataSerializationException. This only changes the exception type reported for request-body transport failures during
@@ -87,6 +96,17 @@
8796

8897
### New Features
8998

99+
- **[client-v2, jdbc-v2]** Added support for an application-supplied `javax.net.ssl.SSLContext`. In client-v2,
100+
`Client.Builder.setSSLContext(SSLContext)` hands the client a fully pre-built context that is used as is; in
101+
jdbc-v2 the same context may be passed as a live object in the connection `Properties` under the `ssl_context`
102+
key (added with `Properties.put`, since it is not a string). Trust/key material options cannot be combined with
103+
a custom context and are rejected; `ssl_mode` still applies but only to server hostname verification. This
104+
supports in-memory TLS material that must never be written to disk, including behind connection pools that only
105+
expose `java.util.Properties`.
106+
- 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
107+
- Examples for jdbc-v2 https://github.com/ClickHouse/clickhouse-java/blob/main/examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/SSLExamples.java
108+
(https://github.com/ClickHouse/clickhouse-java/pull/2918, https://github.com/ClickHouse/clickhouse-java/issues/2909)
109+
90110
- **[jdbc-v2, client-v2]** Implemented SSL modes configuration. Now it is possible to set `ssl_mode` to `DISABLED`,
91111
`TRUST`, `VERIFY_CA` and `STRICT`. Note for V1 users: `NONE` is supported only by JDBC driver and mapped to `TRUST`.
92112
Please migrate to the new naming.

client-v2/src/main/java/com/clickhouse/client/api/Client.java

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@
8282
import java.util.function.Supplier;
8383
import java.util.stream.Collectors;
8484

85+
import javax.net.ssl.SSLContext;
86+
8587
/**
8688
* <p>Client is the starting point for all interactions with ClickHouse. </p>
8789
*
@@ -149,8 +151,12 @@ public class Client implements AutoCloseable {
149151

150152
private Client(Collection<Endpoint> endpoints, Map<String,String> configuration,
151153
ExecutorService sharedOperationExecutor, ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy,
152-
Object metricsRegistry, Supplier<String> queryIdGenerator, CredentialsManager cManager) {
154+
Object metricsRegistry, Supplier<String> queryIdGenerator, CredentialsManager cManager,
155+
SSLContext sslContext) {
153156
Map<String, Object> parsedConfiguration = new ConcurrentHashMap<>(ClientConfigProperties.parseConfigMap(configuration));
157+
if (sslContext != null) {
158+
parsedConfiguration.put(ClientConfigProperties.SSL_CONTEXT.getKey(), sslContext);
159+
}
154160
this.credentialsManager = cManager;
155161
this.session = Session.extractFrom(parsedConfiguration);
156162
this.configuration = new ConcurrentHashMap<>(parsedConfiguration);
@@ -271,6 +277,18 @@ public static class Builder {
271277
private ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy;
272278
private Object metricRegistry = null;
273279
private Supplier<String> queryIdGenerator;
280+
private SSLContext sslContext = null;
281+
282+
// Trust/key material options that feed a context the client would otherwise build; none of them
283+
// may be combined with an application-supplied SSLContext (see build()).
284+
private static final ClientConfigProperties[] SSL_MATERIAL_PROPERTIES = {
285+
ClientConfigProperties.SSL_TRUST_STORE,
286+
ClientConfigProperties.SSL_KEYSTORE_TYPE,
287+
ClientConfigProperties.SSL_KEY_STORE_PASSWORD,
288+
ClientConfigProperties.SSL_KEY,
289+
ClientConfigProperties.CA_CERTIFICATE,
290+
ClientConfigProperties.SSL_CERTIFICATE,
291+
};
274292

275293
public Builder() {
276294
this.endpoints = new LinkedHashSet<>();
@@ -345,6 +363,11 @@ private Builder addEndpoint(Endpoint endpoint) {
345363
* @param value - configuration option value
346364
*/
347365
public Builder setOption(String key, String value) {
366+
if (key.equals(ClientConfigProperties.SSL_CONTEXT.getKey())) {
367+
throw new ClientMisconfigurationException("'" + ClientConfigProperties.SSL_CONTEXT.getKey()
368+
+ "' cannot be set as a string; supply a javax.net.ssl.SSLContext object via "
369+
+ "Client.Builder.setSSLContext(...)");
370+
}
348371
this.configuration.put(key, value);
349372
if (key.equals(ClientConfigProperties.PRODUCT_NAME.getKey())) {
350373
setClientName(value);
@@ -802,6 +825,20 @@ public Builder setSSLCipherSuites(String... cipherSuites) {
802825
return this;
803826
}
804827

828+
/**
829+
* Supplies a pre-built {@link SSLContext}. When set, it is used as is instead of a context built
830+
* from the configured trust/key material (which then cannot be set alongside it). {@link SSLMode}
831+
* still applies, but only to server hostname verification: {@link SSLMode#STRICT} (default) enforces
832+
* it while {@link SSLMode#TRUST} and {@link SSLMode#VERIFY_CA} skip it.
833+
*
834+
* @param sslContext a fully configured SSL context; {@code null} clears any previously set context
835+
* @return same instance of the builder
836+
*/
837+
public Builder setSSLContext(SSLContext sslContext) {
838+
this.sslContext = sslContext;
839+
return this;
840+
}
841+
805842
/**
806843
* Configure client to use server timezone for date/datetime columns. Default is true.
807844
* If this options is selected then server timezone should be set as well.
@@ -1182,14 +1219,28 @@ public Client build() {
11821219

11831220
CredentialsManager cManager = new CredentialsManager(this.configuration);
11841221

1185-
if (configuration.containsKey(ClientConfigProperties.SSL_TRUST_STORE.getKey()) &&
1222+
// A textual 'ssl_context' can never be a live context (also rejected in setOption).
1223+
if (configuration.containsKey(ClientConfigProperties.SSL_CONTEXT.getKey())) {
1224+
throw new ClientMisconfigurationException("'" + ClientConfigProperties.SSL_CONTEXT.getKey()
1225+
+ "' cannot be set as a string; supply a javax.net.ssl.SSLContext object via "
1226+
+ "Client.Builder.setSSLContext(...)");
1227+
}
1228+
1229+
if (this.sslContext != null) {
1230+
// A custom SSLContext replaces any context the client would build, so trust/key material
1231+
// cannot be set alongside it. SSL_MODE (hostname verification) is still allowed.
1232+
for (ClientConfigProperties material : SSL_MATERIAL_PROPERTIES) {
1233+
if (configuration.containsKey(material.getKey())) {
1234+
throw new ClientMisconfigurationException("'" + material.getKey() + "' cannot be combined"
1235+
+ " with a custom SSLContext; the supplied context is used as is. Only 'ssl_mode'"
1236+
+ " (hostname verification) may be set alongside it.");
1237+
}
1238+
}
1239+
} else if (configuration.containsKey(ClientConfigProperties.SSL_TRUST_STORE.getKey()) &&
11861240
configuration.containsKey(ClientConfigProperties.SSL_CERTIFICATE.getKey())) {
11871241
throw new ClientMisconfigurationException("Trust store and certificates cannot be used together");
11881242
}
11891243

1190-
// A trust store and a CA certificate are not rejected here: for VERIFY_CA/STRICT the trust
1191-
// store takes precedence and the CA certificate is ignored with a warning (see createSSLContext).
1192-
11931244
// Resolve ssl_mode case-insensitively and normalize it to the canonical enum name so that
11941245
// downstream parsing is consistent and an unknown value is reported as a misconfiguration
11951246
// here instead of failing later with a generic enum-parsing error.
@@ -1246,7 +1297,8 @@ public Client build() {
12461297
}
12471298

12481299
return new Client(this.endpoints, this.configuration, this.sharedOperationExecutor,
1249-
this.columnToMethodMatchingStrategy, this.metricRegistry, this.queryIdGenerator, cManager);
1300+
this.columnToMethodMatchingStrategy, this.metricRegistry, this.queryIdGenerator, cManager,
1301+
this.sslContext);
12501302
}
12511303
}
12521304

client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import org.slf4j.Logger;
99
import org.slf4j.LoggerFactory;
1010

11+
import javax.net.ssl.SSLContext;
12+
1113
import java.util.ArrayList;
1214
import java.util.Arrays;
1315
import java.util.Collection;
@@ -118,6 +120,8 @@ public enum ClientConfigProperties {
118120

119121
SSL_MODE("ssl_mode", SSLMode.class, SSLMode.STRICT.name()),
120122

123+
SSL_CONTEXT("ssl_context", SSLContext.class),
124+
121125
RETRY_ON_FAILURE("retry", Integer.class, "3"),
122126

123127
INPUT_OUTPUT_FORMAT("format", ClickHouseFormat.class),

client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -609,8 +609,8 @@ public ArrayValue readArray(ClickHouseColumn column) throws IOException {
609609
ArrayValue array;
610610
ClickHouseColumn itemTypeColumn = column.getNestedColumns().get(0);
611611
if (len == 0) {
612-
Class<?> itemClass = itemTypeColumn.getDataType().getPrimitiveClass();
613-
array = new ArrayValue(itemClass == null ? Object.class : itemClass, 0);
612+
Class<?> itemClass = resolveArrayItemClass(itemTypeColumn);
613+
array = new ArrayValue(itemClass, 0);
614614
} else if (column.getArrayNestedLevel() == 1) {
615615
array = readArrayItem(itemTypeColumn, len);
616616
} else {
@@ -625,8 +625,13 @@ public ArrayValue readArray(ClickHouseColumn column) throws IOException {
625625

626626
public ArrayValue readArrayItem(ClickHouseColumn itemTypeColumn, int len) throws IOException {
627627
ArrayValue array;
628-
if (itemTypeColumn.isNullable()
629-
|| itemTypeColumn.getDataType() == ClickHouseDataType.Variant
628+
if (itemTypeColumn.isNullable()) {
629+
Class<?> itemClass = resolveArrayItemClass(itemTypeColumn);
630+
array = new ArrayValue(itemClass, len);
631+
for (int i = 0; i < len; i++) {
632+
array.set(i, readValue(itemTypeColumn));
633+
}
634+
} else if (itemTypeColumn.getDataType() == ClickHouseDataType.Variant
630635
|| itemTypeColumn.getDataType() == ClickHouseDataType.Dynamic
631636
|| itemTypeColumn.getDataType() == ClickHouseDataType.Geometry) {
632637
array = new ArrayValue(Object.class, len);
@@ -667,6 +672,64 @@ public ArrayValue readArrayItem(ClickHouseColumn itemTypeColumn, int len) throws
667672
return array;
668673
}
669674

675+
/**
676+
* Resolves the Java class that {@link #readValue} actually returns for a given column
677+
* so that it can be used as the component type of an array.
678+
*
679+
* <p>For unsigned integer types, {@code readValue} widens the value (e.g. UInt8 → Short,
680+
* UInt32 → Long), so we use {@link ClickHouseDataType#getWiderObjectClass()} or
681+
* {@link ClickHouseDataType#getWiderPrimitiveClass()} which mirrors that widening.
682+
* For Enum types, {@code readValue} returns {@link EnumValue} rather than the
683+
* declared {@code String.class}. All other types use {@link ClickHouseDataType#getObjectClass()}
684+
* or {@link ClickHouseDataType#getPrimitiveClass()}.
685+
*
686+
* @param itemTypeColumn the element column of the array
687+
* @return the Java class to use as the array component type; never {@code null}
688+
*/
689+
private static Class<?> resolveArrayItemClass(ClickHouseColumn itemTypeColumn) {
690+
ClickHouseDataType dataType = itemTypeColumn.getDataType();
691+
if (itemTypeColumn.isNullable()) {
692+
switch (dataType) {
693+
case UInt8:
694+
case UInt16:
695+
case UInt32:
696+
case UInt64:
697+
return dataType.getWiderObjectClass();
698+
case Enum8:
699+
case Enum16:
700+
return EnumValue.class;
701+
default:
702+
Class<?> cls = dataType.getObjectClass();
703+
return cls == null ? Object.class : cls;
704+
}
705+
} else {
706+
switch (dataType) {
707+
case UInt8:
708+
case UInt16:
709+
case UInt32:
710+
case UInt64:
711+
return dataType.getWiderPrimitiveClass();
712+
case Enum8:
713+
case Enum16:
714+
return EnumValue.class;
715+
case Variant:
716+
case Dynamic:
717+
case Geometry:
718+
return Object.class;
719+
case Array:
720+
return ArrayValue.class;
721+
case Tuple:
722+
case Nested:
723+
return Object[].class;
724+
case Map:
725+
return Map.class;
726+
default:
727+
Class<?> cls = dataType.getPrimitiveClass();
728+
return cls == null ? Object.class : cls;
729+
}
730+
}
731+
}
732+
670733
public void skipValue(ClickHouseColumn column) throws IOException {
671734
readValue(column, null);
672735
}

client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/SerializerUtils.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -555,10 +555,10 @@ private static void serializePrimitiveData(OutputStream stream, Object value, Cl
555555
BinaryStreamUtils.writeUnsignedInt256(stream, NumberConverter.toBigInteger(value));
556556
break;
557557
case Float32:
558-
BinaryStreamUtils.writeFloat32(stream, (float) value);
558+
BinaryStreamUtils.writeFloat32(stream, NumberConverter.toFloat(value));
559559
break;
560560
case Float64:
561-
BinaryStreamUtils.writeFloat64(stream, (double) value);
561+
BinaryStreamUtils.writeFloat64(stream, NumberConverter.toDouble(value));
562562
break;
563563
case Decimal:
564564
case Decimal32:

client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ public HttpAPIClientHelper(Map<String, Object> configuration, Object metricsRegi
154154
}
155155

156156
/**
157-
* Creates or returns default SSL context.
157+
* Creates an SSL context from the configured trust/key material.
158158
*
159159
* @return SSLContext
160160
*/
@@ -280,7 +280,21 @@ private HttpClientConnectionManager poolConnectionManager(LayeredConnectionSocke
280280
public CloseableHttpClient createHttpClient(boolean initSslContext, Map<String, Object> configuration) {
281281
// Top Level builders
282282
HttpClientBuilder clientBuilder = HttpClientBuilder.create();
283-
SSLContext sslContext = initSslContext ? createSSLContext(configuration) : null;
283+
// An application-supplied SSLContext is used as is; otherwise one is built from the configured
284+
// trust/key material. Server hostname verification below still applies via the SSL mode.
285+
SSLContext sslContext = null;
286+
if (initSslContext) {
287+
Object customSSLContext = configuration.get(ClientConfigProperties.SSL_CONTEXT.getKey());
288+
if (customSSLContext == null) {
289+
sslContext = createSSLContext(configuration);
290+
} else if (customSSLContext instanceof SSLContext) {
291+
sslContext = (SSLContext) customSSLContext;
292+
} else {
293+
throw new ClientMisconfigurationException("'" + ClientConfigProperties.SSL_CONTEXT.getKey()
294+
+ "' must be a javax.net.ssl.SSLContext instance but was "
295+
+ customSSLContext.getClass().getName() + "; supply it via Client.Builder.setSSLContext(...)");
296+
}
297+
}
284298
LayeredConnectionSocketFactory sslConnectionSocketFactory;
285299
if (sslContext != null) {
286300
String socketSNI = (String)configuration.get(ClientConfigProperties.SSL_SOCKET_SNI.getKey());

0 commit comments

Comments
 (0)