|
13 | 13 | import org.slf4j.Logger; |
14 | 14 | import org.slf4j.LoggerFactory; |
15 | 15 | import org.testng.Assert; |
| 16 | +import org.testng.SkipException; |
16 | 17 | import org.testng.annotations.BeforeClass; |
17 | 18 | import org.testng.annotations.DataProvider; |
18 | 19 | import org.testng.annotations.Test; |
@@ -495,6 +496,151 @@ public void testUnsignedIntegerTypes() throws Exception { |
495 | 496 | } |
496 | 497 |
|
497 | 498 |
|
| 499 | + // BFloat16 was introduced in ClickHouse 24.11. |
| 500 | + private static final String BFLOAT16_UNSUPPORTED_VERSIONS = "(,24.10]"; |
| 501 | + |
| 502 | + @Test(groups = { "integration" }) |
| 503 | + public void testBFloat16() throws SQLException { |
| 504 | + if (ClickHouseVersion.of(getServerVersion()).check(BFLOAT16_UNSUPPORTED_VERSIONS)) { |
| 505 | + throw new SkipException("BFloat16 was introduced in ClickHouse 24.11"); |
| 506 | + } |
| 507 | + |
| 508 | + // Exhaustively cover reading every one of the 2^16 BFloat16 bit patterns through the JDBC |
| 509 | + // driver, which must expose BFloat16 as java.lang.Float. The dataset is written with SQL |
| 510 | + // text literals so the *server* produces the stored bytes, decoupling the read path under |
| 511 | + // test from any write-side codec error. Row b holds the BFloat16 whose bit pattern is b, so |
| 512 | + // a read must widen it to Float.intBitsToFloat(b << 16) (NaN inputs collapse to a single |
| 513 | + // canonical NaN on the server and only read back as NaN). |
| 514 | + final String table = "test_bfloat16"; |
| 515 | + final int count = 1 << 16; |
| 516 | + final int batchSize = 4096; // keep each INSERT well under max_query_size |
| 517 | + runQuery("DROP TABLE IF EXISTS " + table); |
| 518 | + runQuery("CREATE TABLE " + table |
| 519 | + + " (rowId Int32, v BFloat16, vNull Nullable(BFloat16)) ENGINE = MergeTree ORDER BY rowId"); |
| 520 | + |
| 521 | + try (Connection conn = getJdbcConnection(); |
| 522 | + Statement stmt = conn.createStatement()) { |
| 523 | + for (int start = 0; start < count; start += batchSize) { |
| 524 | + int end = Math.min(start + batchSize, count); |
| 525 | + StringBuilder insert = new StringBuilder("INSERT INTO ").append(table).append(" VALUES "); |
| 526 | + for (int b = start; b < end; b++) { |
| 527 | + String literal = bFloat16Literal(b); |
| 528 | + if (b > start) { |
| 529 | + insert.append(','); |
| 530 | + } |
| 531 | + insert.append('(').append(b).append(',').append(literal).append(',') |
| 532 | + .append(b == 0 ? "NULL" : literal).append(')'); |
| 533 | + } |
| 534 | + stmt.executeUpdate(insert.toString()); |
| 535 | + } |
| 536 | + } |
| 537 | + |
| 538 | + // The server's stored bits are the ground truth for the read: reinterpretAsUInt16(v) yields |
| 539 | + // the exact 16-bit pattern the server wrote, so a correct read must widen it to |
| 540 | + // Float.intBitsToFloat(bits << 16). Deriving the expectation from the stored bits keeps the |
| 541 | + // assertion correct regardless of how the server converts the text literal to BFloat16 - in |
| 542 | + // particular the server flushes BFloat16 subnormals to zero, so those patterns are stored as |
| 543 | + // +/-0 even though the literal named a tiny subnormal. |
| 544 | + try (Connection conn = getJdbcConnection(); |
| 545 | + Statement stmt = conn.createStatement(); |
| 546 | + ResultSet rs = stmt.executeQuery( |
| 547 | + "SELECT rowId, v, reinterpretAsUInt16(v) AS bits, vNull FROM " + table + " ORDER BY rowId")) { |
| 548 | + ResultSetMetaData meta = rs.getMetaData(); |
| 549 | + assertEquals(meta.getColumnType(2), Types.FLOAT, "BFloat16 should map to Types.FLOAT"); |
| 550 | + assertEquals(meta.getColumnClassName(2), Float.class.getName(), "BFloat16 should map to java.lang.Float"); |
| 551 | + |
| 552 | + int rows = 0; |
| 553 | + while (rs.next()) { |
| 554 | + int b = rs.getInt("rowId"); |
| 555 | + float expected = Float.intBitsToFloat(rs.getInt("bits") << 16); |
| 556 | + Object obj = rs.getObject("v"); |
| 557 | + assertTrue(obj instanceof Float, |
| 558 | + "BFloat16 getObject should return Float but was " |
| 559 | + + (obj == null ? "null" : obj.getClass().getName())); |
| 560 | + assertBFloat16Equals((Float) obj, expected); |
| 561 | + assertBFloat16Equals(rs.getFloat("v"), expected); |
| 562 | + if (b == 0) { |
| 563 | + assertNull(rs.getObject("vNull")); |
| 564 | + } else { |
| 565 | + assertBFloat16Equals((Float) rs.getObject("vNull"), expected); |
| 566 | + } |
| 567 | + rows++; |
| 568 | + } |
| 569 | + assertEquals(rows, count); |
| 570 | + } |
| 571 | + } |
| 572 | + |
| 573 | + @Test(groups = { "integration" }) |
| 574 | + public void testBFloat16WriteAsFloat() throws SQLException { |
| 575 | + if (ClickHouseVersion.of(getServerVersion()).check(BFLOAT16_UNSUPPORTED_VERSIONS)) { |
| 576 | + throw new SkipException("BFloat16 was introduced in ClickHouse 24.11"); |
| 577 | + } |
| 578 | + |
| 579 | + // Writing through the JDBC Float surface: setFloat must store a BFloat16. Exactly |
| 580 | + // representable values round-trip unchanged; 3.14f is truncated to the high 16 bits |
| 581 | + // (0x4048F5C3 -> 0x40480000 = 3.125f), matching the server's Float32 -> BFloat16 cast. |
| 582 | + final String table = "test_bfloat16_write"; |
| 583 | + runQuery("DROP TABLE IF EXISTS " + table); |
| 584 | + runQuery("CREATE TABLE " + table |
| 585 | + + " (rowId Int32, v BFloat16, vNull Nullable(BFloat16)) ENGINE = MergeTree ORDER BY rowId"); |
| 586 | + |
| 587 | + float[] inputs = { 0f, 0.5f, 1.5f, -2.5f, 3.14f, 128.0f }; |
| 588 | + try (Connection conn = getJdbcConnection(); |
| 589 | + PreparedStatement ps = conn.prepareStatement("INSERT INTO " + table + " VALUES (?, ?, ?)")) { |
| 590 | + for (int i = 0; i < inputs.length; i++) { |
| 591 | + ps.setInt(1, i); |
| 592 | + ps.setFloat(2, inputs[i]); |
| 593 | + ps.setFloat(3, inputs[i]); |
| 594 | + ps.executeUpdate(); |
| 595 | + } |
| 596 | + } |
| 597 | + |
| 598 | + try (Connection conn = getJdbcConnection(); |
| 599 | + Statement stmt = conn.createStatement(); |
| 600 | + ResultSet rs = stmt.executeQuery("SELECT rowId, v, vNull FROM " + table + " ORDER BY rowId")) { |
| 601 | + for (int i = 0; i < inputs.length; i++) { |
| 602 | + assertTrue(rs.next()); |
| 603 | + float expected = Float.intBitsToFloat(Float.floatToIntBits(inputs[i]) & 0xFFFF0000); |
| 604 | + assertEquals(rs.getFloat("v"), expected, 0f); |
| 605 | + assertEquals(rs.getObject("v"), Float.valueOf(expected)); |
| 606 | + assertEquals(rs.getFloat("vNull"), expected, 0f); |
| 607 | + assertEquals(rs.getObject("vNull"), Float.valueOf(expected)); |
| 608 | + } |
| 609 | + assertFalse(rs.next()); |
| 610 | + } |
| 611 | + } |
| 612 | + |
| 613 | + // Text form of the exact BFloat16 whose bit pattern is {@code pattern}, suitable for use as a |
| 614 | + // SQL numeric literal. Non-finite patterns use ClickHouse's nan/inf/-inf tokens; every other |
| 615 | + // pattern uses the shortest round-trippable decimal of Float.intBitsToFloat(pattern << 16), |
| 616 | + // which is exactly representable in BFloat16 (its low 16 mantissa bits are zero). |
| 617 | + private static String bFloat16Literal(int pattern) { |
| 618 | + float value = Float.intBitsToFloat(pattern << 16); |
| 619 | + if (Float.isNaN(value)) { |
| 620 | + return "nan"; |
| 621 | + } |
| 622 | + if (value == Float.POSITIVE_INFINITY) { |
| 623 | + return "inf"; |
| 624 | + } |
| 625 | + if (value == Float.NEGATIVE_INFINITY) { |
| 626 | + return "-inf"; |
| 627 | + } |
| 628 | + return Float.toString(value); |
| 629 | + } |
| 630 | + |
| 631 | + // BFloat16 keeps the high 16 bits of a float32, so every non-NaN value round-trips bit-for-bit; |
| 632 | + // NaN inputs collapse to a single canonical NaN on the server and only read back as NaN. |
| 633 | + private static void assertBFloat16Equals(Float actual, Float expected) { |
| 634 | + if (expected == null) { |
| 635 | + assertNull(actual); |
| 636 | + } else if (Float.isNaN(expected)) { |
| 637 | + assertTrue(actual != null && Float.isNaN(actual), "expected a NaN but got " + actual); |
| 638 | + } else { |
| 639 | + assertTrue(actual != null, "expected " + expected + " but got null"); |
| 640 | + assertEquals(Float.floatToRawIntBits(actual), Float.floatToRawIntBits(expected)); |
| 641 | + } |
| 642 | + } |
| 643 | + |
498 | 644 | @Test(groups = { "integration" }) |
499 | 645 | public void testUUIDTypes() throws Exception { |
500 | 646 | Random rand = new Random(); |
|
0 commit comments