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

Gabriel39 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 c3e2004ebc9 [fix](iceberg) Guard synthesized slots in 
_create_column_ids (#65610)
c3e2004ebc9 is described below

commit c3e2004ebc9eb23c5477e59c9398613d05503fab
Author: daidai <[email protected]>
AuthorDate: Thu Jul 16 10:11:58 2026 +0800

    [fix](iceberg) Guard synthesized slots in _create_column_ids (#65610)
    
    ### What problem does this PR solve?
    Related PR: #65502
    
    Problem Summary:
    Iceberg Parquet/ORC scans resolve each projected column by name through
    the StructNode in _create_column_ids. Synthesized/metadata columns — the
    TopN global row-id and the $row_id column — are never serialized into
    the schema tree, so the node has no entry for them. Calling
    children_column_exists() on such an unregistered name hits
    DCHECK(children.contains(name)) and aborts in debug/ASAN builds (and
    throws std::out_of_range from .at() in release builds), crashing during
    reader init on a TopN projection over an Iceberg table.
    
    Guard the lookup with
    struct_node->get_children().contains(slot->col_name()) before querying
    the child (reusing the pattern already used by the equality-delete
    expand-column path in the same file). Synthesized slots are skipped
    instead of aborting. Name-based resolution and its
    partial-id/name-mapping correctness are unchanged, and the shared
    children_column_exists() helper is left untouched. Applied to both the
    Parquet and ORC _create_column_ids.
    
    
    Made enable_file_scanner_v2 a fuzzy , so external regression runs
    randomly exercise both the V1 and V2 scan paths — exactly the two paths
    this fix spans.
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [ ] Regression test
        - [x] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    - Behavior changed:
        - [x] No.
        - [ ] Yes. <!-- Explain the behavior change -->
    
    - Does this need documentation?
        - [x] No.
    - [ ] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/1214 -->
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label <!-- Add branch pick label that this PR
    should merge into -->
---
 be/src/format/table/iceberg_reader.cpp             | 36 ++++++--
 .../iceberg_reader_create_column_ids_test.cpp      | 95 +++++++++++++++++++++-
 .../java/org/apache/doris/qe/SessionVariable.java  |  6 +-
 3 files changed, 129 insertions(+), 8 deletions(-)

diff --git a/be/src/format/table/iceberg_reader.cpp 
b/be/src/format/table/iceberg_reader.cpp
index 38bb395120d..d4040d7ec4a 100644
--- a/be/src/format/table/iceberg_reader.cpp
+++ b/be/src/format/table/iceberg_reader.cpp
@@ -364,15 +364,27 @@ ColumnIdResult IcebergParquetReader::_create_column_ids(
                 IcebergParquetNestedColumnUtils::extract_nested_column_ids);
     };
 
+    // The Iceberg schema-mapping root is a StructNode whose registered 
children are the real
+    // table columns. When present, resolve each column by name through it so 
the column-id set
+    // stays consistent with the schema-mapping decision (BY_ID or 
BY_NAME/name-mapping);
+    // otherwise fall back to matching by Iceberg field id.
+    const auto* struct_node =
+            dynamic_cast<const 
TableSchemaChangeHelper::StructNode*>(table_info_node.get());
+
     for (const auto* slot : tuple_descriptor->slots()) {
         const FieldSchema* field_schema = nullptr;
-        if (table_info_node != nullptr) {
-            if (table_info_node->children_column_exists(slot->col_name())) {
+        if (struct_node != nullptr) {
+            // Synthesized/metadata slots (e.g. the TopN global row-id or the 
$row_id column) are
+            // never registered as children, so check membership before 
querying: calling
+            // children_column_exists() on an unregistered name DCHECK-aborts 
in debug builds and
+            // throws std::out_of_range from .at() in release builds.
+            if (struct_node->get_children().contains(slot->col_name()) &&
+                struct_node->children_column_exists(slot->col_name())) {
                 // Use the physical child selected by the schema-mapping pass. 
This keeps partial-id
                 // files in BY_NAME mode from binding a projected column 
through an unrelated stale
                 // field id.
                 const auto& file_column_name =
-                        
table_info_node->children_file_column_name(slot->col_name());
+                        
struct_node->children_file_column_name(slot->col_name());
                 for (int i = 0; i < field_desc->size(); ++i) {
                     const auto* candidate = field_desc->get_column(i);
                     if (candidate != nullptr && candidate->name == 
file_column_name) {
@@ -691,15 +703,27 @@ ColumnIdResult IcebergOrcReader::_create_column_ids(
                 IcebergOrcNestedColumnUtils::extract_nested_column_ids);
     };
 
+    // The Iceberg schema-mapping root is a StructNode whose registered 
children are the real
+    // table columns. When present, resolve each column by name through it so 
the column-id set
+    // stays consistent with the schema-mapping decision (BY_ID or 
BY_NAME/name-mapping);
+    // otherwise fall back to matching by Iceberg field id.
+    const auto* struct_node =
+            dynamic_cast<const 
TableSchemaChangeHelper::StructNode*>(table_info_node.get());
+
     for (const auto* slot : tuple_descriptor->slots()) {
         const orc::Type* orc_field = nullptr;
-        if (table_info_node != nullptr) {
-            if (table_info_node->children_column_exists(slot->col_name())) {
+        if (struct_node != nullptr) {
+            // Synthesized/metadata slots (e.g. the TopN global row-id or the 
$row_id column) are
+            // never registered as children, so check membership before 
querying: calling
+            // children_column_exists() on an unregistered name DCHECK-aborts 
in debug builds and
+            // throws std::out_of_range from .at() in release builds.
+            if (struct_node->get_children().contains(slot->col_name()) &&
+                struct_node->children_column_exists(slot->col_name())) {
                 // Select the physical child resolved by the shared 
schema-mapping pass. Hidden
                 // equality keys and projected columns must obey the same 
BY_NAME decision for
                 // partial-id ORC files.
                 const auto& file_column_name =
-                        
table_info_node->children_file_column_name(slot->col_name());
+                        
struct_node->children_file_column_name(slot->col_name());
                 for (uint64_t i = 0; i < orc_type->getSubtypeCount(); ++i) {
                     if (orc_type->getFieldName(i) == file_column_name) {
                         orc_field = orc_type->getSubtype(i);
diff --git 
a/be/test/format/table/iceberg/iceberg_reader_create_column_ids_test.cpp 
b/be/test/format/table/iceberg/iceberg_reader_create_column_ids_test.cpp
index e32153d1ef7..c15cb8931b7 100644
--- a/be/test/format/table/iceberg/iceberg_reader_create_column_ids_test.cpp
+++ b/be/test/format/table/iceberg/iceberg_reader_create_column_ids_test.cpp
@@ -28,6 +28,7 @@
 #include <unordered_map>
 #include <vector>
 
+#include "common/consts.h"
 #include "common/object_pool.h"
 #include "core/block/block.h"
 #include "core/block/column_with_type_and_name.h"
@@ -1166,4 +1167,96 @@ TEST_F(IcebergReaderCreateColumnIdsTest, 
test_create_column_ids_6) {
     }
 }
 
-} // namespace doris
\ No newline at end of file
+// Regression coverage for _create_column_ids() driven by the schema-mapping 
StructNode:
+//   1. Synthesized/metadata slots (e.g. the TopN global row-id column) are 
never serialized into
+//      the Iceberg schema tree, so they are absent from the node. Before the
+//      get_children().contains() guard, StructNode::children_column_exists() 
was called on such an
+//      unregistered name and hit DCHECK(children.contains(name)) -> abort in 
debug/ASAN builds
+//      (and threw std::out_of_range from .at() in release). The projected 
row-id slot reproduces
+//      that; with the guard it is skipped instead.
+//   2. A projected column must resolve to its physical file column BY NAME 
through the node, not by
+//      Iceberg field id and not by its own (table) name, so partial-id / 
name-mapping files stay
+//      correct. Table column "a" maps to physical "legacy_a", while an 
unrelated "stale" column
+//      carries a field id that collides with the projected slot's default 
field id (0). The result
+//      must be "legacy_a"'s column id, never "stale"'s -- a regression to 
id-only or identity-name
+//      binding would produce a different set and fail here.
+TEST_F(IcebergReaderCreateColumnIdsTest, 
parquet_name_mapped_and_synthesized_slots) {
+    // Physical Parquet schema (BY_NAME / partial-id file):
+    //   legacy_a: id-less real data column            -> column id 1
+    //   stale:    unrelated column carrying stale id 0 -> column id 2
+    FieldDescriptor field_desc;
+    FieldSchema legacy_a;
+    legacy_a.name = "legacy_a";
+    legacy_a.data_type =
+            
DataTypeFactory::instance().create_data_type(PrimitiveType::TYPE_BIGINT, true);
+    legacy_a.field_id = -1;
+    field_desc._fields.emplace_back(legacy_a);
+    FieldSchema stale;
+    stale.name = "stale";
+    stale.data_type =
+            
DataTypeFactory::instance().create_data_type(PrimitiveType::TYPE_BIGINT, true);
+    stale.field_id = 0; // collides with the projected slot's default 
col_unique_id (0)
+    field_desc._fields.emplace_back(stale);
+
+    // Table column "a" resolves BY NAME to physical "legacy_a"; the 
synthesized global row-id
+    // column is intentionally NOT registered.
+    auto struct_node = std::make_shared<TableSchemaChangeHelper::StructNode>();
+    struct_node->add_children("a", "legacy_a", 
TableSchemaChangeHelper::ConstNode::get_instance());
+    std::shared_ptr<TableSchemaChangeHelper::Node> table_info_node = 
struct_node;
+
+    SlotDescriptor a_slot;
+    a_slot._type = 
DataTypeFactory::instance().create_data_type(PrimitiveType::TYPE_BIGINT, true);
+    a_slot._col_name = "a";
+
+    SlotDescriptor row_id_slot;
+    row_id_slot._type =
+            
DataTypeFactory::instance().create_data_type(PrimitiveType::TYPE_BIGINT, true);
+    row_id_slot._col_name = BeConsts::GLOBAL_ROWID_COL;
+
+    TupleDescriptor tuple_desc;
+    tuple_desc.add_slot(&a_slot);
+    tuple_desc.add_slot(&row_id_slot);
+
+    // No abort on the synthesized slot; "a" resolves BY NAME to "legacy_a" 
(column id 1), never to
+    // "stale" (column id 2) through the colliding field id.
+    const ColumnIdResult result =
+            IcebergParquetReader::_create_column_ids(&field_desc, &tuple_desc, 
table_info_node);
+    EXPECT_EQ(result.column_ids, (std::set<uint64_t> {1}));
+    EXPECT_TRUE(result.filter_column_ids.empty());
+}
+
+TEST_F(IcebergReaderCreateColumnIdsTest, 
orc_name_mapped_and_synthesized_slots) {
+    // Physical ORC schema (BY_NAME): legacy_a -> column id 1, stale -> column 
id 2. "stale" carries
+    // an iceberg.id attribute colliding with the projected slot's default 
field id (0).
+    std::unique_ptr<orc::Type> orc_type(
+            
orc::Type::buildTypeFromString("struct<legacy_a:bigint,stale:bigint>"));
+    
orc_type->getSubtype(1)->setAttribute(IcebergOrcReader::ICEBERG_ORC_ATTRIBUTE, 
"0");
+
+    // Table column "a" resolves BY NAME to physical "legacy_a"; the 
synthesized global row-id
+    // column is intentionally NOT registered.
+    auto struct_node = std::make_shared<TableSchemaChangeHelper::StructNode>();
+    struct_node->add_children("a", "legacy_a", 
TableSchemaChangeHelper::ConstNode::get_instance());
+    std::shared_ptr<TableSchemaChangeHelper::Node> table_info_node = 
struct_node;
+
+    SlotDescriptor a_slot;
+    a_slot._type = 
DataTypeFactory::instance().create_data_type(PrimitiveType::TYPE_BIGINT, true);
+    a_slot._col_name = "a";
+
+    SlotDescriptor row_id_slot;
+    row_id_slot._type =
+            
DataTypeFactory::instance().create_data_type(PrimitiveType::TYPE_BIGINT, true);
+    row_id_slot._col_name = BeConsts::GLOBAL_ROWID_COL;
+
+    TupleDescriptor tuple_desc;
+    tuple_desc.add_slot(&a_slot);
+    tuple_desc.add_slot(&row_id_slot);
+
+    // No abort on the synthesized slot; "a" resolves BY NAME to "legacy_a" 
(column id 1), never to
+    // "stale" (column id 2) through the colliding field id.
+    const ColumnIdResult result =
+            IcebergOrcReader::_create_column_ids(orc_type.get(), &tuple_desc, 
table_info_node);
+    EXPECT_EQ(result.column_ids, (std::set<uint64_t> {1}));
+    EXPECT_TRUE(result.filter_column_ids.empty());
+}
+
+} // namespace doris
diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java 
b/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
index 3c354128075..aa70a4f6ea6 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
@@ -1166,7 +1166,7 @@ public class SessionVariable implements Serializable, 
Writable {
             "FileScanNode 扫描数据的最大并发,默认为 16", "The max threads to read data of 
FileScanNode, default 16"})
     public int maxFileScannersConcurrency = 16;
 
-    @VarAttrDef.VarAttr(name = ENABLE_FILE_SCANNER_V2, needForward = true, 
description = {
+    @VarAttrDef.VarAttr(name = ENABLE_FILE_SCANNER_V2, needForward = true, 
fuzzy = true, description = {
             "开启后 FileScanNode 会在支持的查询场景使用 FileScannerV2,默认开启",
             "When enabled, FileScanNode uses FileScannerV2 for supported query 
scans. Enabled by default."})
     public boolean enableFileScannerV2 = true;
@@ -3841,6 +3841,10 @@ public class SessionVariable implements Serializable, 
Writable {
         this.enableLocalExchange = random.nextBoolean();
         this.enableSharedExchangeSinkBuffer = random.nextBoolean();
         this.useSerialExchange = random.nextBoolean();
+        // Randomize the external file scanner engine (FileScannerV2 vs the 
legacy V1 path). Kept
+        // here rather than in setFuzzyForCatalog() so it also runs in the 
external regression
+        // pipeline, which enables fuzzy sessions with fuzzy_test_type=p1 (not 
"external").
+        this.enableFileScannerV2 = random.nextBoolean();
         this.disableStreamPreaggregations = random.nextBoolean();
         this.enableStreamingAggHashJoinForcePassthrough = random.nextBoolean();
         this.enableLocalExchangeBeforeAgg = random.nextBoolean();


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

Reply via email to