From 33270ad2d07986cce6af22de1834edece83aca1d Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 31 Mar 2026 14:06:34 -0700 Subject: [PATCH 1/7] Implemented reading string to bytebuffers --- .../internal/AbstractBinaryFormatReader.java | 12 +- .../internal/BinaryStreamReader.java | 106 +++++++++++++++++- .../data_formats/internal/BinaryString.java | 18 +++ .../api/internal/DataTypeConverter.java | 34 ++++-- 4 files changed, 150 insertions(+), 20 deletions(-) create mode 100644 client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryString.java diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java index 8abcdc65b..8b2f64ab8 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java @@ -32,6 +32,7 @@ import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; @@ -87,7 +88,7 @@ protected AbstractBinaryFormatReader(InputStream inputStream, QuerySettings quer boolean jsonAsString = MapUtils.getFlag(settings, ClientConfigProperties.serverSetting(ServerSettings.OUTPUT_FORMAT_BINARY_WRITE_JSON_AS_STRING), false); this.binaryStreamReader = new BinaryStreamReader(inputStream, timeZone, LOG, byteBufferAllocator, jsonAsString, - defaultTypeHintMap); + defaultTypeHintMap, ByteBuffer::allocate); if (schema != null) { setSchema(schema); } @@ -208,13 +209,18 @@ public T readValue(int colIndex) { if (colIndex < 1 || colIndex > getSchema().getColumns().size()) { throw new ClientException("Column index out of bounds: " + colIndex); } - return (T) currentRecord[colIndex - 1]; + + T value = (T) currentRecord[colIndex - 1]; + if (value instanceof BinaryString) { + return (T) ((BinaryString) value).asString(); + } + return value; } @SuppressWarnings("unchecked") @Override public T readValue(String colName) { - return (T) currentRecord[getSchema().nameToIndex(colName)]; + return readValue(getSchema().nameToColumnIndex(colName)); } @Override diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java index 04acf3a2b..cc8e72954 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java @@ -17,6 +17,8 @@ import java.math.BigInteger; import java.net.Inet4Address; import java.net.Inet6Address; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; @@ -33,6 +35,7 @@ import java.util.Map; import java.util.TimeZone; import java.util.UUID; +import java.util.function.Function; /** * This class is not thread safe and should not be shared between multiple threads. @@ -51,6 +54,8 @@ public class BinaryStreamReader { private final ByteBufferAllocator bufferAllocator; + private final StringBufferAllocator stringBufferAllocator; + private final boolean jsonAsString; private final Class arrayDefaultTypeHint; @@ -69,11 +74,17 @@ public class BinaryStreamReader { * @param jsonAsString - use string to serialize/deserialize JSON columns * @param typeHintMapping - what type use as hint if hint is not set or may not be known. */ - BinaryStreamReader(InputStream input, TimeZone timeZone, Logger log, ByteBufferAllocator bufferAllocator, boolean jsonAsString, Map> typeHintMapping) { + BinaryStreamReader(InputStream input, TimeZone timeZone, Logger log, + ByteBufferAllocator bufferAllocator, + boolean jsonAsString, + Map> typeHintMapping, + StringBufferAllocator stringBufferAllocator) { this.log = log == null ? NOPLogger.NOP_LOGGER : log; this.timeZone = timeZone; this.input = input; this.bufferAllocator = bufferAllocator; + this.stringBufferAllocator = stringBufferAllocator; this.jsonAsString = jsonAsString; this.arrayDefaultTypeHint = typeHintMapping == null || @@ -121,13 +132,11 @@ public T readValue(ClickHouseColumn column, Class typeHint) throws IOExce switch (dataType) { // Primitives case FixedString: { - byte[] bytes = precision > STRING_BUFF.length ? - new byte[precision] : STRING_BUFF; - readNBytes(input, bytes, 0, precision); - return (T) new String(bytes, 0, precision, StandardCharsets.UTF_8); + return (T) readBytesToBuffer(precision, stringBufferAllocator::allocate); } case String: { - return (T) readString(); + int len = readVarInt(input); + return (T) readBytesToBuffer(len, stringBufferAllocator::allocate); } case Int8: return (T) Byte.valueOf(readByte()); @@ -1111,6 +1120,28 @@ public String readString() throws IOException { return new String(dest, 0, len, StandardCharsets.UTF_8); } + public BinaryString readBytesToBuffer(int len, Function bufferAllocator) throws IOException { + ByteBuffer buffer = null; + if (len > 0) { + buffer = bufferAllocator.apply(len); + if (buffer == null) { + throw new IOException("bufferAllocator returned `null`"); + } + if (buffer.hasArray()) { + readNBytes(input, buffer.array(), 0, len); + } else { + int left = len; + while (left > 0) { + int chunkSize = Math.min(STRING_BUFF.length, left); + readNBytes(input, STRING_BUFF, 0, chunkSize); + buffer.put(STRING_BUFF, 0, chunkSize); + left -= chunkSize; + } + } + } + return buffer == null ? null : new BinaryStringImpl(buffer); + } + /** * Reads a decimal value from input stream. * @param input - source of bytes @@ -1137,6 +1168,10 @@ public interface ByteBufferAllocator { byte[] allocate(int size); } + public interface StringBufferAllocator { + ByteBuffer allocate(int size); + } + /** * Byte allocator that creates a new byte array for each request. */ @@ -1391,4 +1426,63 @@ private Map readJsonData(InputStream input, ClickHouseColumn col } return obj; } + + static final class BinaryStringImpl implements BinaryString { + + private final ByteBuffer buffer; + private final int len; + private CharBuffer charBuffer = null; + private String strValue = null; + + BinaryStringImpl(ByteBuffer buffer) { + this.buffer = buffer; + this.len = buffer.position(); + } + + @Override + public ByteBuffer rawBuffer() { + return buffer; + } + + @Override + public String asString() { + if (strValue == null) { + if (buffer.hasArray()) { + strValue = new String(buffer.array(), StandardCharsets.UTF_8); + } else { + ensureCharBuffer(); + strValue = charBuffer.toString(); + } + } + return strValue; + } + + @Override + public int length() { + return len; + } + + @Override + public char charAt(int index) { + ensureCharBuffer(); + return charBuffer.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + ensureCharBuffer(); + return charBuffer.subSequence(start, end); + } + + private void ensureCharBuffer() { + if (charBuffer == null) { + charBuffer = buffer.asCharBuffer(); + } + } + + @Override + public int compareTo(String o) { + return asString().compareTo(o); + } + } } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryString.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryString.java new file mode 100644 index 000000000..45d30b8bf --- /dev/null +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryString.java @@ -0,0 +1,18 @@ +package com.clickhouse.client.api.data_formats.internal; + +import java.nio.ByteBuffer; + +public interface BinaryString extends Comparable, CharSequence { + + /** + * Returns a backing byte buffer or creates one + * @return ByteBuffer instance. + */ + ByteBuffer rawBuffer(); + + /** + * Converts raw bytes to a string whenever size is. + * @return String object + */ + String asString(); +} diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/DataTypeConverter.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/DataTypeConverter.java index de4830e9f..ee00e2d60 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/internal/DataTypeConverter.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/DataTypeConverter.java @@ -3,6 +3,8 @@ import com.clickhouse.client.api.ClickHouseException; import com.clickhouse.client.api.DataTypeUtils; import com.clickhouse.client.api.data_formats.internal.BinaryStreamReader; +import com.clickhouse.client.api.data_formats.internal.BinaryString; +import com.clickhouse.client.api.data_formats.internal.ValueConverters; import com.clickhouse.data.ClickHouseColumn; import com.clickhouse.data.ClickHouseDataType; @@ -36,6 +38,8 @@ public class DataTypeConverter { private final ArrayAsStringWriter arrayAsStringWriter = new ArrayAsStringWriter(); + private final ValueConverters valueConverters = new ValueConverters(); + public String convertToString(Object value, ClickHouseColumn column) { if (value == null) { return null; @@ -72,21 +76,29 @@ public String convertToString(Object value, ClickHouseColumn column) { } public String stringToString(Object bytesOrString, ClickHouseColumn column) { - StringBuilder sb = new StringBuilder(); if (column.isArray()) { + StringBuilder sb = new StringBuilder(); sb.append(QUOTE); - } - if (bytesOrString instanceof CharSequence) { - sb.append(((CharSequence) bytesOrString)); - } else if (bytesOrString instanceof byte[]) { - sb.append(new String((byte[]) bytesOrString)); - } else { - sb.append(bytesOrString); - } - if (column.isArray()) { + if (bytesOrString instanceof BinaryString) { + sb.append(((BinaryString)bytesOrString).asString()); // string will be cached + } else if (bytesOrString instanceof CharSequence) { + sb.append(((CharSequence) bytesOrString)); + } else if (bytesOrString instanceof byte[]) { + sb.append(new String((byte[]) bytesOrString)); + } else { + sb.append(bytesOrString); + } sb.append(QUOTE); + return sb.toString(); + } else { + if (bytesOrString instanceof BinaryString) { + return ((BinaryString)bytesOrString).asString(); // string will be cached + } else if (bytesOrString instanceof byte[]) { + return new String((byte[]) bytesOrString); + } else { + return bytesOrString.toString(); + } } - return sb.toString(); } public static ZoneId UTC_ZONE_ID = ZoneId.of("UTC"); From f9ab74cfa0e03886a394fbc3edfb594ac45726ff Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Wed, 8 Apr 2026 14:30:46 -0700 Subject: [PATCH 2/7] Fixed build --- .../internal/BinaryStreamReaderTests.java | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java index 0d94e0a5f..0aeaddf6b 100644 --- a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java @@ -190,18 +190,4 @@ public void testArrayValue() throws Exception { Object[] array2 = array.getArrayOfObjects(); Assert.assertEquals(array1.length, array2.length); } - - @Test - public void testReadNullVariantReturnsNull() throws Exception { - ClickHouseColumn column = ClickHouseColumn.of("v", "Variant(Int32, String)"); - BinaryStreamReader reader = new BinaryStreamReader( - new ByteArrayInputStream(new byte[]{(byte) 0xFF}), - TimeZone.getTimeZone("UTC"), - null, - new BinaryStreamReader.CachingByteBufferAllocator(), - false, - null); - - Assert.assertNull(reader.readValue(column)); - } } From 4867db61f51cf84beb40ae3da3c47c24614df3ba Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Thu, 9 Apr 2026 11:48:11 -0700 Subject: [PATCH 3/7] Finalized impl. This is too complex to handle and UX will be terrible --- .../client/api/ClientConfigProperties.java | 6 +- .../internal/AbstractBinaryFormatReader.java | 22 ++++-- .../internal/BinaryStreamReader.java | 67 ++++++++++------- .../data_formats/internal/BinaryString.java | 11 +-- .../internal/BinaryStreamReaderTests.java | 45 +++++++++-- .../client/datatypes/DataTypeTests.java | 75 ++++++++++++++++++- 6 files changed, 180 insertions(+), 46 deletions(-) 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 e548a90f9..625125fd7 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 @@ -1,6 +1,7 @@ package com.clickhouse.client.api; import com.clickhouse.client.api.data_formats.internal.AbstractBinaryFormatReader; +import com.clickhouse.client.api.data_formats.internal.BinaryString; import com.clickhouse.client.api.internal.ClickHouseLZ4OutputStream; import com.clickhouse.data.ClickHouseDataType; import com.clickhouse.data.ClickHouseFormat; @@ -176,7 +177,10 @@ public Object parseValue(String value) { * Defines mapping between ClickHouse data type and target Java type * Used by binary readers to convert values into desired Java type. */ - TYPE_HINT_MAPPING("type_hint_mapping", Map.class), + TYPE_HINT_MAPPING("type_hint_mapping", Map.class, + "String=" + BinaryString.class.getName() + + ), /** * SNI SSL parameter that will be set for each outbound SSL socket. diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java index 5ed268acc..0d345b29c 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java @@ -71,14 +71,19 @@ public abstract class AbstractBinaryFormatReader implements ClickHouseBinaryForm private TableSchema schema; private ClickHouseColumn[] columns; + private Class[] columnTypeHints; private Map[] convertions; + private Map> defaultTypeHintMap; private boolean hasNext = true; private boolean initialState = true; // reader is in initial state, no records have been read yet 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> defaultTypeHintMap) { + protected AbstractBinaryFormatReader(InputStream inputStream, QuerySettings querySettings, TableSchema schema, + BinaryStreamReader.ByteBufferAllocator byteBufferAllocator, + Map> defaultTypeHintMap) { this.input = inputStream; + this.defaultTypeHintMap = defaultTypeHintMap; Map settings = querySettings == null ? Collections.emptyMap() : querySettings.getAllSettings(); Boolean useServerTimeZone = (Boolean) settings.get(ClientConfigProperties.USE_SERVER_TIMEZONE.getKey()); TimeZone timeZone = (useServerTimeZone == Boolean.TRUE && querySettings != null) ? @@ -189,7 +194,7 @@ protected boolean readRecord(Object[] record) throws IOException { boolean firstColumn = true; for (int i = 0; i < columns.length; i++) { try { - Object val = binaryStreamReader.readValue(columns[i]); + Object val = binaryStreamReader.readValue(columns[i], columnTypeHints[i]); if (val != null) { record[i] = val; } else { @@ -306,16 +311,21 @@ protected void setSchema(TableSchema schema) { this.schema = schema; this.columns = schema.getColumns().toArray(ClickHouseColumn.EMPTY_ARRAY); this.convertions = new Map[columns.length]; - + this.columnTypeHints = new Class[columns.length]; this.currentRecord = new Object[columns.length]; this.nextRecord = new Object[columns.length]; + Class stringTypeHint = defaultTypeHintMap.get(ClickHouseDataType.String); + for (int i = 0; i < columns.length; i++) { ClickHouseColumn column = columns[i]; ClickHouseDataType columnDataType = column.getDataType(); if (columnDataType.equals(ClickHouseDataType.SimpleAggregateFunction)){ columnDataType = column.getNestedColumns().get(0).getDataType(); } + if (columnDataType.equals(ClickHouseDataType.String)) { + columnTypeHints[i] = stringTypeHint; + } switch (columnDataType) { case Int8: case Int16: @@ -536,9 +546,11 @@ private T getPrimitiveArray(int index, Class componentType) { for (int i = 0; i < list.size(); i++) { Array.set(array, i, list.get(i)); } - return (T)array; + return (T) array; } else if (componentType == byte.class) { - if (value instanceof String) { + if (value instanceof BinaryString) { + return (T) ((BinaryString)value).asBytes(); + } else if(value instanceof String) { return (T) ((String) value).getBytes(StandardCharsets.UTF_8); } else if (value instanceof InetAddress) { return (T) ((InetAddress) value).getAddress(); diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java index 8a43a69bd..68d0467d2 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java @@ -132,11 +132,19 @@ public T readValue(ClickHouseColumn column, Class typeHint) throws IOExce switch (dataType) { // Primitives case FixedString: { - return (T) readBytesToBuffer(precision, stringBufferAllocator::allocate); + if (typeHint == BinaryString.class) { + return (T) readBinaryString(precision, stringBufferAllocator::allocate); + } else { + return (T) readString(input, precision); + } } case String: { - int len = readVarInt(input); - return (T) readBytesToBuffer(len, stringBufferAllocator::allocate); + if (typeHint == BinaryString.class) { + int len = readVarInt(input); + return (T) readBinaryString(len, stringBufferAllocator::allocate); + } else { + return (T) readString(input); + } } case Int8: return (T) Byte.valueOf(readByte()); @@ -636,10 +644,10 @@ public ArrayValue readArrayItem(ClickHouseColumn itemTypeColumn, int len) throws if (itemTypeColumn.isNullable() || itemTypeColumn.getDataType() == ClickHouseDataType.Variant) { array = new ArrayValue(Object.class, len); for (int i = 0; i < len; i++) { - array.set(i, readValue(itemTypeColumn)); + array.set(i, readArrayItemValue(itemTypeColumn)); } } else { - Object firstValue = readValue(itemTypeColumn); + Object firstValue = readArrayItemValue(itemTypeColumn); Class itemClass = firstValue.getClass(); if (firstValue instanceof Byte) { itemClass = byte.class; @@ -666,12 +674,17 @@ public ArrayValue readArrayItem(ClickHouseColumn itemTypeColumn, int len) throws array = new ArrayValue(itemClass, len); array.set(0, firstValue); for (int i = 1; i < len; i++) { - array.set(i, readValue(itemTypeColumn)); + array.set(i, readArrayItemValue(itemTypeColumn)); } } return array; } + private Object readArrayItemValue(ClickHouseColumn itemTypeColumn) throws IOException { + Class typeHint = itemTypeColumn.getDataType() == ClickHouseDataType.String ? String.class : null; + return readValue(itemTypeColumn, typeHint); + } + public void skipValue(ClickHouseColumn column) throws IOException { readValue(column, null); } @@ -844,13 +857,18 @@ public String toString() { ClickHouseColumn valueType = column.getValueInfo(); LinkedHashMap map = new LinkedHashMap<>(len); for (int i = 0; i < len; i++) { - Object key = readValue(keyType); - Object value = readValue(valueType); + Object key = readMapKeyOrValue(keyType); + Object value = readMapKeyOrValue(valueType); map.put(key, value); } return map; } + private Object readMapKeyOrValue(ClickHouseColumn c) throws IOException { + Class typeHint = c.getDataType() == ClickHouseDataType.String ? String.class : null; + return readValue(c, typeHint); + } + /** * Reads a tuple. * @param column - column information @@ -1123,7 +1141,7 @@ public String readString() throws IOException { return new String(dest, 0, len, StandardCharsets.UTF_8); } - public BinaryString readBytesToBuffer(int len, Function bufferAllocator) throws IOException { + public BinaryString readBinaryString(int len, Function bufferAllocator) throws IOException { ByteBuffer buffer = null; if (len > 0) { buffer = bufferAllocator.apply(len); @@ -1153,6 +1171,10 @@ public BinaryString readBytesToBuffer(int len, Function buf */ public static String readString(InputStream input) throws IOException { int len = readVarInt(input); + return readString(input, len); + } + + public static String readString(InputStream input, int len) throws IOException { if (len == 0) { return ""; } @@ -1439,12 +1461,7 @@ static final class BinaryStringImpl implements BinaryString { BinaryStringImpl(ByteBuffer buffer) { this.buffer = buffer; - this.len = buffer.position(); - } - - @Override - public ByteBuffer rawBuffer() { - return buffer; + this.len = buffer.limit(); } @Override @@ -1461,25 +1478,23 @@ public String asString() { } @Override - public int length() { - return len; - } + public byte[] asBytes() { + if (buffer.hasArray()) { + return buffer.array(); + } - @Override - public char charAt(int index) { - ensureCharBuffer(); - return charBuffer.charAt(index); + throw new UnsupportedOperationException("String is stored out of the heap and has no byte buffer easily accessible"); } @Override - public CharSequence subSequence(int start, int end) { - ensureCharBuffer(); - return charBuffer.subSequence(start, end); + public int length() { + return len; } private void ensureCharBuffer() { if (charBuffer == null) { - charBuffer = buffer.asCharBuffer(); + buffer.rewind(); + charBuffer = StandardCharsets.UTF_8.decode(buffer); } } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryString.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryString.java index 45d30b8bf..ab8d58a71 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryString.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryString.java @@ -1,18 +1,15 @@ package com.clickhouse.client.api.data_formats.internal; -import java.nio.ByteBuffer; +public interface BinaryString extends Comparable { -public interface BinaryString extends Comparable, CharSequence { - /** - * Returns a backing byte buffer or creates one - * @return ByteBuffer instance. - */ - ByteBuffer rawBuffer(); + int length(); /** * Converts raw bytes to a string whenever size is. * @return String object */ String asString(); + + byte[] asBytes(); } diff --git a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java index 0aeaddf6b..3540c913b 100644 --- a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java @@ -1,20 +1,20 @@ package com.clickhouse.client.api.data_formats.internal; -import com.clickhouse.data.ClickHouseColumn; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.TimeZone; -import org.testng.Assert; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - public class BinaryStreamReaderTests { private ZoneId tzLAX; @@ -190,4 +190,37 @@ public void testArrayValue() throws Exception { Object[] array2 = array.getArrayOfObjects(); Assert.assertEquals(array1.length, array2.length); } + + @Test(dataProvider = "testBinaryStringDP") + public void testBinaryString(String type, String originalString, byte[] originalStrBytes, ByteBuffer buffer) throws Exception { + BinaryString binaryString = new BinaryStreamReader.BinaryStringImpl(buffer); + + String firstStringAttempt = binaryString.asString(); + Assert.assertEquals(firstStringAttempt, originalString); + // Binary caches string + Assert.assertSame(binaryString.asString(), firstStringAttempt); + + Assert.assertTrue(binaryString.compareTo(originalString) == 0); + + // String length is less because of unicode bytes + Assert.assertEquals(binaryString.length(), originalString.getBytes(StandardCharsets.UTF_8).length); + + if (type.equalsIgnoreCase("heap")) { + Assert.assertEquals(binaryString.asBytes(), firstStringAttempt.getBytes()); + } else { + Assert.assertThrows(UnsupportedOperationException.class, binaryString::asBytes); + } + } + + @DataProvider + public static Object[][] testBinaryStringDP() { + final String originalString = "This should be Hello in different languages: 'こんにちは', 'Hej', 'Γεια σας'"; + final byte[] originalStrBytes = originalString.getBytes(); + ByteBuffer directBuffer = ByteBuffer.allocateDirect(originalStrBytes.length); + directBuffer.put(originalStrBytes,0, originalStrBytes.length); + return new Object[][] { + {"heap", originalString, originalStrBytes, ByteBuffer.wrap(originalStrBytes)}, + {"direct", originalString, originalStrBytes, directBuffer}, + }; + } } diff --git a/client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java b/client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java index e87021a5d..fc328fa13 100644 --- a/client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java @@ -8,6 +8,7 @@ import com.clickhouse.client.api.ClientException; import com.clickhouse.client.api.command.CommandSettings; import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader; +import com.clickhouse.client.api.data_formats.RowBinaryFormatReader; import com.clickhouse.client.api.data_formats.internal.BinaryStreamReader; import com.clickhouse.client.api.enums.Protocol; import com.clickhouse.client.api.insert.InsertSettings; @@ -32,6 +33,7 @@ import java.lang.reflect.Method; import java.math.BigDecimal; import java.math.RoundingMode; +import java.nio.charset.StandardCharsets; import java.sql.Connection; import java.time.Duration; import java.time.Instant; @@ -52,6 +54,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; +import java.util.function.Consumer; public class DataTypeTests extends BaseIntegrationTest { @@ -1738,9 +1741,79 @@ public Object[][] testJSONSubPathAccess_dp() { }; } + @Test(groups = {"integration"}) + public void testReadingStrings() throws Exception { + int smallStrLen = 1_000_000; + int tinyStrLen = 100_000; + final String sql = "SELECT repeat('A', " + smallStrLen + ") as smallStr, repeat('B', " + tinyStrLen +") as tinyStr, NULL::Nullable(String) as nullStr FROM numbers(100)"; + try (QueryResponse response = client.query(sql).get(); + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response)) { + + + while (reader.next() != null) { + byte[] smallStrBytes = reader.getByteArray("smallStr"); + Assert.assertEquals(smallStrBytes.length, smallStrLen); + + String smallStrFromBytes = new String(smallStrBytes, StandardCharsets.UTF_8); + String smallStr = reader.readValue("smallStr"); + Assert.assertEquals(smallStr, smallStrFromBytes); + // We should not create new objects + Assert.assertSame(reader.readValue("smallStr"), smallStr); + Assert.assertSame(reader.getString("smallStr"), smallStr); + + + byte[] tinyStrBytes = reader.getByteArray("tinyStr"); + Assert.assertEquals(tinyStrBytes.length, tinyStrLen); + + String tinyStrFromBytes = new String(tinyStrBytes, StandardCharsets.UTF_8); + String tinyStr = reader.readValue("tinyStr"); + Assert.assertEquals(tinyStr, tinyStrFromBytes); + + // We should not create new objects + Assert.assertSame(reader.readValue("tinyStr"), tinyStr); + Assert.assertSame(reader.getString("tinyStr"), tinyStr); + + // check null values + Assert.assertNull(reader.getByteArray("nullStr")); + Assert.assertNull(reader.readValue("nullStr")); + Assert.assertNull(reader.getString("nullStr")); + } + } + } + + @Test(groups = {"integration"}) + public void testStringsInNestedTypes() throws Exception { + final String sqlArray = "SELECT ['a', 'b', 'c'] as strArr, [['item1', null, 'item3'], ['item1', 'item2']]::Array(Array(Nullable(String))) as arr"; + try (QueryResponse response = client.query(sqlArray).get(); + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response)) { + + while (reader.next() != null) { + + List strArr = reader.getList("strArr"); + Assert.assertEquals(strArr, Arrays.asList("a", "b", "c")); + List> arr = reader.getList("arr"); + Assert.assertEquals(arr, Arrays.asList(Arrays.asList("item1", null, "item3"), Arrays.asList("item1", "item2"))); + + } + } + + final String sqlMap = "SELECT map('k1', NULL, 'k2', 'test string') as map1"; + final Map expectedMap = new HashMap<>(); + expectedMap.put("k1", null); + expectedMap.put("k2", "test string"); + try (QueryResponse response = client.query(sqlMap).get(); + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response)) { + + while (reader.next() != null) { + Map map = reader.readValue("map1"); + Assert.assertEquals(map, expectedMap); + } + } + } + public static String tableDefinition(String table, String... columns) { StringBuilder sb = new StringBuilder(); - sb.append("CREATE TABLE " + table + " ( "); + sb.append("CREATE TABLE ").append(table).append(" ( "); Arrays.stream(columns).forEach(s -> { sb.append(s).append(", "); }); From 8724b94af0c59da10b7cbcf41af146e4e64496c7 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Thu, 9 Apr 2026 12:15:34 -0700 Subject: [PATCH 4/7] Made read String as String by default. Option is to read as BinaryString with typehinting --- .../com/clickhouse/client/api/ClientConfigProperties.java | 5 +---- .../data_formats/internal/AbstractBinaryFormatReader.java | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) 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 625125fd7..6c7a8c27f 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 @@ -177,10 +177,7 @@ public Object parseValue(String value) { * Defines mapping between ClickHouse data type and target Java type * Used by binary readers to convert values into desired Java type. */ - TYPE_HINT_MAPPING("type_hint_mapping", Map.class, - "String=" + BinaryString.class.getName() - - ), + TYPE_HINT_MAPPING("type_hint_mapping", Map.class), /** * SNI SSL parameter that will be set for each outbound SSL socket. diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java index 0d345b29c..8ebe3b544 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java @@ -83,7 +83,7 @@ protected AbstractBinaryFormatReader(InputStream inputStream, QuerySettings quer BinaryStreamReader.ByteBufferAllocator byteBufferAllocator, Map> defaultTypeHintMap) { this.input = inputStream; - this.defaultTypeHintMap = defaultTypeHintMap; + this.defaultTypeHintMap = defaultTypeHintMap == null ? Collections.emptyMap() : defaultTypeHintMap; Map settings = querySettings == null ? Collections.emptyMap() : querySettings.getAllSettings(); Boolean useServerTimeZone = (Boolean) settings.get(ClientConfigProperties.USE_SERVER_TIMEZONE.getKey()); TimeZone timeZone = (useServerTimeZone == Boolean.TRUE && querySettings != null) ? From 2db49b4bfe45aa20c8a6f901138fff658341e0a4 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Thu, 9 Apr 2026 16:13:40 -0700 Subject: [PATCH 5/7] Added ability to set custom type mapping for reader --- .../com/clickhouse/client/api/Client.java | 35 ++++++++++++++----- .../client/api/ClientConfigProperties.java | 1 - .../client/datatypes/DataTypeTests.java | 30 ++++++++++++---- 3 files changed, 51 insertions(+), 15 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 d4f979026..c334419b3 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 @@ -2051,11 +2051,13 @@ public CompletableFuture execute(String sql) { *

Create an instance of {@link ClickHouseBinaryFormatReader} based on response. Table schema is option and only * required for {@link ClickHouseFormat#RowBinaryWithNames}, {@link ClickHouseFormat#RowBinary}. * Format {@link ClickHouseFormat#RowBinaryWithDefaults} is not supported for output (read operations).

- * @param response - * @param schema - * @return + * @param response - not closed query response object + * @param schema - schema of the response. Can be null. + * @param customTypeMapping - type hint map + * @return Reader object for the format + * @throws IllegalArgumentException if there is no supported reader for the type */ - public ClickHouseBinaryFormatReader newBinaryFormatReader(QueryResponse response, TableSchema schema) { + public ClickHouseBinaryFormatReader newBinaryFormatReader(QueryResponse response, TableSchema schema, Map> customTypeMapping) { ClickHouseBinaryFormatReader reader = null; // Using caching buffer allocator is risky so this parameter is not exposed to the user boolean useCachingBufferAllocator = MapUtils.getFlag(configuration, "client_allow_binary_reader_to_reuse_buffers", false); @@ -2065,17 +2067,17 @@ public ClickHouseBinaryFormatReader newBinaryFormatReader(QueryResponse response switch (response.getFormat()) { case Native: reader = new NativeFormatReader(response.getInputStream(), response.getSettings(), - byteBufferPool, typeHintMapping); + byteBufferPool, customTypeMapping); break; case RowBinaryWithNamesAndTypes: - reader = new RowBinaryWithNamesAndTypesFormatReader(response.getInputStream(), response.getSettings(), byteBufferPool, typeHintMapping); + reader = new RowBinaryWithNamesAndTypesFormatReader(response.getInputStream(), response.getSettings(), byteBufferPool, customTypeMapping); break; case RowBinaryWithNames: - reader = new RowBinaryWithNamesFormatReader(response.getInputStream(), response.getSettings(), schema, byteBufferPool, typeHintMapping); + reader = new RowBinaryWithNamesFormatReader(response.getInputStream(), response.getSettings(), schema, byteBufferPool, customTypeMapping); break; case RowBinary: reader = new RowBinaryFormatReader(response.getInputStream(), response.getSettings(), schema, - byteBufferPool, typeHintMapping); + byteBufferPool, customTypeMapping); break; default: throw new IllegalArgumentException("Binary readers doesn't support format: " + response.getFormat()); @@ -2083,6 +2085,23 @@ public ClickHouseBinaryFormatReader newBinaryFormatReader(QueryResponse response return reader; } + /** + * See {@link Client#newBinaryFormatReader(QueryResponse, TableSchema, Map)} + * @param response - not closed query response object + * @param schema - schema of the response. Can be null. + * @return Reader object for the format + * @throws IllegalArgumentException if there is no supported reader for the type + */ + public ClickHouseBinaryFormatReader newBinaryFormatReader(QueryResponse response, TableSchema schema) { + return newBinaryFormatReader(response, schema, typeHintMapping); + } + + /** + * See {@link Client#newBinaryFormatReader(QueryResponse, TableSchema, Map)} + * @param response - not closed query response object + * @return Reader object for the format + * @throws IllegalArgumentException if there is no supported reader for the type + */ public ClickHouseBinaryFormatReader newBinaryFormatReader(QueryResponse response) { return newBinaryFormatReader(response, null); } 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 6c7a8c27f..e548a90f9 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 @@ -1,7 +1,6 @@ package com.clickhouse.client.api; import com.clickhouse.client.api.data_formats.internal.AbstractBinaryFormatReader; -import com.clickhouse.client.api.data_formats.internal.BinaryString; import com.clickhouse.client.api.internal.ClickHouseLZ4OutputStream; import com.clickhouse.data.ClickHouseDataType; import com.clickhouse.data.ClickHouseFormat; diff --git a/client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java b/client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java index fc328fa13..e82a8653c 100644 --- a/client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java @@ -10,8 +10,10 @@ import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader; import com.clickhouse.client.api.data_formats.RowBinaryFormatReader; import com.clickhouse.client.api.data_formats.internal.BinaryStreamReader; +import com.clickhouse.client.api.data_formats.internal.BinaryString; import com.clickhouse.client.api.enums.Protocol; import com.clickhouse.client.api.insert.InsertSettings; +import com.clickhouse.client.api.internal.MapUtils; import com.clickhouse.client.api.metadata.TableSchema; import com.clickhouse.client.api.query.GenericRecord; import com.clickhouse.client.api.query.QueryResponse; @@ -1741,13 +1743,17 @@ public Object[][] testJSONSubPathAccess_dp() { }; } - @Test(groups = {"integration"}) - public void testReadingStrings() throws Exception { + @Test(groups = {"integration"}, dataProvider = "testStringsOptions") + public void testReadingStrings(String strType) throws Exception { int smallStrLen = 1_000_000; int tinyStrLen = 100_000; final String sql = "SELECT repeat('A', " + smallStrLen + ") as smallStr, repeat('B', " + tinyStrLen +") as tinyStr, NULL::Nullable(String) as nullStr FROM numbers(100)"; + Assert.assertTrue(strType.equals("binaryStrings") || strType.equals("normalStrings")); + Map> typeMapping = strType.equalsIgnoreCase("binaryStrings") ? Collections.emptyMap() + : Collections.singletonMap(ClickHouseDataType.String, BinaryString.class); + try (QueryResponse response = client.query(sql).get(); - ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response)) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response, null, typeMapping)) { while (reader.next() != null) { @@ -1781,11 +1787,15 @@ public void testReadingStrings() throws Exception { } } - @Test(groups = {"integration"}) - public void testStringsInNestedTypes() throws Exception { + @Test(groups = {"integration"}, dataProvider = "testStringsOptions") + public void testStringsInNestedTypes(String strType) throws Exception { final String sqlArray = "SELECT ['a', 'b', 'c'] as strArr, [['item1', null, 'item3'], ['item1', 'item2']]::Array(Array(Nullable(String))) as arr"; + Assert.assertTrue(strType.equals("binaryStrings") || strType.equals("normalStrings")); + Map> typeMapping = strType.equalsIgnoreCase("binaryStrings") ? Collections.emptyMap() + : Collections.singletonMap(ClickHouseDataType.String, BinaryString.class); + try (QueryResponse response = client.query(sqlArray).get(); - ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response)) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response, null, typeMapping)) { while (reader.next() != null) { @@ -1811,6 +1821,14 @@ public void testStringsInNestedTypes() throws Exception { } } + @DataProvider + public static Object[][] testStringsOptions() { + return new Object[][] { + {"binaryStrings"}, + {"normalStrings"} + }; + } + public static String tableDefinition(String table, String... columns) { StringBuilder sb = new StringBuilder(); sb.append("CREATE TABLE ").append(table).append(" ( "); From 11f68f1705d762a8689f414f6282185dc44399a4 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Thu, 9 Apr 2026 18:12:25 -0700 Subject: [PATCH 6/7] fixed type mapping in jdbc --- .../com/clickhouse/jdbc/ConnectionImpl.java | 20 +++++++++++++++++++ .../com/clickhouse/jdbc/StatementImpl.java | 6 +++++- .../jdbc/internal/JdbcConfiguration.java | 9 +-------- .../clickhouse/jdbc/JdbcDataTypeTests.java | 2 +- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java index 7328048ce..a2b3520b0 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java @@ -2,6 +2,7 @@ import com.clickhouse.client.api.Client; import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.data_formats.internal.BinaryString; import com.clickhouse.client.api.internal.ServerSettings; import com.clickhouse.client.api.metadata.TableSchema; import com.clickhouse.client.api.query.GenericRecord; @@ -38,7 +39,9 @@ import java.time.temporal.ChronoUnit; import java.util.Calendar; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; @@ -70,6 +73,11 @@ public class ConnectionImpl implements Connection, JdbcV2Wrapper { private final FeatureManager featureManager; + private Map> typeMappingHint; + + private static final Map> BINARY_STRING_MAPPING = Collections.singletonMap(ClickHouseDataType.String, BinaryString.class); + private static final Map> ARRAYS_AS_LIST_MAPPING = Collections.singletonMap(ClickHouseDataType.Array, List.class); + public ConnectionImpl(String url, Properties info) throws SQLException { try { this.url = url;//Raw URL @@ -121,6 +129,10 @@ public ConnectionImpl(String url, Properties info) throws SQLException { this.sqlParser = SqlParserFacade.getParser(config.getDriverProperty(DriverProperties.SQL_PARSER.getKey(), DriverProperties.SQL_PARSER.getDefaultValue()), config); this.featureManager = new FeatureManager(this.config); + + this.typeMappingHint = new HashMap<>(); + this.typeMappingHint.putAll(BINARY_STRING_MAPPING); + this.typeMappingHint.putAll(ARRAYS_AS_LIST_MAPPING); } catch (SQLException e) { throw e; } catch (Exception e) { @@ -160,6 +172,14 @@ public JdbcConfiguration getJdbcConfig() { return this.config; } + /** + * Internal API + * @return map of type-to-class mapping + */ + public Map> getTypeMappingHint() { + return typeMappingHint; + } + @Override public Statement createStatement() throws SQLException { ensureOpen(); diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java index f50546393..a92d63f01 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java @@ -181,7 +181,7 @@ protected ResultSetImpl executeQueryImpl(String sql, QuerySettings settings) thr throw new SQLException("Only RowBinaryWithNameAndTypes is supported for output format. Please check your query.", ExceptionUtils.SQL_STATE_CLIENT_ERROR); } - ClickHouseBinaryFormatReader reader = connection.getClient().newBinaryFormatReader(response); + ClickHouseBinaryFormatReader reader = createReader(response); if (reader.getSchema() == null) { long writtenRows = 0L; try { @@ -219,6 +219,10 @@ protected ResultSetImpl executeQueryImpl(String sql, QuerySettings settings) thr } } + protected ClickHouseBinaryFormatReader createReader(QueryResponse response) throws SQLException { + return connection.getClient().newBinaryFormatReader(response, null, connection.getTypeMappingHint()); + } + protected void handleSocketTimeoutException(Exception e) { if (e.getCause() instanceof SocketTimeoutException || e instanceof SocketTimeoutException) { this.connection.onNetworkTimeout(); 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 9aa5ce61a..ea467c2be 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 @@ -356,8 +356,7 @@ public Boolean isSet(DriverProperties driverProp) { public Client.Builder applyClientProperties(Client.Builder builder) { builder.addEndpoint(connectionUrl) - .setOptions(clientProperties) - .typeHintMapping(defaultTypeHintMapping()); + .setOptions(clientProperties); return builder; } @@ -382,12 +381,6 @@ public boolean isFlagSet(DriverProperties prop) { return Boolean.parseBoolean(value); } - private Map> defaultTypeHintMapping() { - Map> mapping = new HashMap<>(); - mapping.put(ClickHouseDataType.Array, List.class); - return mapping; - } - public boolean useMaxResultRows() { return isFlagSet(DriverProperties.USE_MAX_RESULT_ROWS); } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java index 7de1201ea..081bae1f5 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java @@ -1545,7 +1545,7 @@ public void testMapTypes() throws SQLException { @Test(groups = { "integration" }) public void testMapTypesWithArrayValues() throws SQLException { - runQuery("DROP TABLE test_maps;"); + runQuery("DROP TABLE IF EXISTS test_maps"); runQuery("CREATE TABLE test_maps (order Int8, " + "map Map(String, Array(Int32)), " + "map2 Map(String, Array(Int32))" From 74aff7d282632f7077e5d4bef388e0f8fba3f1a9 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Fri, 10 Apr 2026 10:11:14 -0700 Subject: [PATCH 7/7] fixed type mapping in jdbc --- .../internal/BinaryStreamReader.java | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java index 68d0467d2..1508d2bcb 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java @@ -1142,25 +1142,23 @@ public String readString() throws IOException { } public BinaryString readBinaryString(int len, Function bufferAllocator) throws IOException { - ByteBuffer buffer = null; - if (len > 0) { - buffer = bufferAllocator.apply(len); - if (buffer == null) { - throw new IOException("bufferAllocator returned `null`"); - } - if (buffer.hasArray()) { - readNBytes(input, buffer.array(), 0, len); - } else { - int left = len; - while (left > 0) { - int chunkSize = Math.min(STRING_BUFF.length, left); - readNBytes(input, STRING_BUFF, 0, chunkSize); - buffer.put(STRING_BUFF, 0, chunkSize); - left -= chunkSize; - } + ByteBuffer buffer = bufferAllocator.apply(len); + if (buffer == null) { + throw new IOException("bufferAllocator returned `null`"); + } + if (buffer.hasArray()) { + readNBytes(input, buffer.array(), 0, len); + } else { + int left = len; + while (left > 0) { + int chunkSize = Math.min(STRING_BUFF.length, left); + readNBytes(input, STRING_BUFF, 0, chunkSize); + buffer.put(STRING_BUFF, 0, chunkSize); + left -= chunkSize; } } - return buffer == null ? null : new BinaryStringImpl(buffer); + + return new BinaryStringImpl(buffer); } /**