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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
previously set in milliseconds but mistakenly retrieved and used in seconds in some places. Now it correctly uses
milliseconds consistently. (https://github.com/ClickHouse/clickhouse-java/issues/2358)

- **[client-v2]** The public `ClickHouseBinaryFormatWriter` interface gained two methods, `setString(String, byte[])` and `setString(int, byte[])`, for writing raw `String`/`FixedString` bytes. Code that only *uses* the interface is unaffected, but any third party that *implements* `ClickHouseBinaryFormatWriter` directly is source- and binary-incompatible until it adds these methods (recompiling against the new version is required; otherwise an `AbstractMethodError` can occur at runtime).

### New Features

- **[jdbc-v2, client-v2]** Implemented SSL modes configuration. Now it is possible to set `ssl_mode` to `DISABLED`,
Expand Down Expand Up @@ -101,6 +103,19 @@
supported but will be removed. Please migrate to the new property.
(https://github.com/ClickHouse/clickhouse-java/issues/2858)

- **[client-v2, jdbc-v2]** Added opt-in binary string support through the `binary_string_support` configuration property
(or `Client.Builder#binaryStringSupport(boolean)`), disabled by default. The setting is resolved per operation from
the merged client and query settings, so it can be overridden for a single request via the `binary_string_support`
operation option (e.g. `QuerySettings#setOption(ClientConfigProperties.BINARY_STRING_SUPPORT.getKey(), true)`)
independently of the client-level default. When enabled, top-level `String` and `FixedString` columns are read
into a `StringValue` that preserves the raw bytes instead of decoding them into a `String`, allowing non-UTF-8/binary
content to round-trip byte-for-byte. `StringValue` exposes the bytes via `toByteArray()`/`asByteBuffer()` and
lazily decodes a `String` via `asString()` (UTF-8 by default, or a caller-supplied `Charset`). Values nested inside
containers (`Array`, `Map`, `Tuple`, `Nested`, `Variant`) continue to be read as `String`, since those types are not
expected to carry large/binary strings. On the JDBC side, `ResultSet#getBinaryStream(int)` and
`ResultSet#getBinaryStream(String)` are now implemented (previously unsupported) and, together with `getBytes(...)`,
return the raw column bytes.

### Improvements

- **[jdbc-v2, client-v2]** Added support of hostnames with underscore (`_`) in them. Now it is possible to specify endpoint
Expand Down
15 changes: 15 additions & 0 deletions client-v2/src/main/java/com/clickhouse/client/api/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.clickhouse.client.api.command.CommandResponse;
import com.clickhouse.client.api.command.CommandSettings;
import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader;
import com.clickhouse.client.api.data_formats.ClickHouseFormatReader;
import com.clickhouse.client.api.data_formats.NativeFormatReader;
import com.clickhouse.client.api.data_formats.RowBinaryFormatReader;
import com.clickhouse.client.api.data_formats.RowBinaryWithNamesAndTypesFormatReader;
Expand Down Expand Up @@ -1119,6 +1120,20 @@ public Builder typeHintMapping(Map<ClickHouseDataType, Class<?>> typeHintMapping
return this;
}

/**
* Enables reading {@code String} and {@code FixedString} columns into an intermediate {@code byte[]}
* (a new array each time) instead of decoding them into a {@link String}. This improves working with
* large strings and allows {@link ClickHouseFormatReader#getByteArray} to be used more effectively. Can also be configured
* per operation.
*
* @param enable - if the feature is enabled
* @return this builder instance
*/
public Builder binaryStringSupport(boolean enable) {
this.configuration.put(ClientConfigProperties.BINARY_STRING_SUPPORT.getKey(), String.valueOf(enable));
return this;
}


/**
* SNI SSL parameter that will be set for each outbound SSL socket.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.clickhouse.client.api;

import com.clickhouse.client.api.data_formats.ClickHouseFormatReader;
import com.clickhouse.client.api.data_formats.internal.AbstractBinaryFormatReader;
import com.clickhouse.client.api.enums.SSLMode;
import com.clickhouse.client.api.internal.ClickHouseLZ4OutputStream;
Expand Down Expand Up @@ -181,6 +182,13 @@ public Object parseValue(String value) {
*/
TYPE_HINT_MAPPING("type_hint_mapping", Map.class),

/**
* When enabled, {@code String} and {@code FixedString} columns are read into an intermediate {@code byte[]}
* instead of decoding them into a {@link String}. Improves working with large strings and lets
* {@link ClickHouseFormatReader#getByteArray} be used more effectively. Can be configured per operation.
*/
BINARY_STRING_SUPPORT("binary_string_support", Boolean.class, "false"),

/**
* SNI SSL parameter that will be set for each outbound SSL socket.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ public interface ClickHouseBinaryFormatWriter {

void setString(int colIndex, String value);

void setString(String column, byte[] value);

void setString(int colIndex, byte[] value);

void setDate(String column, LocalDate value);

void setDate(int colIndex, LocalDate value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,18 @@ public void writeString(String value) throws IOException {
BinaryStreamUtils.writeString(out, value);
}

public void writeString(byte[] value) throws IOException {
BinaryStreamUtils.writeString(out, value);
}

public void writeFixedString(String value, int len) throws IOException {
BinaryStreamUtils.writeFixedString(out, value, len);
}

public void writeFixedString(byte[] value, int len) throws IOException {
SerializerUtils.writeFixedStringBytes(out, value, len);
}

public void writeDate(ZonedDateTime value) throws IOException {
SerializerUtils.writeDate(out, value, value.getZone());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,16 @@ public void setString(int colIndex, String value) {
setValue(colIndex, value);
}

@Override
public void setString(String column, byte[] value) {
setValue(column, value);
}

@Override
public void setString(int colIndex, byte[] value) {
setValue(colIndex, value);
}

@Override
public void setDate(String column, LocalDate value) {
setValue(column, value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public RowBinaryWithNamesFormatReader(InputStream inputStream,
TableSchema schema,
BinaryStreamReader.ByteBufferAllocator byteBufferAllocator,
Map<ClickHouseDataType, Class<?>> typeHintMapping) {
super(inputStream, querySettings, schema, byteBufferAllocator, typeHintMapping);
super(inputStream, querySettings, schema, byteBufferAllocator, typeHintMapping);
int nCol = 0;
try {
nCol = BinaryStreamReader.readVarInt(input);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
private long row = -1; // before first row
private long lastNextCallTs; // for exception to detect slow reader

protected AbstractBinaryFormatReader(InputStream inputStream, QuerySettings querySettings, TableSchema schema,BinaryStreamReader.ByteBufferAllocator byteBufferAllocator, Map<ClickHouseDataType, Class<?>> defaultTypeHintMap) {
protected AbstractBinaryFormatReader(InputStream inputStream, QuerySettings querySettings, TableSchema schema, BinaryStreamReader.ByteBufferAllocator byteBufferAllocator, Map<ClickHouseDataType, Class<?>> defaultTypeHintMap) {
this.input = inputStream;
Map<String, Object> settings = querySettings == null ? Collections.emptyMap() : querySettings.getAllSettings();
Boolean useServerTimeZone = (Boolean) settings.get(ClientConfigProperties.USE_SERVER_TIMEZONE.getKey());
Expand All @@ -88,8 +88,12 @@
}
boolean jsonAsString = MapUtils.getFlag(settings,
ClientConfigProperties.serverSetting(ServerSettings.OUTPUT_FORMAT_BINARY_WRITE_JSON_AS_STRING), false);
// Binary string support is resolved from the (already merged client + operation) query settings so it can be
// toggled per operation, not just per client.
boolean binaryStringSupport = MapUtils.getFlag(settings,
ClientConfigProperties.BINARY_STRING_SUPPORT.getKey(), false);
this.binaryStreamReader = new BinaryStreamReader(inputStream, timeZone, LOG, byteBufferAllocator, jsonAsString,
defaultTypeHintMap);
defaultTypeHintMap, binaryStringSupport);
if (schema != null) {
setSchema(schema);
}
Expand Down Expand Up @@ -532,8 +536,9 @@
}
return (T)array;
} else if (componentType == byte.class) {
if (value instanceof String) {
return (T) ((String) value).getBytes(StandardCharsets.UTF_8);
byte[] bytes = stringLikeToBytes(value);
if (bytes != null) {
return (T) bytes;
} else if (value instanceof InetAddress) {
return (T) ((InetAddress) value).getAddress();
}
Expand Down Expand Up @@ -676,6 +681,24 @@
throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to Instant");
}

/**
* Converts a string-like value into its raw bytes. For a {@link StringValue} the original bytes are
* returned without re-encoding (so binary content is preserved). For a {@link String} the bytes are
* produced using UTF-8, matching the historical behaviour. Returns {@code null} when the value is not
* a string-like type so callers can fall back to other handling.
*
* @param value value to convert
* @return raw bytes or {@code null} if the value is not string-like
*/
public static byte[] stringLikeToBytes(Object value) {
if (value instanceof StringValue) {
return ((StringValue) value).toByteArray();
} else if (value instanceof String) {
return ((String) value).getBytes(StandardCharsets.UTF_8);
}
return null;

Check warning on line 699 in client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Return an empty array instead of null.

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

static Instant objectToInstant(Object value) {
if (value instanceof LocalDateTime) {
LocalDateTime dateTime = (LocalDateTime) value;
Expand Down Expand Up @@ -866,6 +889,10 @@
BinaryStreamReader.ArrayValue array = (BinaryStreamReader.ArrayValue) value;
if (array.itemType == String.class) {
return (String[]) array.getArray();
} else if (array.itemType == StringValue.class) {
StringValue[] stringValues = (StringValue[]) array.getArray();
return Arrays.stream(stringValues)
.map(sv -> sv == null ? null : sv.asString()).toArray(String[]::new);
} else if (array.itemType == BinaryStreamReader.EnumValue.class) {
BinaryStreamReader.EnumValue[] enumValues = (BinaryStreamReader.EnumValue[]) array.getArray();
return Arrays.stream(enumValues).map(BinaryStreamReader.EnumValue::getName).toArray(String[]::new);
Expand Down
Loading
Loading