This is an automated email from the ASF dual-hosted git repository.

morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 596383efa1d [fix](be) Parse complex Iceberg initial defaults at full 
precision (#69369)
596383efa1d is described below

commit 596383efa1d789a809d902541512a9793a74fb93
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Fri Oct 23 10:10:46 2026 +0100

    [fix](be) Parse complex Iceberg initial defaults at full precision (#69369)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #66538
    
    Problem Summary:
    
    **In short.** This ports a fix from branch-4.1 #66538 (Iceberg V3
    default values): both BE Iceberg readers parse the JSON form of a
    complex initial default with `rapidjson::kParseFullPrecisionFlag`, so a
    DOUBLE inside it reads back bit-identical to Spark.
    
    **Background.** Iceberg stores the initial default of a struct, list or
    map field as single-value JSON, for example `[0.18172760479972437302]`
    for a `list<double>` column added after some data files were written.
    When Doris reads such an older file, it materializes the missing column
    from that JSON: FileScanner V1 in `create_initial_default_column`
    (`be/src/format/table/iceberg_default_value.h`) and FileScanner V2 when
    it builds the default expression
    (`be/src/format_v2/table/iceberg_reader.cpp`). Master added both in
    #65851.
    
    **The problem, and what it cost.** Both readers call rapidjson's default
    `Parse`. Its fast path rounds a long decimal significand to a double and
    then scales it by a power of ten, so a number with a long significand
    can be rounded twice and land on the neighbouring double.
    
    | | Master today | After this PR |
    |---|---|---|
    | `[0.18172760479972437302]` read by V1 / V2 | `0x3fc742d9a3b296db` |
    `0x3fc742d9a3b296dc` (correctly rounded, same as Spark and Python) |
    | Rows from files written before the column existed | can differ from
    Spark in the last bit | identical to Spark |
    
    **How this PR fixes it.** The two `Parse` calls become
    `Parse<rapidjson::kParseFullPrecisionFlag>`, as on branch-4.1 and
    branch-4.2. Top-level scalar defaults do not go through this JSON parse
    and are unchanged.
    
    **Results.** The two unit tests ported from #66538,
    
`IcebergReaderTest.materializes_complex_double_initial_default_at_full_precision`
    (V1) and
    `IcebergV2ReaderTest.AnnotateBuildsComplexDoubleDefaultAtFullPrecision`
    (V2), check the exact bits of the decoded double. Without the fix both
    fail with `0x3fc742d9a3b296db`; with it both pass, and all 144 tests of
    the two reader suites pass.
    
    Co-authored-by: daidai <[email protected]>
---
 be/src/format/table/iceberg_default_value.h        |  3 +-
 be/src/format_v2/table/iceberg_reader.cpp          |  3 +-
 .../format/table/iceberg/iceberg_reader_test.cpp   | 39 ++++++++++++++++++++++
 be/test/format_v2/table/iceberg_reader_test.cpp    | 39 ++++++++++++++++++++++
 4 files changed, 82 insertions(+), 2 deletions(-)

diff --git a/be/src/format/table/iceberg_default_value.h 
b/be/src/format/table/iceberg_default_value.h
index 5fe1834e3bc..9095ef6b9c5 100644
--- a/be/src/format/table/iceberg_default_value.h
+++ b/be/src/format/table/iceberg_default_value.h
@@ -418,7 +418,8 @@ inline Status build_initial_default_field(const 
schema::external::TField& field,
     const auto primitive_type = value_type->get_primitive_type();
     if (is_complex_type(primitive_type)) {
         rapidjson::Document document;
-        document.Parse(field.initial_default_value.data(), 
field.initial_default_value.size());
+        
document.Parse<rapidjson::kParseFullPrecisionFlag>(field.initial_default_value.data(),
+                                                           
field.initial_default_value.size());
         if (document.HasParseError()) {
             return Status::InvalidArgument("Invalid Iceberg JSON initial 
default for field '{}'",
                                            field.name);
diff --git a/be/src/format_v2/table/iceberg_reader.cpp 
b/be/src/format_v2/table/iceberg_reader.cpp
index b1e1e704457..151744480ee 100644
--- a/be/src/format_v2/table/iceberg_reader.cpp
+++ b/be/src/format_v2/table/iceberg_reader.cpp
@@ -616,7 +616,8 @@ static Status build_v2_initial_default_field(const 
format::ColumnDefinition& fie
     }
     if (is_complex_type(primitive_type)) {
         rapidjson::Document document;
-        document.Parse(field.initial_default_value->data(), 
field.initial_default_value->size());
+        
document.Parse<rapidjson::kParseFullPrecisionFlag>(field.initial_default_value->data(),
+                                                           
field.initial_default_value->size());
         if (document.HasParseError()) {
             return Status::InvalidArgument("Invalid Iceberg JSON initial 
default for field '{}'",
                                            field.name);
diff --git a/be/test/format/table/iceberg/iceberg_reader_test.cpp 
b/be/test/format/table/iceberg/iceberg_reader_test.cpp
index 904f6f88589..528ed13f0f4 100644
--- a/be/test/format/table/iceberg/iceberg_reader_test.cpp
+++ b/be/test/format/table/iceberg/iceberg_reader_test.cpp
@@ -28,6 +28,7 @@
 #include <parquet/arrow/writer.h>
 
 #include <array>
+#include <bit>
 #include <cmath>
 #include <filesystem>
 #include <fstream>
@@ -1403,6 +1404,44 @@ protected:
                                                                                
    {"pos", 1}};
 };
 
+TEST_F(IcebergReaderTest, 
materializes_complex_double_initial_default_at_full_precision) {
+    auto element = std::make_shared<schema::external::TField>();
+    element->__set_name("element");
+    element->__set_id(2);
+    element->__set_is_optional(false);
+    TColumnType element_thrift_type;
+    element_thrift_type.__set_type(TPrimitiveType::DOUBLE);
+    element->__set_type(element_thrift_type);
+    schema::external::TFieldPtr element_ptr;
+    element_ptr.__set_field_ptr(std::move(element));
+
+    schema::external::TField values;
+    values.__set_name("values");
+    values.__set_id(1);
+    values.__set_is_optional(true);
+    values.__set_initial_default_value("[0.18172760479972437302]");
+    TColumnType values_thrift_type;
+    values_thrift_type.__set_type(TPrimitiveType::ARRAY);
+    values.__set_type(values_thrift_type);
+    schema::external::TArrayField array_metadata;
+    array_metadata.__set_item_field(std::move(element_ptr));
+    values.nestedField.__set_array_field(std::move(array_metadata));
+    values.__isset.nestedField = true;
+
+    const auto double_type = std::make_shared<DataTypeFloat64>();
+    const auto values_type = 
make_nullable(std::make_shared<DataTypeArray>(double_type));
+    ColumnPtr default_column;
+    ASSERT_TRUE(iceberg::create_initial_default_column(values, values_type, 
&default_column).ok());
+    const auto& nullable = assert_cast<const ColumnNullable&>(*default_column);
+    ASSERT_FALSE(nullable.is_null_at(0));
+    const auto& array = assert_cast<const 
ColumnArray&>(nullable.get_nested_column());
+    const auto& nullable_doubles = assert_cast<const 
ColumnNullable&>(array.get_data());
+    ASSERT_FALSE(nullable_doubles.is_null_at(0));
+    const auto& doubles = assert_cast<const 
ColumnFloat64&>(nullable_doubles.get_nested_column());
+    ASSERT_EQ(doubles.size(), 1);
+    EXPECT_EQ(std::bit_cast<uint64_t>(doubles.get_data()[0]), 
0x3fc742d9a3b296dcULL);
+}
+
 TEST_F(IcebergReaderTest, detects_fully_dictionary_encoded_parquet_column) {
     tparquet::ColumnMetaData column_metadata;
     column_metadata.type = tparquet::Type::BYTE_ARRAY;
diff --git a/be/test/format_v2/table/iceberg_reader_test.cpp 
b/be/test/format_v2/table/iceberg_reader_test.cpp
index 9a7af2a574d..adcecd1c3ad 100644
--- a/be/test/format_v2/table/iceberg_reader_test.cpp
+++ b/be/test/format_v2/table/iceberg_reader_test.cpp
@@ -26,6 +26,7 @@
 
 #include <algorithm>
 #include <array>
+#include <bit>
 #include <chrono>
 #include <cmath>
 #include <cstring>
@@ -2130,6 +2131,44 @@ TEST(IcebergV2ReaderTest, 
AnnotateBuildsComplexInitialDefaults) {
     EXPECT_EQ(values[0].get<TYPE_INT>(), 9);
 }
 
+TEST(IcebergV2ReaderTest, AnnotateBuildsComplexDoubleDefaultAtFullPrecision) {
+    const auto double_type = std::make_shared<DataTypeFloat64>();
+    const auto list_type = 
make_nullable(std::make_shared<DataTypeArray>(double_type));
+    auto list_element =
+            external_schema_field("element", 2, {}, std::nullopt,
+                                  
external_primitive_type(TPrimitiveType::DOUBLE), false, false);
+    auto list_field = external_schema_field("values", 1, {}, 
"[0.18172760479972437302]",
+                                            std::nullopt, false, true);
+    schema::external::TArrayField array_metadata;
+    array_metadata.__set_item_field(std::move(list_element));
+    
list_field.field_ptr->nestedField.__set_array_field(std::move(array_metadata));
+    list_field.field_ptr->__isset.nestedField = true;
+    TFileScanRangeParams scan_params;
+    
scan_params.__set_iceberg_scan_semantics_version(ICEBERG_SCAN_SEMANTICS_VERSION_2);
+    scan_params.__set_current_schema_id(100);
+    scan_params.__set_history_schema_info({external_schema(100, 
{std::move(list_field)})});
+
+    ColumnDefinition column;
+    column.name = "values";
+    column.type = list_type;
+    ProjectedColumnBuildContext context {.scan_params = &scan_params};
+    doris::format::iceberg::IcebergTableReader reader;
+    const auto status = reader.annotate_projected_column(TFileScanSlotInfo(), 
&context, &column);
+    ASSERT_TRUE(status.ok()) << status;
+    ASSERT_NE(column.default_expr, nullptr);
+    const auto* literal = dynamic_cast<const 
VLiteral*>(column.default_expr->root().get());
+    ASSERT_NE(literal, nullptr);
+    const auto literal_column = 
literal->get_column_ptr()->convert_to_full_column_if_const();
+    const auto& nullable = assert_cast<const ColumnNullable&>(*literal_column);
+    ASSERT_FALSE(nullable.is_null_at(0));
+    const auto& array = assert_cast<const 
ColumnArray&>(nullable.get_nested_column());
+    const auto& nullable_doubles = assert_cast<const 
ColumnNullable&>(array.get_data());
+    ASSERT_FALSE(nullable_doubles.is_null_at(0));
+    const auto& doubles = assert_cast<const 
ColumnFloat64&>(nullable_doubles.get_nested_column());
+    ASSERT_EQ(doubles.size(), 1);
+    EXPECT_EQ(std::bit_cast<uint64_t>(doubles.get_data()[0]), 
0x3fc742d9a3b296dcULL);
+}
+
 TEST(IcebergV2ReaderTest, ComplexInitialDefaultPrefersExactChildNameOverAlias) 
{
     const auto int_type = std::make_shared<DataTypeInt32>();
     const auto struct_type =


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to