Skip to content

Commit fa79d8b

Browse files
feat(client-v2): add BFloat16 data type support
BFloat16 (ClickHouse 24.11+) was registered in ClickHouseDataType but client-v2 threw "BFloat16 is not supported yet" on read and UnsupportedOperationException on write. Implement read and write for it, mapping BFloat16 to the Java float type. On the wire BFloat16 is the high 16 bits of the IEEE-754 float32 (little-endian, 2 bytes). Reads widen the value losslessly; writes keep the high 16 bits of the float, matching the server's own Float32 -> BFloat16 conversion (the low mantissa bits are truncated). Covers the generic read path, the numeric accessors (getFloat/...), POJO read/write, Nullable(BFloat16), and BFloat16 held in Dynamic/Variant columns. Adds a deterministic codec unit test and integration tests (round-trip, server truncation parity, Dynamic column). Updates CHANGELOG and docs/features.md. Implements: #2279
1 parent fbe31cd commit fa79d8b

9 files changed

Lines changed: 159 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
[Release Migration Guide](docs/releases/0_11_0.md)
44

5+
### New Features
6+
7+
- **[client-v2]** Added support for the `BFloat16` data type (ClickHouse `24.11+`). `BFloat16` columns are read as Java
8+
`float` values (widening is lossless) and written from `float`/`Float` values, including through generic records, POJO
9+
binding, `Nullable(BFloat16)`, and `BFloat16` values held in `Dynamic`/`Variant` columns. On write the client keeps the
10+
high 16 bits of the `float`, matching the ClickHouse server's own `Float32``BFloat16` conversion. Previously reading
11+
or writing a `BFloat16` column threw "BFloat16 is not supported yet". (https://github.com/ClickHouse/clickhouse-java/issues/2279)
12+
513
### Bug Fixes
614

715
- **[client-v2]** Fixed POJO insert error classification so transport write failures such as java.net.SocketException:

clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseDataType.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ static Map<ClickHouseDataType, Set<Class<?>>> dataTypeClassMap() {
202202
map.put(String, setOf(String.class));
203203
map.put(Float64, setOf(float.class, Float.class, double.class, Double.class));
204204
map.put(Float32, setOf(float.class, Float.class));
205+
map.put(BFloat16, setOf(float.class, Float.class));
205206
map.put(Decimal, setOf(float.class, Float.class, double.class, Double.class, BigDecimal.class));
206207
map.put(Decimal256, setOf(float.class, Float.class, double.class, Double.class, BigDecimal.class));
207208
map.put(Decimal128, setOf(float.class, Float.class, double.class, Double.class, BigDecimal.class));

clickhouse-data/src/main/java/com/clickhouse/data/format/BinaryStreamUtils.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,21 @@ public static void writeFloat32(OutputStream output, float value) throws IOExcep
985985
writeInt32(output, Float.floatToIntBits(value));
986986
}
987987

988+
/**
989+
* Write a bfloat16 value to given output stream. {@code BFloat16} occupies the
990+
* high 16 bits of the IEEE-754 {@code float} representation, so the low 16 bits of
991+
* the mantissa are dropped (truncated toward zero), which matches how the ClickHouse
992+
* server converts {@code Float32} to {@code BFloat16}.
993+
*
994+
* @param output non-null output stream
995+
* @param value float value to be stored as bfloat16
996+
* @throws IOException when failed to write value to output stream or reached
997+
* end of the stream
998+
*/
999+
public static void writeBFloat16(OutputStream output, float value) throws IOException {
1000+
writeInt16(output, (short) (Float.floatToIntBits(value) >>> 16));
1001+
}
1002+
9881003
/**
9891004
* Read a double value from given input stream.
9901005
*

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ protected void setSchema(TableSchema schema) {
323323
case Int256:
324324
case UInt128:
325325
case UInt256:
326+
case BFloat16:
326327
case Float32:
327328
case Float64:
328329
case Decimal:

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ public <T> T readValue(ClickHouseColumn column, Class<?> typeHint) throws IOExce
164164
return (T) readDecimal(ClickHouseDataType.Decimal128.getMaxPrecision(), scale);
165165
case Decimal256:
166166
return (T) readDecimal(ClickHouseDataType.Decimal256.getMaxPrecision(), scale);
167+
case BFloat16:
168+
return (T) (Float)readBFloat16LE();
167169
case Float32:
168170
return (T) (Float)readFloatLE();
169171
case Float64:
@@ -509,6 +511,18 @@ public float readFloatLE() throws IOException {
509511
return Float.intBitsToFloat(readIntLE());
510512
}
511513

514+
/**
515+
* Reads a little-endian {@code BFloat16} value from the internal input stream and
516+
* widens it to a {@code float}. {@code BFloat16} carries the high 16 bits of the
517+
* IEEE-754 {@code float} representation, so widening (shifting them back into the
518+
* high bits) is lossless.
519+
* @return float value
520+
* @throws IOException when IO error occurs
521+
*/
522+
public float readBFloat16LE() throws IOException {
523+
return Float.intBitsToFloat(readUnsignedShortLE() << 16);
524+
}
525+
512526
private static final byte[] B1 = new byte[8];
513527
/**
514528
* Reads a double value from the internal input stream.
@@ -1205,6 +1219,7 @@ public static boolean isReadToPrimitive(ClickHouseDataType dataType) {
12051219
case Int32:
12061220
case UInt32:
12071221
case Int64:
1222+
case BFloat16:
12081223
case Float32:
12091224
case Float64:
12101225
case Bool:
@@ -1410,8 +1425,6 @@ private ClickHouseColumn readDynamicData() throws IOException {
14101425
}
14111426
case AggregateFunction:
14121427
throw new ClientException("Aggregate functions are not supported yet");
1413-
case BFloat16:
1414-
throw new ClientException("BFloat16 is not supported yet");
14151428
default:
14161429
return ClickHouseColumn.of("v", type, false, 0, 0);
14171430
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,9 @@ private static void serializePrimitiveData(OutputStream stream, Object value, Cl
554554
case UInt256:
555555
BinaryStreamUtils.writeUnsignedInt256(stream, NumberConverter.toBigInteger(value));
556556
break;
557+
case BFloat16:
558+
BinaryStreamUtils.writeBFloat16(stream, (float) value);
559+
break;
557560
case Float32:
558561
BinaryStreamUtils.writeFloat32(stream, (float) value);
559562
break;
@@ -1112,6 +1115,11 @@ private static void binaryReaderMethodForType(MethodVisitor mv, Class<?> targetT
11121115
readerMethodReturnType = Type.getDescriptor(long.class);
11131116
convertOpcode = longToOpcode(targetType);
11141117
break;
1118+
case BFloat16:
1119+
readerMethod = "readBFloat16LE";
1120+
readerMethodReturnType = Type.getDescriptor(float.class);
1121+
convertOpcode = floatToOpcode(targetType);
1122+
break;
11151123
case Float32:
11161124
readerMethod = "readFloatLE";
11171125
readerMethodReturnType = Type.getDescriptor(float.class);

client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/SerializerUtilsPrimitiveSerializationTests.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.clickhouse.data.ClickHouseColumn;
66
import com.clickhouse.data.format.BinaryStreamUtils;
77
import org.testng.Assert;
8+
import org.testng.annotations.DataProvider;
89
import org.testng.annotations.Test;
910

1011
import java.io.ByteArrayInputStream;
@@ -45,6 +46,31 @@ public void testSerializeDecimalTargetFromGenericNumberValue() throws IOExceptio
4546
Assert.assertEquals(reader.getBigDecimal("value"), new BigDecimal("987654.321"));
4647
}
4748

49+
@DataProvider(name = "bFloat16Values")
50+
public static Object[][] bFloat16Values() {
51+
// input float, expected float after a BFloat16 round-trip. BFloat16 keeps the high
52+
// 16 bits of the float32 representation, so writing truncates the low mantissa bits
53+
// (matching the ClickHouse server) and reading widens the value back losslessly.
54+
return new Object[][] {
55+
{0.0f, 0.0f}, // zero
56+
{1.5f, 1.5f}, // exactly representable
57+
{-2.5f, -2.5f}, // exactly representable, sign preserved
58+
{100.0f, 100.0f}, // exactly representable
59+
{256.0f, 256.0f}, // exactly representable
60+
{3.14f, 3.125f}, // 0x4048F5C3 -> 0x40480000 (low bits dropped)
61+
{0.1f, 0.099609375f}, // 0x3DCCCCCD -> 0x3DCC0000 (low bits dropped)
62+
};
63+
}
64+
65+
@Test(dataProvider = "bFloat16Values")
66+
public void testSerializeBFloat16RoundTrip(float input, float expected) throws IOException {
67+
RowBinaryWithNamesAndTypesFormatReader reader = serializeSingleValue("BFloat16", input);
68+
69+
reader.next();
70+
71+
Assert.assertEquals(reader.getFloat("value"), expected, 0.0f);
72+
}
73+
4874
private RowBinaryWithNamesAndTypesFormatReader serializeSingleValue(String type, Object value)
4975
throws IOException {
5076
ByteArrayOutputStream out = new ByteArrayOutputStream();

client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,89 @@ public static String tblCreateSQL(String table) {
154154
}
155155
}
156156

157+
@Data
158+
@AllArgsConstructor
159+
@NoArgsConstructor
160+
public static class DTOForBFloat16Tests {
161+
private int rowId;
162+
private float bFloat16;
163+
private Float bFloat16Nullable;
164+
165+
public static String tblCreateSQL(String table) {
166+
return tableDefinition(table, "rowId Int16", "bFloat16 BFloat16",
167+
"bFloat16Nullable Nullable(BFloat16)");
168+
}
169+
}
170+
171+
// BFloat16 was introduced in ClickHouse 24.11.
172+
private static final String BFLOAT16_UNSUPPORTED_VERSIONS = "(,24.10]";
173+
174+
@Test(groups = {"integration"})
175+
public void testBFloat16() throws Exception {
176+
if (isVersionMatch(BFLOAT16_UNSUPPORTED_VERSIONS)) {
177+
return;
178+
}
179+
180+
final String table = "test_bfloat16";
181+
// Only exactly-representable BFloat16 values are used here so a client-side write
182+
// followed by a read returns the same value. Truncation of non-representable values
183+
// is covered by testBFloat16TruncationMatchesServer.
184+
writeReadVerify(table,
185+
DTOForBFloat16Tests.tblCreateSQL(table),
186+
DTOForBFloat16Tests.class,
187+
Arrays.asList(
188+
new DTOForBFloat16Tests(0, 1.5f, -2.5f),
189+
new DTOForBFloat16Tests(1, 0.0f, null),
190+
new DTOForBFloat16Tests(2, 256.0f, 100.0f)),
191+
(data, dto) -> {
192+
DTOForBFloat16Tests expected = data.get(dto.getRowId());
193+
Assert.assertEquals(dto.getBFloat16(), expected.getBFloat16(), 0.0f);
194+
Assert.assertEquals(dto.getBFloat16Nullable(), expected.getBFloat16Nullable());
195+
});
196+
}
197+
198+
@Test(groups = {"integration"})
199+
public void testBFloat16TruncationMatchesServer() throws Exception {
200+
if (isVersionMatch(BFLOAT16_UNSUPPORTED_VERSIONS)) {
201+
return;
202+
}
203+
204+
// (a) The client must decode a BFloat16 produced by the server (Float32 -> BFloat16
205+
// drops the low mantissa bits) to the same value.
206+
List<GenericRecord> cast = client.queryAll(
207+
"SELECT CAST(3.14 AS BFloat16) AS v, CAST(0.1 AS BFloat16) AS v2");
208+
Assert.assertEquals(cast.get(0).getFloat("v"), 3.125f, 0.0f); // 0x4048F5C3 -> 0x40480000
209+
Assert.assertEquals(cast.get(0).getFloat("v2"), 0.099609375f, 0.0f); // 0x3DCCCCCD -> 0x3DCC0000
210+
211+
// (b) A value written by the client must be stored byte-for-byte identically to the
212+
// server's own Float32 -> BFloat16 conversion of the same value.
213+
final String table = "test_bfloat16_truncation";
214+
writeReadVerify(table,
215+
DTOForBFloat16Tests.tblCreateSQL(table),
216+
DTOForBFloat16Tests.class,
217+
Arrays.asList(new DTOForBFloat16Tests(0, 3.14f, 0.1f)),
218+
(data, dto) -> {
219+
Assert.assertEquals(dto.getBFloat16(), 3.125f, 0.0f);
220+
Assert.assertEquals(dto.getBFloat16Nullable(), Float.valueOf(0.099609375f));
221+
});
222+
List<GenericRecord> parity = client.queryAll(
223+
"SELECT reinterpretAsUInt16(bFloat16) AS written, " +
224+
"reinterpretAsUInt16(CAST(toFloat32(3.14) AS BFloat16)) AS server FROM " + table);
225+
Assert.assertEquals(parity.get(0).getInteger("written"), parity.get(0).getInteger("server"));
226+
}
227+
228+
@Test(groups = {"integration"})
229+
public void testBFloat16InDynamicColumn() throws Exception {
230+
if (isVersionMatch("(,24.8]") || isVersionMatch(BFLOAT16_UNSUPPORTED_VERSIONS)) {
231+
return;
232+
}
233+
234+
// Reading a BFloat16 held in a Dynamic column exercises the dynamic type-tag read path.
235+
List<GenericRecord> rows = client.queryAll(
236+
"SELECT CAST(CAST(1.5 AS BFloat16) AS Dynamic) AS v SETTINGS allow_experimental_dynamic_type = 1");
237+
Assert.assertEquals(rows.get(0).getObject("v"), Float.valueOf(1.5f));
238+
}
239+
157240
@Test(groups = {"integration"})
158241
public void testVariantWithSimpleDataTypes() throws Exception {
159242
if (isVersionMatch("(,24.8]")) {

docs/features.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ This document lists stable, user-visible behavior in `client-v2` and `jdbc-v2` t
1818
- Binary format readers: Reads ClickHouse binary result formats including `Native`, `RowBinary`, `RowBinaryWithNames`, and `RowBinaryWithNamesAndTypes`.
1919
- JSONEachRow text reader: Can stream `JSONEachRow` responses through a caller-supplied `JsonParser`, with Jackson and Gson parser factory implementations available as optional classpath dependencies, and infers a best-effort schema from the first row.
2020
- Data type conversion: Maps ClickHouse types to Java values for binary reads, POJO binding, and SQL parameter formatting, including date/time handling.
21+
- BFloat16 type support: For ClickHouse `24.11+`, reads and writes the `BFloat16` type through generic records, binary readers, POJO binding, and `Nullable`/`Dynamic`/`Variant` wrappers. `BFloat16` maps to the Java `float` type: a read widens the stored 16-bit value losslessly, while a write keeps the high 16 bits of the `float`.
2122
- Geometry type support: For ClickHouse `25.11+`, where `Geometry` changed from a string alias to `Variant(Point, Ring, LineString, MultiLineString, Polygon, MultiPolygon)`, the client reads and writes `Geometry` values through generic records, binary readers, POJO binding, and SQL parameter formatting, using Java array dimensionality to represent the geometry shape.
2223
- Insert APIs: Supports inserting registered POJOs, raw streams, and callback-driven writers, with optional column lists and format selection.
2324
- Insert controls: Supports insert-specific settings such as deduplication token, query id, compression behavior, and request headers.
@@ -39,6 +40,7 @@ Compatibility-sensitive traits:
3940
- String escaping behavior in `SQLUtils` is compatibility-sensitive: `enquoteLiteral()` uses SQL-style doubled single quotes, while `escapeSingleQuotes()` escapes both backslashes and single quotes with backslashes.
4041
- Identifier quoting behavior is stable API for helper callers: identifiers are double-quoted, embedded double quotes are doubled, and optional quoting keeps simple identifiers unchanged.
4142
- Instant formatting is type-sensitive and should not drift: `Date` formatting depends on an explicit timezone, `DateTime` is serialized as epoch seconds, and higher-precision timestamps preserve up to 9 fractional digits.
43+
- `BFloat16` conversion is precision-sensitive and should not drift: a write keeps only the high 16 bits of the `float` (the low mantissa bits are truncated toward zero, matching the server's `Float32``BFloat16` conversion), so values that are not exactly representable in bfloat16 change when written; a read widens the 16-bit value back to `float` losslessly.
4244
- Timezone conversion helpers preserve nanoseconds and can intentionally shift local date or time when interpreted in a different timezone; this behavior is covered by tests and should not be normalized away.
4345
- `Geometry` handling is shape-sensitive: supported values are 1D through 4D Java arrays representing the nested geometry variants, and unsupported shapes or non-array values are rejected during serialization.
4446
- `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`.

0 commit comments

Comments
 (0)