github-actions[bot] commented on code in PR #66825:
URL: https://github.com/apache/doris/pull/66825#discussion_r3793865758


##########
be/src/exec/pipeline/pipeline_fragment_context.cpp:
##########
@@ -2205,8 +2175,20 @@ void 
PipelineFragmentContext::_coordinator_callback(const ReportStatusRequest& r
         params.__set_backend_id(_exec_env->cluster_info()->backend_id);
     }
 
+    Status report_size_status = validate_report_exec_status_size(
+            params, req.runtime_state->coordinator_thrift_message_limit());
+    if (!report_size_status.ok()) {
+        if (req.done) {

Review Comment:
   [P1] Retain a cleanup owner for rejected Hive S3 reports
   
   This pre-RPC rejection finalizes the report as REJECTED, but only the 
Iceberg writers register rejected-report cleanup callbacks. Hive keeps its S3 
upload IDs only in the final `THivePartitionUpdate`, and `S3FileWriter` 
deliberately does not abort the MPU on destruction. When the combined final 
report exceeds the limit, FE never receives those IDs and neither side owns 
cleanup, leaving every staged upload indefinitely unless the bucket happens to 
have a lifecycle rule. Register a Hive/S3 rejection cleanup handle, or enforce 
the report budget before any upload is staged.



##########
be/src/exec/pipeline/pipeline_fragment_context.cpp:
##########
@@ -2235,14 +2217,40 @@ void 
PipelineFragmentContext::_coordinator_callback(const ReportStatusRequest& r
 
         rpc_status = Status::create<false>(res.status);
     } catch (apache::thrift::TException& e) {
+        report_outcome_ambiguous = true;
         rpc_status = Status::InternalError("ReportExecStatus() to {} failed: 
{}",
                                            
PrintThriftNetworkAddress(req.coord_addr), e.what());
     }
 
+    // Only Iceberg keeps BE rollback callbacks after close; the other vectors 
remain compatible
+    // with coordinators that acknowledge acceptance through the RPC status 
alone.
+    const bool requires_external_file_ack = 
params.__isset.iceberg_commit_datas;
+    if (rpc_status.ok() && requires_external_file_ack &&
+        (!res.__isset.external_file_commit_data_accepted ||
+         !res.external_file_commit_data_accepted)) {
+        rpc_status = Status::InternalError(
+                "Coordinator did not accept ownership of the external-file 
report");
+    }
+
     if (!rpc_status.ok()) {
+        if (req.done && !report_outcome_ambiguous) {

Review Comment:
   [P1] Mark the first report attempt ambiguous before retrying
   
   A `TTransportException` from the first call can mean FE accepted the commit 
data but its response was lost. The inner catch retries without setting 
`report_outcome_ambiguous`; if the retry then returns an explicit error, this 
branch treats the report as REJECTED and runs Iceberg rollback callbacks. FE 
publishes acceptance before replying, but a restart or lost coordinator/cache 
can make the retry reject after the table commit, so BE can delete files 
already referenced by the committed snapshot. Mark ownership ambiguous as soon 
as the first send has an uncertain outcome, and do not downgrade it on a later 
rejection.



##########
be/src/format_v2/column_mapper.cpp:
##########
@@ -2204,15 +2163,15 @@ Status 
TableColumnMapper::_create_mapping_for_column(const ColumnDefinition& tab
         // Doris internal Iceberg row locator is never a physical Iceberg data 
column. It is built
         // from file path, row position and partition metadata for 
delete/update/merge.
         mapping->virtual_column_type = TableVirtualColumnType::ICEBERG_ROWID;
-    } else if (table_column.initial_default_value.has_value()) {
-        VExprContextSPtr initial_default;
-        RETURN_IF_ERROR(build_initial_default_literal(table_column, 
&initial_default));
-        // Iceberg metadata is the authoritative logical value for files 
written before the field
-        // existed; the generic FE expression may still contain its Base64 
transport text.
-        _set_constant_mapping(mapping, std::move(initial_default));
     } else if (table_column.default_expr != nullptr) {

Review Comment:
   [P1] Preserve V1 binary initial-default decoding
   
   For a semantics-V1 request, `IcebergTableReader` does not replace the 
generic FE `default_expr` with a typed Iceberg literal; that happens only in 
V2, while the request still carries the Base64 initial-default metadata. 
Selecting `default_expr` first therefore makes a new BE materialize Base64 text 
for UUID/BINARY/FIXED fields instead of the logical bytes. The existing 
`IcebergInitialDefaultMetadataOverridesGenericBinaryDefaultExpr` test 
constructs exactly this V1 state and expects the decoded 16-byte value. Please 
preserve the metadata decode precedence for V1, or prepare the typed binary 
literal for both semantics versions.



##########
be/src/format_v2/column_mapper.cpp:
##########
@@ -2770,10 +2729,24 @@ Status TableColumnMapper::_create_direct_mapping(const 
ColumnDefinition& table_c
                 child_mapping.file_type = table_child.type;
                 child_mapping.variant_access_paths = 
table_child.variant_access_paths;
                 child_mapping.filter_conversion = 
FilterConversionType::FINALIZE_ONLY;
-                // A missing nested field still has its Iceberg 
initial-default value in every row
-                // written before the field was added; carry it into recursive 
materialization.
-                RETURN_IF_ERROR(build_initial_default_column(
-                        table_child, &child_mapping.initial_default_column));
+                if (table_child.default_expr != nullptr) {
+                    const auto* literal =
+                            dynamic_cast<const 
VLiteral*>(table_child.default_expr->root().get());
+                    if (literal == nullptr) {
+                        return Status::InvalidArgument(
+                                "Missing typed initial-default literal for 
table field '{}'",
+                                table_child.name);
+                    }
+                    // Keep the literal's owning column alive for recursive 
materialization.
+                    child_mapping.initial_default_column = 
literal->get_column_ptr();
+                } else if (table_child.initial_default_value.has_value()) {

Review Comment:
   [P1] Carry typed defaults into projected nested children
   
   V2 prepares typed default expressions recursively on 
`build_context.schema_column`, but `FileScannerV2` then rebuilds the actual 
projected child tree through `AccessPathParser`. Its metadata inheritance 
copies the raw initial value and Base64 bit, not the prepared `default_expr`, 
so a missing nested child reaches this new branch and fails with `Missing typed 
initial-default expression` instead of materializing its value. The existing 
`ProjectedStructFillsMissingChildWithBinaryInitialDefault` test is a direct 
witness. Copy the prepared literal into projected children, or prepare the 
completed projected tree after access-path expansion.



##########
be/src/format_v2/column_mapper.cpp:
##########
@@ -2204,15 +2163,15 @@ Status 
TableColumnMapper::_create_mapping_for_column(const ColumnDefinition& tab
         // Doris internal Iceberg row locator is never a physical Iceberg data 
column. It is built
         // from file path, row position and partition metadata for 
delete/update/merge.
         mapping->virtual_column_type = TableVirtualColumnType::ICEBERG_ROWID;
-    } else if (table_column.initial_default_value.has_value()) {
-        VExprContextSPtr initial_default;
-        RETURN_IF_ERROR(build_initial_default_literal(table_column, 
&initial_default));
-        // Iceberg metadata is the authoritative logical value for files 
written before the field
-        // existed; the generic FE expression may still contain its Base64 
transport text.
-        _set_constant_mapping(mapping, std::move(initial_default));
     } else if (table_column.default_expr != nullptr) {
-        // Missing schema-evolution column with an explicit default expression.
+        // The table-format reader supplies a typed literal so complex and 
binary defaults stay exact.
         _set_constant_mapping(mapping, table_column.default_expr);
+    } else if (table_column.initial_default_value.has_value()) {
+        return Status::InvalidArgument(
+                "Missing typed initial-default expression for table field 
'{}'", table_column.name);
+    } else if (_options.reject_missing_required_field && 
table_column.is_optional.has_value() &&

Review Comment:
   [P1] Transport Iceberg requiredness instead of Doris nullability
   
   This rejection requires `is_optional` to be explicitly false, but 
`IcebergUtils.parseSchema` constructs every top-level Iceberg `Column` with 
`allowNull=true`, and `ExternalUtil` serializes that presentation property as 
`TField.is_optional`. Projected nested children lose `is_optional` again in 
`AccessPathParser`. Consequently the exact V2 history cases selected for 
missing-required-field rejection arrive here as true/unset and keep the generic 
NULL fallback, silently changing Iceberg semantics. Serialize 
`NestedField.isOptional()` recursively from the Iceberg schema and preserve it 
through access-path pruning.



##########
be/src/format/transformer/iceberg_partition_function.cpp:
##########
@@ -196,8 +249,13 @@ Status 
IcebergInsertPartitionFunction::_compute_hashes_with_transform(
         if (_partition_fields[i].transformer == nullptr) {
             return Status::InternalError("Merge partitioning transform is not 
initialized");
         }
+        ColumnWithTypeAndName source = block->get_by_position(results[i]);
+        if (!_partition_fields[i].source_field_path.empty()) {

Review Comment:
   [P1] Resolve the nested leaf type before creating its transform
   
   This extraction happens in `get_partitions`, but `open()` has already 
constructed the transformer with `field.expr_ctx->root()->data_type()`. For a 
nested source that is the root STRUCT, and `PartitionColumnTransforms::create` 
rejects STRUCT for bucket/truncate/time transforms before this code can reach 
the primitive leaf. The added regression uses `bucket(4, 
payload.nested.count)`, so its merge partitioner fails during open. Resolve 
`source_field_path` through the root type before constructing the transformer, 
and cover a nested non-identity transform in the BE unit test.



##########
fe/fe-core/src/main/java/org/apache/doris/qe/Coordinator.java:
##########
@@ -2587,87 +2588,108 @@ public void 
updateFragmentExecStatus(TReportExecStatusParams params) {
         }
 
         PipelineExecContext ctx = 
pipelineExecContexts.get(Pair.of(params.getFragmentId(), 
params.getBackendId()));
-        if (ctx == null || !ctx.updatePipelineStatus(params)) {
+        boolean hasExternalCommitData = params.isSetHivePartitionUpdates()
+                || params.isSetIcebergCommitDatas() || 
params.isSetMcCommitDatas()
+                || params.isSetPaimonCommitMessages();
+        if (ctx == null) {
+            if (hasExternalCommitData) {
+                throw new IllegalStateException("Missing fragment handler for 
external-file report");
+            }
+            return false;
+        }
+        if (!ctx.updatePipelineStatus(params)) {
+            if (hasExternalCommitData && !ctx.done) {

Review Comment:
   [P1] Accept legacy non-final commit vectors during rollout
   
   Legacy BEs attach any accumulated Hive/Iceberg/MC/Paimon vectors to periodic 
reports and retain them for the eventual final resend. If one parallel writer 
closes or rolls over while the fragment is still running, 
`updatePipelineStatus` returns false and this new check throws; 
`QeProcessorImpl` returns `INTERNAL_ERROR`, so the old BE cancels the write 
instead of sending its final report. Ignore or buffer legacy non-final vectors 
while returning ordinary OK, or exclude old BEs before any external write 
starts. The same compatibility handling is needed in `LoadProcessor`.



##########
be/src/format_v2/table/iceberg_reader.cpp:
##########
@@ -81,61 +92,450 @@ static bool is_projected_iceberg_rowid(const 
format::ColumnDefinition& column) {
     return column.name == BeConsts::ICEBERG_ROWID_COL;
 }
 
+static int iceberg_hex_value(char value) {
+    if (value >= '0' && value <= '9') {
+        return value - '0';
+    }
+    if (value >= 'a' && value <= 'f') {
+        return value - 'a' + 10;
+    }
+    if (value >= 'A' && value <= 'F') {
+        return value - 'A' + 10;
+    }
+    return -1;
+}
+
+static Status decode_iceberg_hex(std::string_view encoded, std::string* 
decoded) {
+    DORIS_CHECK(decoded != nullptr);
+    if ((encoded.size() & 1U) != 0) {
+        return Status::InvalidArgument("Invalid odd-length Iceberg binary 
default");
+    }
+    decoded->resize(encoded.size() / 2);
+    for (size_t index = 0; index < encoded.size(); index += 2) {
+        const int high = iceberg_hex_value(encoded[index]);
+        const int low = iceberg_hex_value(encoded[index + 1]);
+        if (high < 0 || low < 0) {
+            return Status::InvalidArgument("Invalid hexadecimal Iceberg binary 
default");
+        }
+        (*decoded)[index / 2] = static_cast<char>((high << 4) | low);
+    }
+    return Status::OK();
+}
+
+static Status decode_iceberg_json_binary(std::string_view encoded, 
std::string* decoded) {
+    DORIS_CHECK(decoded != nullptr);
+    const bool is_uuid = encoded.size() == 36 && encoded[8] == '-' && 
encoded[13] == '-' &&
+                         encoded[18] == '-' && encoded[23] == '-';
+    if (!is_uuid) {
+        return decode_iceberg_hex(encoded, decoded);
+    }
+
+    std::string uuid_hex;
+    uuid_hex.reserve(32);
+    for (size_t index = 0; index < encoded.size(); ++index) {
+        if (index != 8 && index != 13 && index != 18 && index != 23) {
+            uuid_hex.push_back(encoded[index]);
+        }
+    }
+    return decode_iceberg_hex(uuid_hex, decoded);
+}
+
+static std::string iceberg_json_scalar_text(const rapidjson::Value& value) {
+    if (value.IsString()) {
+        return {value.GetString(), value.GetStringLength()};
+    }
+    rapidjson::StringBuffer buffer;
+    rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
+    value.Accept(writer);
+    return {buffer.GetString(), buffer.GetSize()};
+}
+
+static void normalize_iceberg_json_timestamp(PrimitiveType primitive_type, 
std::string* value) {
+    if (primitive_type != TYPE_DATETIME && primitive_type != TYPE_DATETIMEV2 &&
+        primitive_type != TYPE_TIMESTAMPTZ) {
+        return;
+    }
+    if (const size_t separator = value->find('T'); separator != 
std::string::npos) {
+        (*value)[separator] = ' ';
+    }
+    if (primitive_type == TYPE_TIMESTAMPTZ) {
+        return;
+    }
+    if (value->ends_with('Z')) {
+        value->pop_back();
+        return;
+    }
+    const size_t time_start = value->find(' ');
+    if (time_start == std::string::npos) {
+        return;
+    }
+    const size_t offset = value->find_first_of("+-", time_start + 1);
+    if (offset != std::string::npos) {
+        value->erase(offset);
+    }
+}
+
+static Status build_v2_null_default(const format::ColumnDefinition& field,
+                                    const DataTypePtr& data_type, Field* 
result) {
+    DORIS_CHECK(data_type != nullptr);
+    DORIS_CHECK(result != nullptr);
+    if (field.is_optional.has_value() && !*field.is_optional) {
+        return Status::InvalidArgument("Required Iceberg field '{}' has a null 
default",
+                                       field.name);
+    }
+    if (!data_type->is_nullable()) {
+        return Status::InternalError(
+                "Optional Iceberg field '{}' has a null default, but its Doris 
type '{}' is not "
+                "nullable",
+                field.name, data_type->get_name());
+    }
+    *result = Field();
+    return Status::OK();
+}
+
+static const format::ColumnDefinition* find_v2_struct_child(const 
format::ColumnDefinition& field,
+                                                            const std::string& 
name) {
+    const auto exact_child = std::ranges::find_if(
+            field.children, [&](const auto& candidate) { return 
iequal(candidate.name, name); });
+    if (exact_child != field.children.end()) {
+        return &*exact_child;
+    }
+    const auto aliased_child = std::ranges::find_if(field.children, [&](const 
auto& candidate) {
+        return std::ranges::any_of(candidate.name_mapping,
+                                   [&](const auto& alias) { return 
iequal(alias, name); });
+    });
+    return aliased_child == field.children.end() ? nullptr : &*aliased_child;
+}
+
+static Status build_v2_initial_default_field(const format::ColumnDefinition& 
field,
+                                             const DataTypePtr& data_type,
+                                             std::deque<std::string>* 
binary_storage,
+                                             Field* result);
+
+static Status build_v2_json_default_field(const format::ColumnDefinition& 
field,
+                                          const DataTypePtr& data_type,
+                                          const rapidjson::Value& json_value,
+                                          std::deque<std::string>* 
binary_storage, Field* result);
+
+static Status build_v2_json_struct_default(const format::ColumnDefinition& 
field,
+                                           const DataTypePtr& value_type,
+                                           const rapidjson::Value& json_value,
+                                           std::deque<std::string>* 
binary_storage, Field* result) {
+    if (!json_value.IsObject()) {
+        return Status::InvalidArgument("Invalid Iceberg struct default for 
field '{}'", field.name);
+    }
+
+    const auto& struct_type = assert_cast<const DataTypeStruct&>(*value_type);
+    Struct struct_value;
+    struct_value.reserve(struct_type.get_elements().size());
+    for (size_t index = 0; index < struct_type.get_elements().size(); ++index) 
{
+        const auto* child = find_v2_struct_child(field, 
struct_type.get_element_name(index));
+        if (child == nullptr || !child->has_identifier_field_id()) {
+            return Status::InvalidArgument(
+                    "Iceberg struct default for field '{}' has incomplete 
child metadata",
+                    field.name);
+        }
+
+        const std::string child_id = 
std::to_string(child->get_identifier_field_id());
+        const auto member = json_value.FindMember(child_id.c_str());
+        Field child_value;
+        if (member == json_value.MemberEnd()) {
+            RETURN_IF_ERROR(build_v2_initial_default_field(*child, 
struct_type.get_element(index),
+                                                           binary_storage, 
&child_value));
+        } else {
+            RETURN_IF_ERROR(build_v2_json_default_field(*child, 
struct_type.get_element(index),
+                                                        member->value, 
binary_storage,
+                                                        &child_value));
+        }
+        struct_value.push_back(std::move(child_value));
+    }
+    *result = Field::create_field<TYPE_STRUCT>(std::move(struct_value));
+    return Status::OK();
+}
+
+// The child ColumnDefinition, recursively transported from the item TField, 
describes the element
+// schema and its field-level default metadata. It cannot represent a 
particular list literal's
+// length or per-position values, so the parent initial-default keeps those 
values in Iceberg's
+// single-value JSON array.
+static Status build_v2_json_array_default(const format::ColumnDefinition& 
field,
+                                          const DataTypePtr& value_type,
+                                          const rapidjson::Value& json_value,
+                                          std::deque<std::string>* 
binary_storage, Field* result) {
+    if (!json_value.IsArray() || field.children.size() != 1) {
+        return Status::InvalidArgument("Invalid Iceberg list default for field 
'{}'", field.name);
+    }
+
+    const auto& array_type = assert_cast<const DataTypeArray&>(*value_type);
+    Array array_value;
+    array_value.reserve(json_value.Size());
+    for (const auto& json_element : json_value.GetArray()) {
+        Field element_value;
+        RETURN_IF_ERROR(build_v2_json_default_field(field.children.front(),
+                                                    
array_type.get_nested_type(), json_element,
+                                                    binary_storage, 
&element_value));
+        array_value.push_back(std::move(element_value));
+    }
+    *result = Field::create_field<TYPE_ARRAY>(std::move(array_value));
+    return Status::OK();
+}
+
+// The child ColumnDefinitions, recursively transported from the key/value 
TFields, describe entry
+// schemas and field-level default metadata. They cannot represent the number, 
order, or concrete
+// values of map entries, so the parent initial-default keeps the entries in 
Iceberg's single-value
+// JSON key/value arrays.
+static Status build_v2_json_map_default(const format::ColumnDefinition& field,
+                                        const DataTypePtr& value_type,
+                                        const rapidjson::Value& json_value,
+                                        std::deque<std::string>* 
binary_storage, Field* result) {
+    if (!json_value.IsObject() || !json_value.HasMember("keys") || 
!json_value["keys"].IsArray() ||
+        !json_value.HasMember("values") || !json_value["values"].IsArray() ||
+        field.children.size() != 2) {
+        return Status::InvalidArgument("Invalid Iceberg map default for field 
'{}'", field.name);
+    }
+    const auto& keys = json_value["keys"];
+    const auto& values = json_value["values"];
+    if (keys.Size() != values.Size()) {
+        return Status::InvalidArgument(
+                "Iceberg map default for field '{}' has {} keys but {} 
values", field.name,
+                keys.Size(), values.Size());
+    }
+
+    const auto& map_type = assert_cast<const DataTypeMap&>(*value_type);
+    Array key_fields;
+    Array value_fields;
+    key_fields.reserve(keys.Size());
+    value_fields.reserve(values.Size());
+    for (rapidjson::SizeType index = 0; index < keys.Size(); ++index) {
+        Field key_value;
+        Field mapped_value;
+        RETURN_IF_ERROR(build_v2_json_default_field(field.children[0], 
map_type.get_key_type(),
+                                                    keys[index], 
binary_storage, &key_value));
+        RETURN_IF_ERROR(build_v2_json_default_field(field.children[1], 
map_type.get_value_type(),
+                                                    values[index], 
binary_storage, &mapped_value));
+        key_fields.push_back(std::move(key_value));
+        value_fields.push_back(std::move(mapped_value));
+    }
+    Map map_value;
+    
map_value.push_back(Field::create_field<TYPE_ARRAY>(std::move(key_fields)));
+    
map_value.push_back(Field::create_field<TYPE_ARRAY>(std::move(value_fields)));
+    *result = Field::create_field<TYPE_MAP>(std::move(map_value));
+    return Status::OK();
+}
+
+static Status build_v2_json_scalar_default(const format::ColumnDefinition& 
field,
+                                           const DataTypePtr& value_type,
+                                           const rapidjson::Value& json_value,
+                                           std::deque<std::string>* 
binary_storage, Field* result) {
+    const auto primitive_type = value_type->get_primitive_type();
+    std::string serialized_value = iceberg_json_scalar_text(json_value);
+    const bool binary_like =
+            field.initial_default_value_is_base64 || primitive_type == 
TYPE_VARBINARY;
+    if (binary_like) {
+        if (!json_value.IsString()) {
+            return Status::InvalidArgument(
+                    "Iceberg binary default for field '{}' is not a JSON 
string", field.name);
+        }
+        binary_storage->emplace_back();
+        RETURN_IF_ERROR(decode_iceberg_json_binary(serialized_value, 
&binary_storage->back()));
+        if (primitive_type == TYPE_VARBINARY) {
+            *result = 
Field::create_field<TYPE_VARBINARY>(StringView(binary_storage->back()));
+        } else if (is_string_type(primitive_type)) {
+            *result = Field::create_field<TYPE_STRING>(binary_storage->back());
+        } else {
+            return Status::InvalidArgument(
+                    "Iceberg binary default for field '{}' has incompatible 
Doris type '{}'",
+                    field.name, value_type->get_name());
+        }
+        return Status::OK();
+    }
+
+    if (is_string_type(primitive_type)) {
+        if (!json_value.IsString()) {
+            return Status::InvalidArgument("Iceberg string default for field 
'{}' is not a string",
+                                           field.name);
+        }
+        *result = 
Field::create_field<TYPE_STRING>(std::move(serialized_value));
+        return Status::OK();
+    }
+    normalize_iceberg_json_timestamp(primitive_type, &serialized_value);
+    if (doris::iceberg::detail::parse_non_finite_default(primitive_type, 
serialized_value,
+                                                         result)) {
+        return Status::OK();
+    }
+    RETURN_IF_ERROR(value_type->get_serde()->from_fe_string(serialized_value, 
*result));
+    return Status::OK();
+}
+
+static Status build_v2_json_default_field(const format::ColumnDefinition& 
field,
+                                          const DataTypePtr& data_type,
+                                          const rapidjson::Value& json_value,
+                                          std::deque<std::string>* 
binary_storage, Field* result) {
+    DORIS_CHECK(data_type != nullptr);
+    DORIS_CHECK(binary_storage != nullptr);
+    DORIS_CHECK(result != nullptr);
+    if (json_value.IsNull()) {
+        return build_v2_null_default(field, data_type, result);
+    }
+
+    const auto value_type = remove_nullable(data_type);
+    switch (value_type->get_primitive_type()) {
+    case TYPE_STRUCT:
+        return build_v2_json_struct_default(field, value_type, json_value, 
binary_storage, result);
+    case TYPE_ARRAY:
+        return build_v2_json_array_default(field, value_type, json_value, 
binary_storage, result);
+    case TYPE_MAP:
+        return build_v2_json_map_default(field, value_type, json_value, 
binary_storage, result);
+    default:
+        return build_v2_json_scalar_default(field, value_type, json_value, 
binary_storage, result);
+    }
+}
+
+static Status build_v2_initial_default_field(const format::ColumnDefinition& 
field,
+                                             const DataTypePtr& data_type,
+                                             std::deque<std::string>* 
binary_storage,
+                                             Field* result) {
+    DORIS_CHECK(data_type != nullptr);
+    DORIS_CHECK(binary_storage != nullptr);
+    DORIS_CHECK(result != nullptr);
+    if (!field.initial_default_value.has_value()) {
+        if (field.is_optional.has_value() && !*field.is_optional) {
+            return Status::InvalidArgument(
+                    "Required Iceberg field '{}' is missing from the data file 
and has no initial "
+                    "default",
+                    field.name);
+        }
+        return build_v2_null_default(field, data_type, result);
+    }
+
+    const auto value_type = remove_nullable(data_type);
+    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());

Review Comment:
   [P1] Send the single-value JSON representation this parser expects
   
   The new complex path parses Iceberg single-value JSON and its helpers 
require struct members keyed by field ID, list arrays, and map `keys`/`values`. 
FE still builds `initial_default_value` with 
`Transforms.identity(type).toHumanString(type, value)`, whose nested 
representation is not `SingleValueParser` JSON. A real FE-produced 
struct/list/map default therefore fails parsing or has the wrong shape before 
it can be materialized. Use Iceberg's single-value JSON serializer for complex 
defaults while preserving the existing scalar/Base64 contract, and add an 
FE-to-BE round-trip test.



##########
be/src/format_v2/table/iceberg_reader.cpp:
##########
@@ -81,61 +92,450 @@ static bool is_projected_iceberg_rowid(const 
format::ColumnDefinition& column) {
     return column.name == BeConsts::ICEBERG_ROWID_COL;
 }
 
+static int iceberg_hex_value(char value) {
+    if (value >= '0' && value <= '9') {
+        return value - '0';
+    }
+    if (value >= 'a' && value <= 'f') {
+        return value - 'a' + 10;
+    }
+    if (value >= 'A' && value <= 'F') {
+        return value - 'A' + 10;
+    }
+    return -1;
+}
+
+static Status decode_iceberg_hex(std::string_view encoded, std::string* 
decoded) {
+    DORIS_CHECK(decoded != nullptr);
+    if ((encoded.size() & 1U) != 0) {
+        return Status::InvalidArgument("Invalid odd-length Iceberg binary 
default");
+    }
+    decoded->resize(encoded.size() / 2);
+    for (size_t index = 0; index < encoded.size(); index += 2) {
+        const int high = iceberg_hex_value(encoded[index]);
+        const int low = iceberg_hex_value(encoded[index + 1]);
+        if (high < 0 || low < 0) {
+            return Status::InvalidArgument("Invalid hexadecimal Iceberg binary 
default");
+        }
+        (*decoded)[index / 2] = static_cast<char>((high << 4) | low);
+    }
+    return Status::OK();
+}
+
+static Status decode_iceberg_json_binary(std::string_view encoded, 
std::string* decoded) {
+    DORIS_CHECK(decoded != nullptr);
+    const bool is_uuid = encoded.size() == 36 && encoded[8] == '-' && 
encoded[13] == '-' &&
+                         encoded[18] == '-' && encoded[23] == '-';
+    if (!is_uuid) {
+        return decode_iceberg_hex(encoded, decoded);
+    }
+
+    std::string uuid_hex;
+    uuid_hex.reserve(32);
+    for (size_t index = 0; index < encoded.size(); ++index) {
+        if (index != 8 && index != 13 && index != 18 && index != 23) {
+            uuid_hex.push_back(encoded[index]);
+        }
+    }
+    return decode_iceberg_hex(uuid_hex, decoded);
+}
+
+static std::string iceberg_json_scalar_text(const rapidjson::Value& value) {
+    if (value.IsString()) {
+        return {value.GetString(), value.GetStringLength()};
+    }
+    rapidjson::StringBuffer buffer;
+    rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
+    value.Accept(writer);
+    return {buffer.GetString(), buffer.GetSize()};
+}
+
+static void normalize_iceberg_json_timestamp(PrimitiveType primitive_type, 
std::string* value) {
+    if (primitive_type != TYPE_DATETIME && primitive_type != TYPE_DATETIMEV2 &&
+        primitive_type != TYPE_TIMESTAMPTZ) {
+        return;
+    }
+    if (const size_t separator = value->find('T'); separator != 
std::string::npos) {
+        (*value)[separator] = ' ';
+    }
+    if (primitive_type == TYPE_TIMESTAMPTZ) {
+        return;
+    }
+    if (value->ends_with('Z')) {
+        value->pop_back();
+        return;
+    }
+    const size_t time_start = value->find(' ');
+    if (time_start == std::string::npos) {
+        return;
+    }
+    const size_t offset = value->find_first_of("+-", time_start + 1);
+    if (offset != std::string::npos) {
+        value->erase(offset);
+    }
+}
+
+static Status build_v2_null_default(const format::ColumnDefinition& field,
+                                    const DataTypePtr& data_type, Field* 
result) {
+    DORIS_CHECK(data_type != nullptr);
+    DORIS_CHECK(result != nullptr);
+    if (field.is_optional.has_value() && !*field.is_optional) {
+        return Status::InvalidArgument("Required Iceberg field '{}' has a null 
default",
+                                       field.name);
+    }
+    if (!data_type->is_nullable()) {
+        return Status::InternalError(
+                "Optional Iceberg field '{}' has a null default, but its Doris 
type '{}' is not "
+                "nullable",
+                field.name, data_type->get_name());
+    }
+    *result = Field();
+    return Status::OK();
+}
+
+static const format::ColumnDefinition* find_v2_struct_child(const 
format::ColumnDefinition& field,
+                                                            const std::string& 
name) {
+    const auto exact_child = std::ranges::find_if(
+            field.children, [&](const auto& candidate) { return 
iequal(candidate.name, name); });
+    if (exact_child != field.children.end()) {
+        return &*exact_child;
+    }
+    const auto aliased_child = std::ranges::find_if(field.children, [&](const 
auto& candidate) {
+        return std::ranges::any_of(candidate.name_mapping,
+                                   [&](const auto& alias) { return 
iequal(alias, name); });
+    });
+    return aliased_child == field.children.end() ? nullptr : &*aliased_child;
+}
+
+static Status build_v2_initial_default_field(const format::ColumnDefinition& 
field,
+                                             const DataTypePtr& data_type,
+                                             std::deque<std::string>* 
binary_storage,
+                                             Field* result);
+
+static Status build_v2_json_default_field(const format::ColumnDefinition& 
field,
+                                          const DataTypePtr& data_type,
+                                          const rapidjson::Value& json_value,
+                                          std::deque<std::string>* 
binary_storage, Field* result);
+
+static Status build_v2_json_struct_default(const format::ColumnDefinition& 
field,
+                                           const DataTypePtr& value_type,
+                                           const rapidjson::Value& json_value,
+                                           std::deque<std::string>* 
binary_storage, Field* result) {
+    if (!json_value.IsObject()) {
+        return Status::InvalidArgument("Invalid Iceberg struct default for 
field '{}'", field.name);
+    }
+
+    const auto& struct_type = assert_cast<const DataTypeStruct&>(*value_type);
+    Struct struct_value;
+    struct_value.reserve(struct_type.get_elements().size());
+    for (size_t index = 0; index < struct_type.get_elements().size(); ++index) 
{
+        const auto* child = find_v2_struct_child(field, 
struct_type.get_element_name(index));
+        if (child == nullptr || !child->has_identifier_field_id()) {
+            return Status::InvalidArgument(
+                    "Iceberg struct default for field '{}' has incomplete 
child metadata",
+                    field.name);
+        }
+
+        const std::string child_id = 
std::to_string(child->get_identifier_field_id());
+        const auto member = json_value.FindMember(child_id.c_str());
+        Field child_value;
+        if (member == json_value.MemberEnd()) {
+            RETURN_IF_ERROR(build_v2_initial_default_field(*child, 
struct_type.get_element(index),
+                                                           binary_storage, 
&child_value));
+        } else {
+            RETURN_IF_ERROR(build_v2_json_default_field(*child, 
struct_type.get_element(index),
+                                                        member->value, 
binary_storage,
+                                                        &child_value));
+        }
+        struct_value.push_back(std::move(child_value));
+    }
+    *result = Field::create_field<TYPE_STRUCT>(std::move(struct_value));
+    return Status::OK();
+}
+
+// The child ColumnDefinition, recursively transported from the item TField, 
describes the element
+// schema and its field-level default metadata. It cannot represent a 
particular list literal's
+// length or per-position values, so the parent initial-default keeps those 
values in Iceberg's
+// single-value JSON array.
+static Status build_v2_json_array_default(const format::ColumnDefinition& 
field,
+                                          const DataTypePtr& value_type,
+                                          const rapidjson::Value& json_value,
+                                          std::deque<std::string>* 
binary_storage, Field* result) {
+    if (!json_value.IsArray() || field.children.size() != 1) {
+        return Status::InvalidArgument("Invalid Iceberg list default for field 
'{}'", field.name);
+    }
+
+    const auto& array_type = assert_cast<const DataTypeArray&>(*value_type);
+    Array array_value;
+    array_value.reserve(json_value.Size());
+    for (const auto& json_element : json_value.GetArray()) {
+        Field element_value;
+        RETURN_IF_ERROR(build_v2_json_default_field(field.children.front(),
+                                                    
array_type.get_nested_type(), json_element,
+                                                    binary_storage, 
&element_value));
+        array_value.push_back(std::move(element_value));
+    }
+    *result = Field::create_field<TYPE_ARRAY>(std::move(array_value));
+    return Status::OK();
+}
+
+// The child ColumnDefinitions, recursively transported from the key/value 
TFields, describe entry
+// schemas and field-level default metadata. They cannot represent the number, 
order, or concrete
+// values of map entries, so the parent initial-default keeps the entries in 
Iceberg's single-value
+// JSON key/value arrays.
+static Status build_v2_json_map_default(const format::ColumnDefinition& field,
+                                        const DataTypePtr& value_type,
+                                        const rapidjson::Value& json_value,
+                                        std::deque<std::string>* 
binary_storage, Field* result) {
+    if (!json_value.IsObject() || !json_value.HasMember("keys") || 
!json_value["keys"].IsArray() ||
+        !json_value.HasMember("values") || !json_value["values"].IsArray() ||
+        field.children.size() != 2) {
+        return Status::InvalidArgument("Invalid Iceberg map default for field 
'{}'", field.name);
+    }
+    const auto& keys = json_value["keys"];
+    const auto& values = json_value["values"];
+    if (keys.Size() != values.Size()) {
+        return Status::InvalidArgument(
+                "Iceberg map default for field '{}' has {} keys but {} 
values", field.name,
+                keys.Size(), values.Size());
+    }
+
+    const auto& map_type = assert_cast<const DataTypeMap&>(*value_type);
+    Array key_fields;
+    Array value_fields;
+    key_fields.reserve(keys.Size());
+    value_fields.reserve(values.Size());
+    for (rapidjson::SizeType index = 0; index < keys.Size(); ++index) {
+        Field key_value;
+        Field mapped_value;
+        RETURN_IF_ERROR(build_v2_json_default_field(field.children[0], 
map_type.get_key_type(),
+                                                    keys[index], 
binary_storage, &key_value));
+        RETURN_IF_ERROR(build_v2_json_default_field(field.children[1], 
map_type.get_value_type(),
+                                                    values[index], 
binary_storage, &mapped_value));
+        key_fields.push_back(std::move(key_value));
+        value_fields.push_back(std::move(mapped_value));
+    }
+    Map map_value;
+    
map_value.push_back(Field::create_field<TYPE_ARRAY>(std::move(key_fields)));
+    
map_value.push_back(Field::create_field<TYPE_ARRAY>(std::move(value_fields)));
+    *result = Field::create_field<TYPE_MAP>(std::move(map_value));
+    return Status::OK();
+}
+
+static Status build_v2_json_scalar_default(const format::ColumnDefinition& 
field,
+                                           const DataTypePtr& value_type,
+                                           const rapidjson::Value& json_value,
+                                           std::deque<std::string>* 
binary_storage, Field* result) {
+    const auto primitive_type = value_type->get_primitive_type();
+    std::string serialized_value = iceberg_json_scalar_text(json_value);
+    const bool binary_like =
+            field.initial_default_value_is_base64 || primitive_type == 
TYPE_VARBINARY;

Review Comment:
   [P1] Preserve nested binary source types in complex defaults
   
   A binary-like JSON scalar is decoded only when this child has its own 
Base64-default marker or maps to VARBINARY. For a UUID/BINARY/FIXED element or 
value inside a complex parent's default, FE marks only the parent default; the 
child has no separate marker. With varbinary mapping disabled, that child maps 
to STRING/CHAR and takes the plain string branch, returning Iceberg's textual 
JSON spelling instead of the logical bytes. Transport the recursive Iceberg 
source type (or an equivalent binary marker) independently of whether the child 
owns an initial default, then decode all nested binary-like values.



##########
fe/fe-core/src/main/java/org/apache/doris/planner/HiveTableSink.java:
##########
@@ -93,6 +93,9 @@ public String getExplainString(String prefix, TExplainLevel 
explainLevel) {
     public void bindDataSink(Optional<InsertCommandContext> insertCtx)
             throws AnalysisException {
         THiveTableSink tSink = new THiveTableSink();
+        // The legacy planner uses the same deferred Azure protocol as the 
connector planner, so the BE
+        // must not fall back to publishing blocks before FE has durably 
accepted the commit records.
+        tSink.setSupportsDeferredAzureMultipart(true);

Review Comment:
   [P1] Gate deferred Azure writes on backend capability
   
   This flag is sent unconditionally, but an old BE ignores it and uses the 
legacy Azure path, which returns neither an upload token nor the exact staged 
block IDs. The new FE then reaches 
`HMSTransaction.validateObjectStoreCommitRecords` only after the whole write 
was staged and rejects the transaction because there is no completion record. 
Thus new-FE/old-BE rolling clusters cannot complete Azure Hive writes. Gate 
eligible backends on this protocol (or add a capability handshake) before 
opening the upload; final validation should remain only a fail-closed backstop.



##########
be/src/exec/sink/writer/iceberg/viceberg_table_writer.cpp:
##########
@@ -121,24 +129,106 @@ std::vector<VIcebergTableWriter::IcebergPartitionColumn>
 VIcebergTableWriter::_to_iceberg_partition_columns() {
     std::vector<IcebergPartitionColumn> partition_columns;
 
-    std::unordered_map<int, int> id_to_column_idx;
-    id_to_column_idx.reserve(_schema->columns().size());
-    for (int i = 0; i < _schema->columns().size(); i++) {
-        id_to_column_idx[_schema->columns()[i].field_id()] = i;
-    }
     for (const auto& partition_field : _partition_spec->fields()) {
-        int column_idx = id_to_column_idx[partition_field.source_id()];
+        const auto* field_path = 
_schema->find_field_path(partition_field.source_id());
+        if (field_path == nullptr || field_path->empty()) {

Review Comment:
   [P1] Gate nested partition sources away from old writers
   
   This PR teaches the new writer to resolve a schema-wide nested source ID, 
but a new FE can still schedule an old BE for the same table. The authoritative 
old writer indexed only top-level IDs and used `operator[]`; an unknown nested 
ID therefore default-inserted column index 0 and wrote rows under the wrong 
partition transform. There is no nested-partition writer capability or 
smooth-upgrade gate, so ordinary INSERTs and merge insert images can silently 
stage mispartitioned files. Reject such writes while an eligible old BE 
remains, or schedule them only on backends advertising the new writer behavior.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to