Skip to content

Commit f703a75

Browse files
test(client-v2): exhaustively cover all 65,536 BFloat16 values
Address review feedback: instead of a handful of hand-picked values, round-trip every one of the 2^16 BFloat16 bit patterns. - SerializerUtilsPrimitiveSerializationTests: replace the 7-row bFloat16Values DataProvider with a single test that serializes and reads back all 65,536 patterns in-memory. Non-NaN patterns (incl. +/-0, subnormals, +/-Infinity) must round-trip bit-for-bit; NaN inputs collapse to one canonical NaN on write (Float#floatToIntBits normalizes the payload) so they only need to read back as NaN. - DataTypeTests#testBFloat16: exhaustively insert all 65,536 values through the full client write -> server -> client read POJO round-trip (incl. Nullable(BFloat16)); widen the DTO rowId column to Int32 to key 65,536 rows.
1 parent 40f1dca commit f703a75

2 files changed

Lines changed: 71 additions & 34 deletions

File tree

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

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -111,38 +111,53 @@ public void testSerializeFloatColumnRejectsUnsupportedValue() {
111111
() -> serializeSingleValue("Float64", new Object()));
112112
}
113113

114-
@DataProvider(name = "bFloat16Values")
115-
public static Object[][] bFloat16Values() {
116-
// input float, expected float after a BFloat16 round-trip. BFloat16 keeps the high
117-
// 16 bits of the float32 representation, so writing truncates the low mantissa bits
118-
// (matching the ClickHouse server) and reading widens the value back losslessly.
119-
return new Object[][] {
120-
{0.0f, 0.0f}, // zero
121-
{1.5f, 1.5f}, // exactly representable
122-
{-2.5f, -2.5f}, // exactly representable, sign preserved
123-
{100.0f, 100.0f}, // exactly representable
124-
{256.0f, 256.0f}, // exactly representable
125-
{3.14f, 3.125f}, // 0x4048F5C3 -> 0x40480000 (low bits dropped)
126-
{0.1f, 0.099609375f}, // 0x3DCCCCCD -> 0x3DCC0000 (low bits dropped)
127-
};
128-
}
129-
130-
@Test(dataProvider = "bFloat16Values")
131-
public void testSerializeBFloat16RoundTrip(float input, float expected) throws IOException {
132-
RowBinaryWithNamesAndTypesFormatReader reader = serializeSingleValue("BFloat16", input);
114+
@Test
115+
public void testSerializeBFloat16RoundTripAllValues() throws IOException {
116+
// Exhaustively round-trip every one of the 2^16 BFloat16 bit patterns. For pattern b the
117+
// canonical float32 is intBitsToFloat(b << 16); the client writes its high 16 bits and reads
118+
// them back widened. Every non-NaN pattern - including +/-0, subnormals and +/-Infinity -
119+
// must round-trip bit-for-bit. NaN inputs collapse to a single NaN because Float#floatToIntBits
120+
// normalizes the payload on write, so they are only required to read back as a NaN.
121+
final int count = 1 << 16;
122+
Object[] inputs = new Object[count];
123+
for (int b = 0; b < count; b++) {
124+
inputs[b] = Float.intBitsToFloat(b << 16);
125+
}
133126

134-
reader.next();
127+
RowBinaryWithNamesAndTypesFormatReader reader = serializeColumn("BFloat16", inputs);
128+
129+
for (int b = 0; b < count; b++) {
130+
Assert.assertNotNull(reader.next(), "missing row for BFloat16 pattern " + hex(b));
131+
float actual = reader.getFloat("value");
132+
if (Float.isNaN((Float) inputs[b])) {
133+
Assert.assertTrue(Float.isNaN(actual), "BFloat16 pattern " + hex(b) + " must read back as NaN");
134+
} else {
135+
Assert.assertEquals(Float.floatToRawIntBits(actual), b << 16,
136+
"BFloat16 pattern " + hex(b) + " did not round-trip");
137+
}
138+
}
139+
Assert.assertNull(reader.next(), "unexpected extra row after all 65,536 BFloat16 patterns");
140+
}
135141

136-
Assert.assertEquals(reader.getFloat("value"), expected, 0.0f);
142+
private static String hex(int bFloat16Bits) {
143+
return String.format("0x%04X", bFloat16Bits);
137144
}
138145

139146
private RowBinaryWithNamesAndTypesFormatReader serializeSingleValue(String type, Object value)
140147
throws IOException {
148+
return serializeColumn(type, new Object[]{value});
149+
}
150+
151+
private RowBinaryWithNamesAndTypesFormatReader serializeColumn(String type, Object[] values)
152+
throws IOException {
141153
ByteArrayOutputStream out = new ByteArrayOutputStream();
142154
BinaryStreamUtils.writeVarInt(out, 1);
143155
BinaryStreamUtils.writeString(out, "value");
144156
BinaryStreamUtils.writeString(out, type);
145-
SerializerUtils.serializeData(out, value, ClickHouseColumn.of("value", type));
157+
ClickHouseColumn column = ClickHouseColumn.of("value", type);
158+
for (Object value : values) {
159+
SerializerUtils.serializeData(out, value, column);
160+
}
146161

147162
return new RowBinaryWithNamesAndTypesFormatReader(
148163
new ByteArrayInputStream(out.toByteArray()),

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

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ public static class DTOForBFloat16Tests {
163163
private Float bFloat16Nullable;
164164

165165
public static String tblCreateSQL(String table) {
166-
return tableDefinition(table, "rowId Int16", "bFloat16 BFloat16",
166+
return tableDefinition(table, "rowId Int32", "bFloat16 BFloat16",
167167
"bFloat16Nullable Nullable(BFloat16)");
168168
}
169169
}
@@ -177,24 +177,46 @@ public void testBFloat16() throws Exception {
177177
return;
178178
}
179179

180+
// Exhaustively cover every one of the 2^16 BFloat16 bit patterns through a full client
181+
// write -> server -> client read round-trip (POJO path, incl. Nullable(BFloat16)). For
182+
// pattern b the value is Float.intBitsToFloat(b << 16): non-NaN values (incl. +/-0,
183+
// subnormals, +/-Infinity) must return bit-for-bit, while NaN inputs collapse to a single
184+
// canonical NaN on write and are only required to read back as NaN. Truncation of
185+
// non-representable float inputs is covered by testBFloat16TruncationMatchesServer.
180186
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.
187+
final int count = 1 << 16;
188+
List<DTOForBFloat16Tests> data = new ArrayList<>(count);
189+
for (int b = 0; b < count; b++) {
190+
float value = Float.intBitsToFloat(b << 16);
191+
// Row 0 exercises the null path; every other row round-trips a non-null Nullable(BFloat16).
192+
data.add(new DTOForBFloat16Tests(b, value, b == 0 ? null : value));
193+
}
194+
184195
writeReadVerify(table,
185196
DTOForBFloat16Tests.tblCreateSQL(table),
186197
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());
198+
data,
199+
(all, dto) -> {
200+
DTOForBFloat16Tests expected = all.get(dto.getRowId());
201+
assertBFloat16Equals(dto.getBFloat16(), expected.getBFloat16());
202+
assertBFloat16Equals(dto.getBFloat16Nullable(), expected.getBFloat16Nullable());
195203
});
196204
}
197205

206+
// BFloat16 keeps the high 16 bits of a float32, so every non-NaN value round-trips bit-for-bit;
207+
// NaN inputs collapse to a single canonical NaN on write and are only required to read back as NaN.
208+
private static void assertBFloat16Equals(Float actual, Float expected) {
209+
if (expected == null) {
210+
Assert.assertNull(actual);
211+
} else if (Float.isNaN(expected)) {
212+
Assert.assertNotNull(actual);
213+
Assert.assertTrue(Float.isNaN(actual), "expected a NaN but got " + actual);
214+
} else {
215+
Assert.assertNotNull(actual);
216+
Assert.assertEquals(Float.floatToRawIntBits(actual), Float.floatToRawIntBits(expected));
217+
}
218+
}
219+
198220
@Test(groups = {"integration"})
199221
public void testBFloat16TruncationMatchesServer() throws Exception {
200222
if (isVersionMatch(BFLOAT16_UNSUPPORTED_VERSIONS)) {

0 commit comments

Comments
 (0)