Coverage Report

Created: 2026-10-30 05:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format/table/iceberg_reader.cpp
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#include "format/table/iceberg_reader.h"
19
20
#include <gen_cpp/Descriptors_types.h>
21
#include <gen_cpp/Metrics_types.h>
22
#include <gen_cpp/PlanNodes_types.h>
23
#include <gen_cpp/parquet_types.h>
24
#include <glog/logging.h>
25
#include <parallel_hashmap/phmap.h>
26
#include <rapidjson/document.h>
27
28
#include <algorithm>
29
#include <cstring>
30
#include <functional>
31
#include <memory>
32
#include <optional>
33
34
#include "common/compiler_util.h" // IWYU pragma: keep
35
#include "common/consts.h"
36
#include "common/status.h"
37
#include "core/assert_cast.h"
38
#include "core/block/block.h"
39
#include "core/block/column_with_type_and_name.h"
40
#include "core/column/column.h"
41
#include "core/column/column_nullable.h"
42
#include "core/column/column_string.h"
43
#include "core/column/column_vector.h"
44
#include "core/data_type/data_type_factory.hpp"
45
#include "core/data_type/data_type_nullable.h"
46
#include "core/data_type/data_type_struct.h"
47
#include "core/data_type/define_primitive_type.h"
48
#include "core/data_type/primitive_type.h"
49
#include "core/string_ref.h"
50
#include "core/typeid_cast.h"
51
#include "exprs/aggregate/aggregate_function.h"
52
#include "format/format_common.h"
53
#include "format/generic_reader.h"
54
#include "format/orc/vorc_reader.h"
55
#include "format/parquet/schema_desc.h"
56
#include "format/parquet/vparquet_column_chunk_reader.h"
57
#include "format/table/deletion_vector_reader.h"
58
#include "format/table/iceberg/iceberg_orc_nested_column_utils.h"
59
#include "format/table/iceberg/iceberg_parquet_nested_column_utils.h"
60
#include "format/table/iceberg_scan_semantics.h"
61
#include "format/table/nested_column_access_helper.h"
62
#include "format/table/table_schema_change_helper.h"
63
#include "runtime/runtime_state.h"
64
#include "util/coding.h"
65
#include "util/string_util.h"
66
67
namespace cctz {
68
class time_zone;
69
} // namespace cctz
70
namespace doris {
71
class RowDescriptor;
72
class SlotDescriptor;
73
class TupleDescriptor;
74
75
namespace io {
76
struct IOContext;
77
} // namespace io
78
class VExprContext;
79
} // namespace doris
80
81
namespace doris {
82
namespace {
83
84
constexpr auto kIcebergOrcAttribute = "iceberg.id";
85
86
58
bool orc_subtree_has_iceberg_id(const orc::Type* type, const std::string& attribute) {
87
58
    if (type->hasAttributeKey(attribute)) {
88
17
        return true;
89
17
    }
90
59
    for (uint64_t idx = 0; idx < type->getSubtypeCount(); ++idx) {
91
35
        if (orc_subtree_has_iceberg_id(type->getSubtype(idx), attribute)) {
92
17
            return true;
93
17
        }
94
35
    }
95
24
    return false;
96
41
}
97
98
92
bool parquet_subtree_has_iceberg_id(const FieldSchema& field) {
99
92
    if (field.field_id >= 0) {
100
69
        return true;
101
69
    }
102
23
    return std::ranges::any_of(field.children, parquet_subtree_has_iceberg_id);
103
92
}
104
105
struct ParquetEqualityFieldPath {
106
    std::vector<const FieldSchema*> fields;
107
    std::vector<size_t> child_indexes;
108
};
109
110
bool find_parquet_equality_field_path_by_id(const FieldDescriptor* descriptor, int32_t field_id,
111
27
                                            ParquetEqualityFieldPath* result) {
112
27
    DORIS_CHECK(descriptor != nullptr);
113
27
    DORIS_CHECK(result != nullptr);
114
27
    const auto find = [field_id](const auto& self, const FieldSchema* field,
115
86
                                 ParquetEqualityFieldPath* path) -> bool {
116
86
        DORIS_CHECK(field != nullptr);
117
86
        path->fields.push_back(field);
118
86
        if (field->field_id == field_id) {
119
21
            return true;
120
21
        }
121
73
        for (size_t index = 0; index < field->children.size(); ++index) {
122
27
            path->child_indexes.push_back(index);
123
27
            if (self(self, &field->children[index], path)) {
124
19
                return true;
125
19
            }
126
8
            path->child_indexes.pop_back();
127
8
        }
128
46
        path->fields.pop_back();
129
46
        return false;
130
65
    };
131
65
    for (int index = 0; index < descriptor->size(); ++index) {
132
59
        if (find(find, descriptor->get_column(index), result)) {
133
21
            return true;
134
21
        }
135
59
    }
136
6
    return false;
137
27
}
138
139
bool find_parquet_equality_field_prefix_by_id_path(
140
        const FieldDescriptor* descriptor,
141
        const std::vector<const schema::external::TField*>& table_path,
142
6
        ParquetEqualityFieldPath* result) {
143
6
    DORIS_CHECK(descriptor != nullptr);
144
6
    DORIS_CHECK(result != nullptr);
145
6
    DORIS_CHECK(!table_path.empty());
146
6
    const std::vector<FieldSchema>* candidates = nullptr;
147
7
    for (size_t path_index = 0; path_index < table_path.size(); ++path_index) {
148
7
        const auto* table_field = table_path[path_index];
149
7
        DORIS_CHECK(table_field != nullptr);
150
7
        DORIS_CHECK(table_field->__isset.id);
151
7
        const FieldSchema* match = nullptr;
152
7
        size_t match_index = 0;
153
7
        const size_t candidate_count =
154
7
                candidates == nullptr ? cast_set<size_t>(descriptor->size()) : candidates->size();
155
25
        for (size_t candidate_index = 0; candidate_index < candidate_count; ++candidate_index) {
156
18
            const auto* candidate = candidates == nullptr
157
18
                                            ? descriptor->get_column(cast_set<int>(candidate_index))
158
18
                                            : &(*candidates)[candidate_index];
159
18
            if (candidate != nullptr && candidate->field_id == table_field->id) {
160
0
                match = candidate;
161
0
                match_index = candidate_index;
162
0
                break;
163
0
            }
164
18
        }
165
7
        if (match == nullptr) {
166
7
            const auto wrapper =
167
7
                    candidates == nullptr
168
7
                            ? TableSchemaChangeHelper::BuildTableInfoUtil::
169
6
                                      find_unique_idless_parquet_wrapper_index(
170
6
                                              *table_field, descriptor->get_fields_schema())
171
7
                            : TableSchemaChangeHelper::BuildTableInfoUtil::
172
1
                                      find_unique_idless_parquet_wrapper_index(*table_field,
173
1
                                                                               *candidates);
174
7
            if (wrapper.has_value()) {
175
1
                match_index = *wrapper;
176
1
                match = candidates == nullptr ? descriptor->get_column(cast_set<int>(match_index))
177
1
                                              : &(*candidates)[match_index];
178
1
            }
179
7
        }
180
7
        if (match == nullptr) {
181
6
            return false;
182
6
        }
183
1
        if (!result->fields.empty()) {
184
0
            result->child_indexes.push_back(match_index);
185
0
        }
186
1
        result->fields.push_back(match);
187
1
        candidates = &match->children;
188
1
    }
189
0
    return true;
190
6
}
191
192
std::vector<std::string> equality_field_name_candidates(const schema::external::TField& table_field,
193
21
                                                        const std::string* leaf_fallback) {
194
21
    std::vector<std::string> candidates;
195
21
    if (table_field.__isset.name_mapping) {
196
11
        candidates.insert(candidates.end(), table_field.name_mapping.begin(),
197
11
                          table_field.name_mapping.end());
198
11
        if (table_field.__isset.name_mapping_is_authoritative &&
199
11
            table_field.name_mapping_is_authoritative) {
200
10
            return candidates;
201
10
        }
202
11
    }
203
11
    if (table_field.__isset.name) {
204
11
        candidates.push_back(table_field.name);
205
11
    }
206
11
    if (leaf_fallback != nullptr) {
207
11
        candidates.push_back(*leaf_fallback);
208
11
    }
209
11
    return candidates;
210
21
}
211
212
bool find_parquet_equality_field_prefix_by_name_path(
213
        const FieldDescriptor* descriptor,
214
        const std::vector<const schema::external::TField*>& table_path,
215
7
        const std::string& leaf_fallback, ParquetEqualityFieldPath* result) {
216
7
    DORIS_CHECK(descriptor != nullptr);
217
7
    DORIS_CHECK(result != nullptr);
218
7
    DORIS_CHECK(!table_path.empty());
219
7
    const std::vector<FieldSchema>* children = nullptr;
220
17
    for (size_t path_index = 0; path_index < table_path.size(); ++path_index) {
221
11
        const auto* table_field = table_path[path_index];
222
11
        DORIS_CHECK(table_field != nullptr);
223
11
        const auto names = equality_field_name_candidates(
224
11
                *table_field, path_index + 1 == table_path.size() ? &leaf_fallback : nullptr);
225
11
        const FieldSchema* match = nullptr;
226
11
        size_t match_index = 0;
227
11
        const size_t child_count =
228
11
                children == nullptr ? cast_set<size_t>(descriptor->size()) : children->size();
229
15
        for (const auto& name : names) {
230
29
            for (size_t child_index = 0; child_index < child_count; ++child_index) {
231
24
                const auto* child = children == nullptr
232
24
                                            ? descriptor->get_column(cast_set<int>(child_index))
233
24
                                            : &(*children)[child_index];
234
24
                if (child != nullptr && iequal(child->name, name)) {
235
10
                    match = child;
236
10
                    match_index = child_index;
237
10
                    break;
238
10
                }
239
24
            }
240
15
            if (match != nullptr) {
241
10
                break;
242
10
            }
243
15
        }
244
11
        if (match == nullptr) {
245
1
            return false;
246
1
        }
247
10
        if (!result->fields.empty()) {
248
4
            result->child_indexes.push_back(match_index);
249
4
        }
250
10
        result->fields.push_back(match);
251
10
        children = &match->children;
252
10
    }
253
6
    return true;
254
7
}
255
256
struct OrcEqualityFieldPath {
257
    std::vector<const orc::Type*> fields;
258
    std::vector<std::string> names;
259
    std::vector<size_t> child_indexes;
260
};
261
262
bool find_orc_equality_field_path_by_id(const orc::Type* root, int32_t field_id,
263
14
                                        OrcEqualityFieldPath* result) {
264
14
    DORIS_CHECK(root != nullptr);
265
14
    DORIS_CHECK(result != nullptr);
266
14
    const auto find = [field_id](const auto& self, const orc::Type* field,
267
14
                                 const std::string& field_name,
268
38
                                 OrcEqualityFieldPath* path) -> bool {
269
38
        DORIS_CHECK(field != nullptr);
270
38
        path->fields.push_back(field);
271
38
        path->names.push_back(field_name);
272
38
        if (field->hasAttributeKey(kIcebergOrcAttribute) &&
273
38
            std::stoi(field->getAttributeValue(kIcebergOrcAttribute)) == field_id) {
274
7
            return true;
275
7
        }
276
32
        for (size_t index = 0; index < field->getSubtypeCount(); ++index) {
277
6
            path->child_indexes.push_back(index);
278
6
            if (self(self, field->getSubtype(index), field->getFieldName(index), path)) {
279
5
                return true;
280
5
            }
281
1
            path->child_indexes.pop_back();
282
1
        }
283
26
        path->fields.pop_back();
284
26
        path->names.pop_back();
285
26
        return false;
286
31
    };
287
39
    for (size_t index = 0; index < root->getSubtypeCount(); ++index) {
288
32
        if (find(find, root->getSubtype(index), root->getFieldName(index), result)) {
289
7
            return true;
290
7
        }
291
32
    }
292
7
    return false;
293
14
}
294
295
bool find_orc_equality_field_prefix_by_id_path(
296
        const orc::Type* root, const std::vector<const schema::external::TField*>& table_path,
297
6
        OrcEqualityFieldPath* result) {
298
6
    DORIS_CHECK(root != nullptr);
299
6
    DORIS_CHECK(result != nullptr);
300
6
    DORIS_CHECK(!table_path.empty());
301
6
    const orc::Type* parent = root;
302
7
    for (const auto* table_field : table_path) {
303
7
        DORIS_CHECK(table_field != nullptr);
304
7
        DORIS_CHECK(table_field->__isset.id);
305
7
        const orc::Type* match = nullptr;
306
7
        size_t match_index = 0;
307
25
        for (size_t candidate_index = 0; candidate_index < parent->getSubtypeCount();
308
18
             ++candidate_index) {
309
18
            const auto* candidate = parent->getSubtype(candidate_index);
310
18
            if (candidate->hasAttributeKey(kIcebergOrcAttribute) &&
311
18
                std::stoi(candidate->getAttributeValue(kIcebergOrcAttribute)) == table_field->id) {
312
0
                match = candidate;
313
0
                match_index = candidate_index;
314
0
                break;
315
0
            }
316
18
        }
317
7
        if (match == nullptr) {
318
7
            const auto wrapper = TableSchemaChangeHelper::BuildTableInfoUtil::
319
7
                    find_unique_idless_orc_wrapper_index(*table_field, parent,
320
7
                                                         kIcebergOrcAttribute);
321
7
            if (wrapper.has_value()) {
322
1
                match_index = *wrapper;
323
1
                match = parent->getSubtype(match_index);
324
1
            }
325
7
        }
326
7
        if (match == nullptr) {
327
6
            return false;
328
6
        }
329
1
        if (!result->fields.empty()) {
330
0
            result->child_indexes.push_back(match_index);
331
0
        }
332
1
        result->fields.push_back(match);
333
1
        result->names.push_back(parent->getFieldName(match_index));
334
1
        parent = match;
335
1
    }
336
0
    return true;
337
6
}
338
339
bool find_orc_equality_field_prefix_by_name_path(
340
        const orc::Type* root, const std::vector<const schema::external::TField*>& table_path,
341
6
        const std::string& leaf_fallback, OrcEqualityFieldPath* result) {
342
6
    DORIS_CHECK(root != nullptr);
343
6
    DORIS_CHECK(result != nullptr);
344
6
    DORIS_CHECK(!table_path.empty());
345
6
    const orc::Type* parent = root;
346
15
    for (size_t path_index = 0; path_index < table_path.size(); ++path_index) {
347
10
        const auto* table_field = table_path[path_index];
348
10
        DORIS_CHECK(table_field != nullptr);
349
10
        const auto names = equality_field_name_candidates(
350
10
                *table_field, path_index + 1 == table_path.size() ? &leaf_fallback : nullptr);
351
10
        const orc::Type* match = nullptr;
352
10
        size_t match_index = 0;
353
14
        for (const auto& name : names) {
354
26
            for (size_t child_index = 0; child_index < parent->getSubtypeCount(); ++child_index) {
355
21
                if (iequal(parent->getFieldName(child_index), name)) {
356
9
                    match = parent->getSubtype(child_index);
357
9
                    match_index = child_index;
358
9
                    break;
359
9
                }
360
21
            }
361
14
            if (match != nullptr) {
362
9
                break;
363
9
            }
364
14
        }
365
10
        if (match == nullptr) {
366
1
            return false;
367
1
        }
368
9
        if (!result->fields.empty()) {
369
4
            result->child_indexes.push_back(match_index);
370
4
        }
371
9
        result->fields.push_back(match);
372
9
        result->names.push_back(parent->getFieldName(match_index));
373
9
        parent = match;
374
9
    }
375
5
    return true;
376
6
}
377
378
struct ProjectedIcebergStructChild {
379
    size_t index;
380
    const schema::external::TField* field;
381
};
382
383
std::optional<ProjectedIcebergStructChild> find_projected_iceberg_struct_child(
384
        const schema::external::TField& parent, const schema::external::TField& target,
385
11
        const DataTypeStruct& projected_type) {
386
11
    DORIS_CHECK(parent.__isset.nestedField);
387
11
    DORIS_CHECK(parent.nestedField.__isset.struct_field);
388
11
    DORIS_CHECK(parent.nestedField.struct_field.__isset.fields);
389
11
    DORIS_CHECK(target.__isset.name);
390
11
    DORIS_CHECK(target.__isset.id);
391
392
    // FE keeps current children first and appends historical equality fields to the schema carrier
393
    // by field ID. A dropped and re-added field may therefore appear twice with the same name,
394
    // while the query's DataTypeStruct contains only the current occurrence.
395
11
    size_t schema_name_ordinal = 0;
396
11
    const schema::external::TField* schema_child = nullptr;
397
16
    for (const auto& child_ptr : parent.nestedField.struct_field.fields) {
398
16
        DORIS_CHECK(child_ptr.__isset.field_ptr && child_ptr.field_ptr != nullptr);
399
16
        const auto& child = child_ptr.field_ptr;
400
16
        DORIS_CHECK(child->__isset.name);
401
16
        DORIS_CHECK(child->__isset.id);
402
16
        if (child->name != target.name) {
403
5
            continue;
404
5
        }
405
11
        if (child->id == target.id) {
406
11
            schema_child = child.get();
407
11
            break;
408
11
        }
409
0
        ++schema_name_ordinal;
410
0
    }
411
11
    if (schema_child == nullptr) {
412
0
        return std::nullopt;
413
0
    }
414
415
11
    size_t projected_name_ordinal = 0;
416
16
    for (size_t index = 0; index < projected_type.get_elements().size(); ++index) {
417
15
        if (projected_type.get_element_name(index) != target.name) {
418
5
            continue;
419
5
        }
420
10
        if (projected_name_ordinal == schema_name_ordinal) {
421
10
            return ProjectedIcebergStructChild {.index = index, .field = schema_child};
422
10
        }
423
0
        ++projected_name_ordinal;
424
0
    }
425
1
    return std::nullopt;
426
11
}
427
428
// Resolves a nested equality key inside the projected root column. That column has the table's
429
// projected struct type, not the file's physical struct, so the child indexes come from the
430
// projected type. `found` stays false when the projected type does not hold the key, e.g. when the
431
// key was dropped from the table or the struct was pruned.
432
Status find_projected_equality_key_path(
433
        const schema::external::TField& projected_root,
434
        const std::vector<const schema::external::TField*>& key_path, const DataTypePtr& root_type,
435
11
        std::vector<size_t>* child_indexes, DataTypePtr* leaf_type, bool* found) {
436
11
    DORIS_CHECK(!key_path.empty());
437
11
    DORIS_CHECK(child_indexes != nullptr);
438
11
    DORIS_CHECK(leaf_type != nullptr);
439
11
    DORIS_CHECK(found != nullptr);
440
11
    child_indexes->clear();
441
11
    *found = false;
442
11
    const auto* projected_parent = &projected_root;
443
11
    DataTypePtr current_type = root_type;
444
21
    for (size_t path_index = 1; path_index < key_path.size(); ++path_index) {
445
11
        const auto* child = key_path[path_index];
446
11
        DORIS_CHECK(child != nullptr);
447
11
        if (!projected_parent->__isset.nestedField ||
448
11
            !projected_parent->nestedField.__isset.struct_field ||
449
11
            !projected_parent->nestedField.struct_field.__isset.fields) {
450
0
            return Status::NotSupported(
451
0
                    "Iceberg equality-delete field {} has a non-struct projected-schema parent",
452
0
                    child->name);
453
0
        }
454
11
        const auto* struct_type =
455
11
                typeid_cast<const DataTypeStruct*>(remove_nullable(current_type).get());
456
11
        if (struct_type == nullptr) {
457
0
            return Status::OK();
458
0
        }
459
11
        const auto projected_child =
460
11
                find_projected_iceberg_struct_child(*projected_parent, *child, *struct_type);
461
11
        if (!projected_child.has_value()) {
462
1
            return Status::OK();
463
1
        }
464
10
        child_indexes->push_back(projected_child->index);
465
10
        current_type = struct_type->get_element(projected_child->index);
466
10
        projected_parent = projected_child->field;
467
10
    }
468
10
    *leaf_type = current_type;
469
10
    *found = true;
470
10
    return Status::OK();
471
11
}
472
473
// A projected column is read through its own schema mapping, as the struct reader resolves each
474
// child (`children_file_column_name`), while a hidden key is located in the file by field ID or by
475
// its name path. The two can bind different file fields: a plan without the Iceberg scan semantics
476
// marker maps a struct level by name when not all of its file children carry field IDs. The key
477
// can therefore come from the projected column only when that mapping reads the same file field at
478
// every level of the key's physical path.
479
bool projected_root_reads_physical_path(const TableSchemaChangeHelper::Node& root_node,
480
                                        const std::string& root_name, const DataTypePtr& root_type,
481
                                        const std::vector<size_t>& child_indexes,
482
10
                                        const ParquetEqualityFieldPath& file_path) {
483
10
    if (child_indexes.size() + 1 != file_path.fields.size() ||
484
10
        !root_node.children_column_exists(root_name) ||
485
10
        root_node.children_file_column_name(root_name) != file_path.fields.front()->name) {
486
0
        return false;
487
0
    }
488
10
    auto node = root_node.get_children_node(root_name);
489
10
    DataTypePtr type = root_type;
490
19
    for (size_t level = 0; level < child_indexes.size(); ++level) {
491
10
        const auto& struct_type = assert_cast<const DataTypeStruct&>(*remove_nullable(type));
492
10
        const auto& child_name = struct_type.get_element_name(child_indexes[level]);
493
10
        if (!node->children_column_exists(child_name) ||
494
10
            node->children_file_column_name(child_name) != file_path.fields[level + 1]->name) {
495
1
            return false;
496
1
        }
497
9
        node = node->get_children_node(child_name);
498
9
        type = struct_type.get_element(child_indexes[level]);
499
9
    }
500
9
    return true;
501
10
}
502
503
} // namespace
504
505
const std::string IcebergOrcReader::ICEBERG_ORC_ATTRIBUTE = kIcebergOrcAttribute;
506
507
bool IcebergTableReader::_is_fully_dictionary_encoded(
508
8
        const tparquet::ColumnMetaData& column_metadata) {
509
14
    const auto is_dictionary_encoding = [](tparquet::Encoding::type encoding) {
510
14
        return encoding == tparquet::Encoding::PLAIN_DICTIONARY ||
511
14
               encoding == tparquet::Encoding::RLE_DICTIONARY;
512
14
    };
513
12
    const auto is_data_page = [](tparquet::PageType::type page_type) {
514
12
        return page_type == tparquet::PageType::DATA_PAGE ||
515
12
               page_type == tparquet::PageType::DATA_PAGE_V2;
516
12
    };
517
8
    const auto is_level_encoding = [](tparquet::Encoding::type encoding) {
518
2
        return encoding == tparquet::Encoding::RLE || encoding == tparquet::Encoding::BIT_PACKED;
519
2
    };
520
521
    // A column chunk may have a dictionary page but still contain plain-encoded data pages.
522
    // Only treat it as dictionary-coded when all data pages are dictionary encoded.
523
8
    if (column_metadata.__isset.encoding_stats) {
524
7
        bool has_data_page_stats = false;
525
12
        for (const tparquet::PageEncodingStats& enc_stat : column_metadata.encoding_stats) {
526
12
            if (is_data_page(enc_stat.page_type) && enc_stat.count > 0) {
527
8
                has_data_page_stats = true;
528
8
                if (!is_dictionary_encoding(enc_stat.encoding)) {
529
2
                    return false;
530
2
                }
531
8
            }
532
12
        }
533
5
        if (has_data_page_stats) {
534
4
            return true;
535
4
        }
536
5
    }
537
538
2
    bool has_dict_encoding = false;
539
2
    bool has_nondict_encoding = false;
540
3
    for (const tparquet::Encoding::type& encoding : column_metadata.encodings) {
541
3
        if (is_dictionary_encoding(encoding)) {
542
1
            has_dict_encoding = true;
543
1
        }
544
545
3
        if (!is_dictionary_encoding(encoding) && !is_level_encoding(encoding)) {
546
2
            has_nondict_encoding = true;
547
2
            break;
548
2
        }
549
3
    }
550
2
    if (!has_dict_encoding || has_nondict_encoding) {
551
2
        return false;
552
2
    }
553
554
0
    return true;
555
2
}
556
557
// ============================================================================
558
// IcebergParquetReader: on_before_init_reader (Parquet-specific schema matching)
559
// ============================================================================
560
// This format-specific setup mirrors the existing reader initialization sequence.
561
// NOLINTNEXTLINE(readability-function-cognitive-complexity,readability-function-size)
562
37
Status IcebergParquetReader::on_before_init_reader(ReaderInitContext* ctx) {
563
37
    _column_descs = ctx->column_descs;
564
37
    _fill_col_name_to_block_idx = ctx->col_name_to_block_idx;
565
37
    _file_format = Fileformat::PARQUET;
566
37
    set_preserve_binary_uuid(true);
567
568
    // Get file metadata schema first (available because _open_file() already ran)
569
37
    const FieldDescriptor* field_desc = nullptr;
570
37
    RETURN_IF_ERROR(this->get_file_metadata_schema(&field_desc));
571
37
    DCHECK(field_desc != nullptr);
572
573
    // Build table_info_node by field_id or name matching.
574
    // This must happen BEFORE column classification so we can use children_column_exists
575
    // to check if a column exists in the file (by field ID, not name).
576
37
    if (!get_scan_params().__isset.history_schema_info ||
577
37
        get_scan_params().history_schema_info.empty()) [[unlikely]] {
578
1
        RETURN_IF_ERROR(BuildTableInfoUtil::by_parquet_name(ctx->tuple_descriptor, *field_desc,
579
1
                                                            ctx->table_info_node));
580
36
    } else {
581
36
        RETURN_IF_ERROR(BuildTableInfoUtil::by_parquet_field_id_with_name_mapping(
582
36
                get_scan_params().history_schema_info.front().root_field, *field_desc,
583
36
                ctx->table_info_node, supports_iceberg_scan_semantics_v1(&get_scan_params())));
584
36
    }
585
586
37
    std::unordered_set<std::string> partition_col_names;
587
37
    if (ctx->range->__isset.columns_from_path_keys) {
588
0
        partition_col_names.insert(ctx->range->columns_from_path_keys.begin(),
589
0
                                   ctx->range->columns_from_path_keys.end());
590
0
    }
591
592
    // Single pass: classify columns, detect $row_id, handle partition fallback.
593
37
    bool has_partition_from_path = false;
594
52
    for (const auto& desc : *ctx->column_descs) {
595
52
        if (desc.category == ColumnCategory::SYNTHESIZED) {
596
0
            if (desc.name == BeConsts::ICEBERG_ROWID_COL) {
597
0
                this->register_synthesized_column_handler(
598
0
                        BeConsts::ICEBERG_ROWID_COL, [this](Block* block, size_t rows) -> Status {
599
0
                            return _fill_iceberg_row_id(block, rows);
600
0
                        });
601
0
                continue;
602
0
            } else if (desc.name.starts_with(BeConsts::GLOBAL_ROWID_COL)) {
603
0
                auto topn_row_id_column_iter = _create_topn_row_id_column_iterator();
604
0
                this->register_synthesized_column_handler(
605
0
                        desc.name,
606
0
                        [iter = std::move(topn_row_id_column_iter), this, &desc](
607
0
                                Block* block, size_t rows) -> Status {
608
0
                            return fill_topn_row_id(iter, desc.name, block, rows);
609
0
                        });
610
0
                continue;
611
0
            }
612
52
        } else if (desc.category == ColumnCategory::PARTITION_KEY) {
613
0
            bool has_partition_value = partition_col_names.contains(desc.name);
614
0
            bool exists_in_file = ctx->table_info_node->children_column_exists(desc.name);
615
0
            if (!has_partition_value || exists_in_file) {
616
                // Keep PARTITION_KEY category stable for scan planning, but still read
617
                // from file when the column exists there.
618
0
                ctx->column_names.push_back(desc.name);
619
0
                continue;
620
0
            }
621
0
            has_partition_from_path = true;
622
52
        } else if (desc.category == ColumnCategory::REGULAR) {
623
52
            ctx->column_names.push_back(desc.name);
624
52
        } else if (desc.category == ColumnCategory::GENERATED) {
625
0
            _init_row_lineage_columns();
626
0
            if (desc.name == ROW_LINEAGE_ROW_ID) {
627
0
                ctx->column_names.push_back(desc.name);
628
0
                this->register_generated_column_handler(
629
0
                        ROW_LINEAGE_ROW_ID, [this](Block* block, size_t rows) -> Status {
630
0
                            return _fill_row_lineage_row_id(block, rows);
631
0
                        });
632
0
                continue;
633
0
            } else if (desc.name == ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER) {
634
0
                ctx->column_names.push_back(desc.name);
635
0
                this->register_generated_column_handler(
636
0
                        ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER,
637
0
                        [this](Block* block, size_t rows) -> Status {
638
0
                            return _fill_row_lineage_last_updated_sequence_number(block, rows);
639
0
                        });
640
0
                continue;
641
0
            }
642
0
        }
643
52
    }
644
645
    // Set up partition value extraction if any partition columns need filling from path
646
37
    if (has_partition_from_path) {
647
0
        RETURN_IF_ERROR(_extract_partition_values(*ctx->range, ctx->tuple_descriptor,
648
0
                                                  _fill_partition_values,
649
0
                                                  &_fill_partition_value_is_null));
650
0
    }
651
652
37
    _all_required_col_names = ctx->column_names;
653
654
    // Create column IDs from field descriptor
655
37
    auto column_id_result =
656
37
            _create_column_ids(field_desc, ctx->tuple_descriptor, ctx->table_info_node);
657
37
    ctx->column_ids = std::move(column_id_result.column_ids);
658
37
    ctx->filter_column_ids = std::move(column_id_result.filter_column_ids);
659
660
    // Build field_id -> block_column_name mapping for equality delete filtering.
661
    // This was previously done in init_reader() column matching (pre-CRTP refactoring).
662
52
    for (const auto* slot : ctx->tuple_descriptor->slots()) {
663
52
        _id_to_block_column_name.emplace(slot->col_unique_id(), slot->col_name());
664
52
    }
665
666
    // Process delete files (must happen before _do_init_reader so expand col IDs are included)
667
37
    RETURN_IF_ERROR(_init_row_filters());
668
669
    // Add expand column IDs for equality delete and remap expand column names
670
    // to match master's behavior:
671
    // - Use field_id to find the actual file column name in Parquet schema
672
    // - Prefix with __equality_delete_column__ to avoid name conflicts
673
    // - Correctly map table_col_name → file_col_name in table_info_node
674
37
    const static std::string EQ_DELETE_PRE = "__equality_delete_column__";
675
37
    bool all_file_columns_have_field_ids = true;
676
37
    bool any_file_column_has_field_id = false;
677
122
    for (int i = 0; i < field_desc->size(); ++i) {
678
85
        const auto* field_schema = field_desc->get_column(i);
679
85
        if (field_schema) {
680
85
            if (field_schema->field_id < 0) {
681
19
                all_file_columns_have_field_ids = false;
682
19
            }
683
85
            if (parquet_subtree_has_iceberg_id(*field_schema)) {
684
69
                any_file_column_has_field_id = true;
685
69
            }
686
85
        }
687
85
    }
688
37
    const bool use_field_ids_for_hidden_keys =
689
37
            supports_iceberg_scan_semantics_v1(&get_scan_params())
690
37
                    ? any_file_column_has_field_id
691
37
                    : all_file_columns_have_field_ids;
692
37
    const auto find_file_column_by_name = [&](const std::string& name) -> const FieldSchema* {
693
0
        for (int j = 0; j < field_desc->size(); ++j) {
694
0
            const auto* candidate = field_desc->get_column(j);
695
0
            if (candidate != nullptr && iequal(candidate->name, name)) {
696
0
                return candidate;
697
0
            }
698
0
        }
699
0
        return nullptr;
700
0
    };
701
37
    std::unordered_map<int32_t, const SlotDescriptor*> projected_slots;
702
52
    for (const auto* slot : ctx->tuple_descriptor->slots()) {
703
52
        projected_slots.emplace(slot->col_unique_id(), slot);
704
52
    }
705
    // Finds a nested key inside its root struct when the query projects that root, so the key is
706
    // taken from the projected column instead of reading the root a second time.
707
37
    const auto find_projected_root = [&](int32_t field_id, const DataTypePtr& key_type,
708
37
                                         const ParquetEqualityFieldPath& file_path,
709
37
                                         std::string* root_name,
710
37
                                         std::vector<size_t>* child_indexes) -> Status {
711
23
        const auto key_path = _find_schema_field_path(field_id);
712
23
        if (key_path.empty()) {
713
0
            return Status::OK();
714
0
        }
715
23
        DORIS_CHECK(key_path.front()->__isset.id);
716
23
        const auto slot = projected_slots.find(key_path.front()->id);
717
23
        if (slot == projected_slots.end()) {
718
12
            return Status::OK();
719
12
        }
720
11
        std::vector<const schema::external::TField*> root_path;
721
11
        if (!_find_schema_field_path_in_root(_current_schema_root(), key_path.front()->id,
722
11
                                             &root_path)) {
723
0
            return Status::OK();
724
0
        }
725
11
        DORIS_CHECK(root_path.size() == 1);
726
11
        DataTypePtr leaf_type;
727
11
        bool found = false;
728
11
        RETURN_IF_ERROR(find_projected_equality_key_path(*root_path.front(), key_path,
729
11
                                                         slot->second->type(), child_indexes,
730
11
                                                         &leaf_type, &found));
731
        // The projected leaf has the table's current type, which is the type a key is compared in
732
        // (equality_delete_key_type) even when the key was written before a type promotion. Only a
733
        // STRING / VARBINARY key keeps its delete file's type: a STRING key under a VARBINARY
734
        // projected leaf still reads the physical root, where the leaf is read as STRING too.
735
11
        if (found && remove_nullable(leaf_type)->equals(*remove_nullable(key_type)) &&
736
11
            projected_root_reads_physical_path(*ctx->table_info_node, slot->second->col_name(),
737
10
                                               slot->second->type(), *child_indexes, file_path)) {
738
9
            *root_name = slot->second->col_name();
739
9
        }
740
11
        return Status::OK();
741
11
    };
742
    // Physical root struct -> the carrier that reads it for every hidden key under that root.
743
37
    std::unordered_map<std::string, std::string> physical_root_carriers;
744
745
    // Rebuild _expand_col_names with proper file-column-based names
746
37
    std::vector<std::string> new_expand_col_names;
747
37
    DORIS_CHECK(_expand_col_names.size() == _expand_col_field_ids.size());
748
37
    DORIS_CHECK(_expand_col_names.size() == _expand_columns.size());
749
71
    for (size_t i = 0; i < _expand_col_names.size(); ++i) {
750
34
        const auto& old_name = _expand_col_names[i];
751
34
        const int32_t field_id = _expand_col_field_ids[i];
752
753
34
        const FieldSchema* file_column = nullptr;
754
34
        ParquetEqualityFieldPath file_path;
755
34
        bool complete_file_path = false;
756
34
        if (use_field_ids_for_hidden_keys) {
757
27
            complete_file_path =
758
27
                    find_parquet_equality_field_path_by_id(field_desc, field_id, &file_path);
759
27
            if (!complete_file_path && supports_iceberg_scan_semantics_v2(&get_scan_params())) {
760
6
                const auto table_path = _find_schema_field_path(field_id);
761
6
                if (!table_path.empty()) {
762
6
                    complete_file_path = find_parquet_equality_field_prefix_by_id_path(
763
6
                            field_desc, table_path, &file_path);
764
6
                }
765
6
            }
766
27
            if (!file_path.fields.empty()) {
767
22
                file_column = file_path.fields.front();
768
22
            }
769
27
        } else {
770
7
            const auto table_path = _find_schema_field_path(field_id);
771
7
            if (!table_path.empty()) {
772
7
                complete_file_path = find_parquet_equality_field_prefix_by_name_path(
773
7
                        field_desc, table_path, old_name, &file_path);
774
7
                if (!file_path.fields.empty()) {
775
6
                    file_column = file_path.fields.front();
776
6
                }
777
7
            } else {
778
0
                file_column = find_file_column_by_name(old_name);
779
0
                complete_file_path = file_column != nullptr;
780
0
            }
781
7
        }
782
783
34
        std::string leaf_name = old_name;
784
34
        if (!file_path.fields.empty()) {
785
28
            leaf_name = file_path.fields.back()->name;
786
28
        } else if (file_column != nullptr) {
787
0
            leaf_name = file_column->name;
788
0
        }
789
34
        const std::string file_col_name = file_column == nullptr ? old_name : file_column->name;
790
34
        std::string table_col_name = EQ_DELETE_PRE + std::to_string(field_id) + "_" + leaf_name;
791
792
        // Update _id_to_block_column_name
793
34
        if (field_id >= 0) {
794
34
            _id_to_block_column_name[field_id] = table_col_name;
795
34
        }
796
797
        // Update _expand_columns name
798
34
        _expand_columns[i].name = table_col_name;
799
800
34
        if (file_column == nullptr) {
801
6
            RETURN_IF_ERROR(_register_missing_equality_delete_column(field_id, table_col_name,
802
6
                                                                     _expand_columns[i].type));
803
            // The old data file predates this equality key. Its logical initial default is appended
804
            // after the read; do not advertise it to Parquet as a physical child.
805
6
            new_expand_col_names.push_back(table_col_name);
806
6
            continue;
807
6
        }
808
809
28
        new_expand_col_names.push_back(table_col_name);
810
811
        // A hidden nested key is read through its containing top-level struct. V1 column IDs are
812
        // pre-order ranges, so include the complete subtree before extracting the primitive leaf.
813
        // A projected root that serves as the key's source then reads the key's leaf as well.
814
28
        for (uint64_t column_id = file_column->get_column_id();
815
91
             column_id <= file_column->get_max_column_id(); ++column_id) {
816
63
            ctx->column_ids.insert(column_id);
817
63
        }
818
819
28
        if (!complete_file_path || !file_path.child_indexes.empty()) {
820
            // Parquet reads each table column through its own column reader. Without the two
821
            // branches below, a projected root and a carrier, or two carriers of keys under one
822
            // root, all named the same file column, and only one of them was read. Example:
823
            // `SELECT id, payload` with an equality delete on payload.existing returned an empty
824
            // payload, or no rows at all when payload was listed first.
825
24
            std::string projected_root_name;
826
24
            std::vector<size_t> projected_child_indexes;
827
24
            if (complete_file_path) {
828
23
                RETURN_IF_ERROR(find_projected_root(field_id, _expand_columns[i].type, file_path,
829
23
                                                    &projected_root_name,
830
23
                                                    &projected_child_indexes));
831
23
            }
832
24
            if (!projected_root_name.empty()) {
833
                // The query projects payload and the file has payload.existing: extract the key
834
                // from the projected payload column.
835
9
                _nested_equality_delete_columns.push_back({
836
9
                        .field_id = field_id,
837
9
                        .block_name = table_col_name,
838
9
                        .leaf_type = _expand_columns[i].type,
839
9
                        .source_leaf_type = _expand_columns[i].type,
840
9
                        .child_indexes = std::move(projected_child_indexes),
841
9
                        .missing_value = nullptr,
842
9
                        .source_block_name = std::move(projected_root_name),
843
9
                });
844
9
                continue;
845
9
            }
846
15
            ColumnPtr missing_value;
847
15
            if (!complete_file_path) {
848
1
                RETURN_IF_ERROR(_create_missing_equality_delete_value(
849
1
                        field_id, _expand_columns[i].type, file_path.fields.size(),
850
1
                        &missing_value));
851
1
            }
852
            // The physical root holds the leaf in the data file's own type, which is narrower than
853
            // the compared type when the file was written before a type promotion: payload.existing
854
            // promoted from INT to BIGINT is INT in older files, and copying those INT values into a
855
            // BIGINT column read the wrong bytes. A leaf the file lacks is the missing default,
856
            // which is built in the compared type.
857
15
            const DataTypePtr source_leaf_type =
858
15
                    complete_file_path ? make_nullable(file_path.fields.back()->data_type)
859
15
                                       : _expand_columns[i].type;
860
15
            const auto root_carrier = physical_root_carriers.find(file_column->name);
861
15
            if (root_carrier != physical_root_carriers.end()) {
862
                // Keys payload.a and payload.b: the carrier of payload.a reads payload, and
863
                // payload.b is extracted from that carrier's column.
864
2
                _nested_equality_delete_columns.push_back({
865
2
                        .field_id = field_id,
866
2
                        .block_name = table_col_name,
867
2
                        .leaf_type = _expand_columns[i].type,
868
2
                        .source_leaf_type = source_leaf_type,
869
2
                        .child_indexes = file_path.child_indexes,
870
2
                        .missing_value = std::move(missing_value),
871
2
                        .source_block_name = root_carrier->second,
872
2
                });
873
2
                continue;
874
2
            }
875
13
            physical_root_carriers.emplace(file_column->name, table_col_name);
876
13
            _nested_equality_delete_columns.push_back({
877
13
                    .field_id = field_id,
878
13
                    .block_name = table_col_name,
879
13
                    .leaf_type = _expand_columns[i].type,
880
13
                    .source_leaf_type = source_leaf_type,
881
13
                    .child_indexes = file_path.child_indexes,
882
13
                    .missing_value = std::move(missing_value),
883
13
            });
884
13
            _expand_columns[i].type = make_nullable(file_column->data_type);
885
13
            _expand_columns[i].column = _expand_columns[i].type->create_column();
886
13
        }
887
888
        // Register in table_info_node: table_col_name → file_col_name
889
17
        ctx->column_names.push_back(table_col_name);
890
17
        ctx->table_info_node->add_children(table_col_name, file_col_name,
891
17
                                           TableSchemaChangeHelper::ConstNode::get_instance());
892
17
    }
893
37
    _expand_col_names = std::move(new_expand_col_names);
894
895
    // Enable group filtering for Iceberg
896
37
    _filter_groups = true;
897
898
37
    return Status::OK();
899
37
}
900
901
// ============================================================================
902
// IcebergParquetReader: _create_column_ids
903
// ============================================================================
904
ColumnIdResult IcebergParquetReader::_create_column_ids(
905
        const FieldDescriptor* field_desc, const TupleDescriptor* tuple_descriptor,
906
44
        const std::shared_ptr<TableSchemaChangeHelper::Node>& table_info_node) {
907
44
    auto* mutable_field_desc = const_cast<FieldDescriptor*>(field_desc);
908
44
    mutable_field_desc->assign_ids();
909
910
44
    std::unordered_map<int, const FieldSchema*> iceberg_id_to_field_schema_map;
911
179
    for (int i = 0; i < field_desc->size(); ++i) {
912
135
        const auto* field_schema = field_desc->get_column(i);
913
135
        if (!field_schema) {
914
0
            continue;
915
0
        }
916
135
        int iceberg_id = field_schema->field_id;
917
135
        iceberg_id_to_field_schema_map[iceberg_id] = field_schema;
918
135
    }
919
920
44
    std::set<uint64_t> column_ids;
921
44
    std::set<uint64_t> filter_column_ids;
922
923
44
    auto process_access_paths = [](const FieldSchema* parquet_field,
924
44
                                   const std::vector<TColumnAccessPath>& access_paths,
925
44
                                   std::set<uint64_t>& out_ids) {
926
26
        process_nested_access_paths(
927
26
                parquet_field, access_paths, out_ids,
928
26
                [](const FieldSchema* field) { return field->get_column_id(); },
929
26
                [](const FieldSchema* field) { return field->get_max_column_id(); },
930
26
                IcebergParquetNestedColumnUtils::extract_nested_column_ids);
931
26
    };
932
933
    // The Iceberg schema-mapping root is a StructNode whose registered children are the real
934
    // table columns. When present, resolve each column by name through it so the column-id set
935
    // stays consistent with the schema-mapping decision (BY_ID or BY_NAME/name-mapping);
936
    // otherwise fall back to matching by Iceberg field id.
937
44
    const auto* struct_node =
938
44
            dynamic_cast<const TableSchemaChangeHelper::StructNode*>(table_info_node.get());
939
940
67
    for (const auto* slot : tuple_descriptor->slots()) {
941
67
        const FieldSchema* field_schema = nullptr;
942
67
        if (struct_node != nullptr) {
943
            // Synthesized/metadata slots (e.g. the TopN global row-id or the $row_id column) are
944
            // never registered as children, so check membership before querying: calling
945
            // children_column_exists() on an unregistered name DCHECK-aborts in debug builds and
946
            // throws std::out_of_range from .at() in release builds.
947
54
            if (struct_node->get_children().contains(slot->col_name()) &&
948
54
                struct_node->children_column_exists(slot->col_name())) {
949
                // Use the physical child selected by the schema-mapping pass. This keeps partial-id
950
                // files in BY_NAME mode from binding a projected column through an unrelated stale
951
                // field id.
952
51
                const auto& file_column_name =
953
51
                        struct_node->children_file_column_name(slot->col_name());
954
71
                for (int i = 0; i < field_desc->size(); ++i) {
955
71
                    const auto* candidate = field_desc->get_column(i);
956
71
                    if (candidate != nullptr && candidate->name == file_column_name) {
957
51
                        field_schema = candidate;
958
51
                        break;
959
51
                    }
960
71
                }
961
51
                DORIS_CHECK(field_schema != nullptr);
962
51
            }
963
54
        } else {
964
13
            auto it = iceberg_id_to_field_schema_map.find(slot->col_unique_id());
965
13
            if (it != iceberg_id_to_field_schema_map.end()) {
966
13
                field_schema = it->second;
967
13
            }
968
13
        }
969
67
        if (field_schema == nullptr) {
970
3
            continue;
971
3
        }
972
973
64
        if ((slot->col_type() != TYPE_STRUCT && slot->col_type() != TYPE_ARRAY &&
974
64
             slot->col_type() != TYPE_MAP)) {
975
44
            column_ids.insert(field_schema->column_id);
976
44
            if (slot->is_predicate()) {
977
0
                filter_column_ids.insert(field_schema->column_id);
978
0
            }
979
44
            continue;
980
44
        }
981
982
20
        const auto& all_access_paths = slot->all_access_paths();
983
20
        process_access_paths(field_schema, all_access_paths, column_ids);
984
985
20
        const auto& predicate_access_paths = slot->predicate_access_paths();
986
20
        if (!predicate_access_paths.empty()) {
987
6
            process_access_paths(field_schema, predicate_access_paths, filter_column_ids);
988
6
        }
989
20
    }
990
44
    return {std::move(column_ids), std::move(filter_column_ids)};
991
44
}
992
993
// ============================================================================
994
// IcebergParquetReader: _read_position_delete_file
995
// ============================================================================
996
Status IcebergParquetReader::_read_position_delete_file(const TFileRangeDesc* delete_range,
997
2
                                                        DeleteFile* position_delete) {
998
2
    ParquetReader parquet_delete_reader(get_profile(), get_scan_params(), *delete_range,
999
2
                                        READ_DELETE_FILE_BATCH_SIZE, &get_state()->timezone_obj(),
1000
2
                                        get_io_ctx(), get_state(), _meta_cache);
1001
    // The delete file range has size=-1 (read whole file). We must disable
1002
    // row group filtering before init; otherwise _do_init_reader returns EndOfFile
1003
    // when _filter_groups && _range_size < 0.
1004
2
    ParquetInitContext delete_ctx;
1005
2
    delete_ctx.filter_groups = false;
1006
2
    delete_ctx.column_names = delete_file_col_names;
1007
2
    delete_ctx.col_name_to_block_idx =
1008
2
            const_cast<std::unordered_map<std::string, uint32_t>*>(&DELETE_COL_NAME_TO_BLOCK_IDX);
1009
2
    RETURN_IF_ERROR(parquet_delete_reader.init_reader(&delete_ctx));
1010
1011
0
    const tparquet::FileMetaData* meta_data = parquet_delete_reader.get_meta_data();
1012
0
    bool dictionary_coded = true;
1013
0
    for (const auto& row_group : meta_data->row_groups) {
1014
0
        const auto& column_chunk = row_group.columns[ICEBERG_FILE_PATH_INDEX];
1015
0
        if (!(column_chunk.__isset.meta_data && has_dict_page(column_chunk.meta_data))) {
1016
0
            dictionary_coded = false;
1017
0
            break;
1018
0
        }
1019
0
    }
1020
0
    DataTypePtr data_type_file_path = make_nullable(std::make_shared<DataTypeString>());
1021
0
    DataTypePtr data_type_pos = make_nullable(std::make_shared<DataTypeInt64>());
1022
0
    bool eof = false;
1023
0
    while (!eof) {
1024
0
        Block block = {
1025
0
                dictionary_coded
1026
0
                        ? ColumnWithTypeAndName {ColumnNullable::create(ColumnDictI32::create(),
1027
0
                                                                        ColumnUInt8::create()),
1028
0
                                                 data_type_file_path, ICEBERG_FILE_PATH}
1029
0
                        : ColumnWithTypeAndName {data_type_file_path, ICEBERG_FILE_PATH},
1030
1031
0
                {data_type_pos, ICEBERG_ROW_POS}};
1032
0
        size_t read_rows = 0;
1033
0
        RETURN_IF_ERROR(parquet_delete_reader.get_next_block(&block, &read_rows, &eof));
1034
1035
0
        if (read_rows <= 0) {
1036
0
            break;
1037
0
        }
1038
0
        RETURN_IF_ERROR(_gen_position_delete_file_range(block, position_delete, read_rows,
1039
0
                                                        dictionary_coded));
1040
0
    }
1041
0
    return Status::OK();
1042
0
};
1043
1044
// ============================================================================
1045
// IcebergOrcReader: on_before_init_reader (ORC-specific schema matching)
1046
// ============================================================================
1047
// This format-specific setup mirrors the existing reader initialization sequence.
1048
// NOLINTNEXTLINE(readability-function-cognitive-complexity,readability-function-size)
1049
24
Status IcebergOrcReader::on_before_init_reader(ReaderInitContext* ctx) {
1050
24
    _column_descs = ctx->column_descs;
1051
24
    _fill_col_name_to_block_idx = ctx->col_name_to_block_idx;
1052
24
    _file_format = Fileformat::ORC;
1053
1054
    // Get ORC file type first (available because _create_file_reader() already ran)
1055
24
    const orc::Type* orc_type_ptr = nullptr;
1056
24
    RETURN_IF_ERROR(this->get_file_type(&orc_type_ptr));
1057
1058
    // Build table_info_node by field_id or name matching.
1059
    // This must happen BEFORE column classification so we can use children_column_exists
1060
    // to check if a column exists in the file (by field ID, not name).
1061
24
    if (!get_scan_params().__isset.history_schema_info ||
1062
24
        get_scan_params().history_schema_info.empty()) [[unlikely]] {
1063
1
        RETURN_IF_ERROR(BuildTableInfoUtil::by_orc_name(ctx->tuple_descriptor, orc_type_ptr,
1064
1
                                                        ctx->table_info_node));
1065
23
    } else {
1066
23
        RETURN_IF_ERROR(BuildTableInfoUtil::by_orc_field_id_with_name_mapping(
1067
23
                get_scan_params().history_schema_info.front().root_field, orc_type_ptr,
1068
23
                ICEBERG_ORC_ATTRIBUTE, ctx->table_info_node,
1069
23
                supports_iceberg_scan_semantics_v1(&get_scan_params())));
1070
23
    }
1071
1072
24
    std::unordered_set<std::string> partition_col_names;
1073
24
    if (ctx->range->__isset.columns_from_path_keys) {
1074
0
        partition_col_names.insert(ctx->range->columns_from_path_keys.begin(),
1075
0
                                   ctx->range->columns_from_path_keys.end());
1076
0
    }
1077
1078
    // Single pass: classify columns, detect $row_id, handle partition fallback.
1079
24
    bool has_partition_from_path = false;
1080
31
    for (const auto& desc : *ctx->column_descs) {
1081
31
        if (desc.category == ColumnCategory::SYNTHESIZED) {
1082
0
            if (desc.name == BeConsts::ICEBERG_ROWID_COL) {
1083
0
                this->register_synthesized_column_handler(
1084
0
                        BeConsts::ICEBERG_ROWID_COL, [this](Block* block, size_t rows) -> Status {
1085
0
                            return _fill_iceberg_row_id(block, rows);
1086
0
                        });
1087
0
                continue;
1088
0
            } else if (desc.name.starts_with(BeConsts::GLOBAL_ROWID_COL)) {
1089
0
                auto topn_row_id_column_iter = _create_topn_row_id_column_iterator();
1090
0
                this->register_synthesized_column_handler(
1091
0
                        desc.name,
1092
0
                        [iter = std::move(topn_row_id_column_iter), this, &desc](
1093
0
                                Block* block, size_t rows) -> Status {
1094
0
                            return fill_topn_row_id(iter, desc.name, block, rows);
1095
0
                        });
1096
0
                continue;
1097
0
            }
1098
31
        } else if (desc.category == ColumnCategory::PARTITION_KEY) {
1099
0
            bool has_partition_value = partition_col_names.contains(desc.name);
1100
0
            bool exists_in_file = ctx->table_info_node->children_column_exists(desc.name);
1101
0
            if (!has_partition_value || exists_in_file) {
1102
0
                ctx->column_names.push_back(desc.name);
1103
0
                continue;
1104
0
            }
1105
0
            has_partition_from_path = true;
1106
31
        } else if (desc.category == ColumnCategory::REGULAR) {
1107
31
            ctx->column_names.push_back(desc.name);
1108
31
        } else if (desc.category == ColumnCategory::GENERATED) {
1109
0
            _init_row_lineage_columns();
1110
0
            if (desc.name == ROW_LINEAGE_ROW_ID) {
1111
0
                ctx->column_names.push_back(desc.name);
1112
0
                this->register_generated_column_handler(
1113
0
                        ROW_LINEAGE_ROW_ID, [this](Block* block, size_t rows) -> Status {
1114
0
                            return _fill_row_lineage_row_id(block, rows);
1115
0
                        });
1116
0
                continue;
1117
0
            } else if (desc.name == ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER) {
1118
0
                ctx->column_names.push_back(desc.name);
1119
0
                this->register_generated_column_handler(
1120
0
                        ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER,
1121
0
                        [this](Block* block, size_t rows) -> Status {
1122
0
                            return _fill_row_lineage_last_updated_sequence_number(block, rows);
1123
0
                        });
1124
0
                continue;
1125
0
            }
1126
0
        }
1127
31
    }
1128
1129
24
    if (has_partition_from_path) {
1130
0
        RETURN_IF_ERROR(_extract_partition_values(*ctx->range, ctx->tuple_descriptor,
1131
0
                                                  _fill_partition_values,
1132
0
                                                  &_fill_partition_value_is_null));
1133
0
    }
1134
1135
24
    _all_required_col_names = ctx->column_names;
1136
1137
    // Create column IDs from ORC type
1138
24
    auto column_id_result =
1139
24
            _create_column_ids(orc_type_ptr, ctx->tuple_descriptor, ctx->table_info_node);
1140
24
    ctx->column_ids = std::move(column_id_result.column_ids);
1141
24
    ctx->filter_column_ids = std::move(column_id_result.filter_column_ids);
1142
1143
    // Build field_id -> block_column_name mapping for equality delete filtering.
1144
31
    for (const auto* slot : ctx->tuple_descriptor->slots()) {
1145
31
        _id_to_block_column_name.emplace(slot->col_unique_id(), slot->col_name());
1146
31
    }
1147
1148
    // Process delete files (must happen before _do_init_reader so expand col IDs are included)
1149
24
    RETURN_IF_ERROR(_init_row_filters());
1150
1151
    // Add expand column IDs for equality delete and remap expand column names
1152
    // (matching master's behavior with __equality_delete_column__ prefix)
1153
24
    const static std::string EQ_DELETE_PRE = "__equality_delete_column__";
1154
24
    bool all_file_columns_have_field_ids = true;
1155
81
    for (uint64_t i = 0; i < orc_type_ptr->getSubtypeCount(); ++i) {
1156
57
        const orc::Type* sub_type = orc_type_ptr->getSubtype(i);
1157
57
        if (!sub_type->hasAttributeKey(ICEBERG_ORC_ATTRIBUTE)) {
1158
16
            all_file_columns_have_field_ids = false;
1159
16
        }
1160
57
    }
1161
24
    const bool use_field_ids_for_hidden_keys =
1162
24
            supports_iceberg_scan_semantics_v1(&get_scan_params())
1163
24
                    ? orc_subtree_has_iceberg_id(orc_type_ptr, ICEBERG_ORC_ATTRIBUTE)
1164
24
                    : all_file_columns_have_field_ids;
1165
24
    const auto find_file_column_by_name = [&](const std::string& name) -> const orc::Type* {
1166
0
        for (uint64_t j = 0; j < orc_type_ptr->getSubtypeCount(); ++j) {
1167
0
            if (iequal(orc_type_ptr->getFieldName(j), name)) {
1168
0
                return orc_type_ptr->getSubtype(j);
1169
0
            }
1170
0
        }
1171
0
        return nullptr;
1172
0
    };
1173
1174
24
    std::vector<std::string> new_expand_col_names;
1175
24
    DORIS_CHECK(_expand_col_names.size() == _expand_col_field_ids.size());
1176
24
    DORIS_CHECK(_expand_col_names.size() == _expand_columns.size());
1177
44
    for (size_t i = 0; i < _expand_col_names.size(); ++i) {
1178
20
        const auto& old_name = _expand_col_names[i];
1179
20
        const int32_t field_id = _expand_col_field_ids[i];
1180
1181
20
        const orc::Type* file_column = nullptr;
1182
20
        OrcEqualityFieldPath file_path;
1183
20
        bool complete_file_path = false;
1184
20
        if (use_field_ids_for_hidden_keys) {
1185
14
            complete_file_path =
1186
14
                    find_orc_equality_field_path_by_id(orc_type_ptr, field_id, &file_path);
1187
14
            if (!complete_file_path && supports_iceberg_scan_semantics_v2(&get_scan_params())) {
1188
6
                const auto table_path = _find_schema_field_path(field_id);
1189
6
                if (!table_path.empty()) {
1190
6
                    complete_file_path = find_orc_equality_field_prefix_by_id_path(
1191
6
                            orc_type_ptr, table_path, &file_path);
1192
6
                }
1193
6
            }
1194
14
            if (!file_path.fields.empty()) {
1195
8
                file_column = file_path.fields.front();
1196
8
            }
1197
14
        } else {
1198
6
            const auto table_path = _find_schema_field_path(field_id);
1199
6
            if (!table_path.empty()) {
1200
6
                complete_file_path = find_orc_equality_field_prefix_by_name_path(
1201
6
                        orc_type_ptr, table_path, old_name, &file_path);
1202
6
                if (!file_path.fields.empty()) {
1203
5
                    file_column = file_path.fields.front();
1204
5
                }
1205
6
            } else {
1206
0
                file_column = find_file_column_by_name(old_name);
1207
0
                complete_file_path = file_column != nullptr;
1208
0
            }
1209
6
        }
1210
1211
20
        std::string file_col_name = old_name;
1212
20
        std::string leaf_name = old_name;
1213
20
        if (!file_path.fields.empty()) {
1214
13
            file_col_name = file_path.names.front();
1215
13
            leaf_name = file_path.names.back();
1216
13
        } else if (file_column != nullptr) {
1217
0
            for (uint64_t j = 0; j < orc_type_ptr->getSubtypeCount(); ++j) {
1218
0
                if (orc_type_ptr->getSubtype(j) == file_column) {
1219
0
                    file_col_name = orc_type_ptr->getFieldName(j);
1220
0
                    leaf_name = file_col_name;
1221
0
                    break;
1222
0
                }
1223
0
            }
1224
0
        }
1225
20
        std::string table_col_name = EQ_DELETE_PRE + std::to_string(field_id) + "_" + leaf_name;
1226
1227
20
        if (field_id >= 0) {
1228
20
            _id_to_block_column_name[field_id] = table_col_name;
1229
20
        }
1230
20
        _expand_columns[i].name = table_col_name;
1231
20
        if (file_column == nullptr) {
1232
7
            RETURN_IF_ERROR(_register_missing_equality_delete_column(field_id, table_col_name,
1233
7
                                                                     _expand_columns[i].type));
1234
            // The old data file predates this equality key. Its logical initial default is appended
1235
            // after the read. Adding it to column_names/table_info_node would mark it as an
1236
            // existing ORC child and make OrcReader read a column that is not present in the file.
1237
7
            new_expand_col_names.push_back(table_col_name);
1238
7
            continue;
1239
7
        }
1240
13
        new_expand_col_names.push_back(table_col_name);
1241
1242
13
        if (!complete_file_path) {
1243
1
            ColumnPtr missing_value;
1244
1
            RETURN_IF_ERROR(_create_missing_equality_delete_value(
1245
1
                    field_id, _expand_columns[i].type, file_path.fields.size(), &missing_value));
1246
1
            _nested_equality_delete_columns.push_back({
1247
1
                    .field_id = field_id,
1248
1
                    .block_name = table_col_name,
1249
1
                    .leaf_type = _expand_columns[i].type,
1250
1
                    .source_leaf_type = _expand_columns[i].type,
1251
1
                    .child_indexes = file_path.child_indexes,
1252
1
                    .missing_value = std::move(missing_value),
1253
1
            });
1254
1
            _expand_columns[i].type = make_nullable(convert_to_doris_type(file_column));
1255
1
            _expand_columns[i].column = _expand_columns[i].type->create_column();
1256
12
        } else if (!file_path.child_indexes.empty()) {
1257
            // The root is read in the data file's own types; a leaf written before a type
1258
            // promotion (INT payload.existing of a column now BIGINT) is widened after extraction.
1259
9
            _nested_equality_delete_columns.push_back({
1260
9
                    .field_id = field_id,
1261
9
                    .block_name = table_col_name,
1262
9
                    .leaf_type = _expand_columns[i].type,
1263
9
                    .source_leaf_type =
1264
9
                            make_nullable(convert_to_doris_type(file_path.fields.back())),
1265
9
                    .child_indexes = file_path.child_indexes,
1266
9
                    .missing_value = nullptr,
1267
9
            });
1268
9
            _expand_columns[i].type = make_nullable(convert_to_doris_type(file_column));
1269
9
            _expand_columns[i].column = _expand_columns[i].type->create_column();
1270
9
        }
1271
1272
13
        for (uint64_t column_id = file_column->getColumnId();
1273
36
             column_id <= file_column->getMaximumColumnId(); ++column_id) {
1274
23
            ctx->column_ids.insert(column_id);
1275
23
        }
1276
1277
13
        ctx->column_names.push_back(table_col_name);
1278
13
        ctx->table_info_node->add_children(table_col_name, file_col_name,
1279
13
                                           TableSchemaChangeHelper::ConstNode::get_instance());
1280
13
    }
1281
24
    _expand_col_names = std::move(new_expand_col_names);
1282
1283
24
    return Status::OK();
1284
24
}
1285
1286
// ============================================================================
1287
// IcebergOrcReader: _create_column_ids
1288
// ============================================================================
1289
ColumnIdResult IcebergOrcReader::_create_column_ids(
1290
        const orc::Type* orc_type, const TupleDescriptor* tuple_descriptor,
1291
31
        const std::shared_ptr<TableSchemaChangeHelper::Node>& table_info_node) {
1292
31
    std::unordered_map<int, const orc::Type*> iceberg_id_to_orc_type_map;
1293
138
    for (uint64_t i = 0; i < orc_type->getSubtypeCount(); ++i) {
1294
107
        const auto* orc_sub_type = orc_type->getSubtype(i);
1295
107
        if (!orc_sub_type) {
1296
0
            continue;
1297
0
        }
1298
107
        if (!orc_sub_type->hasAttributeKey(ICEBERG_ORC_ATTRIBUTE)) {
1299
17
            continue;
1300
17
        }
1301
90
        int iceberg_id = std::stoi(orc_sub_type->getAttributeValue(ICEBERG_ORC_ATTRIBUTE));
1302
90
        iceberg_id_to_orc_type_map[iceberg_id] = orc_sub_type;
1303
90
    }
1304
1305
31
    std::set<uint64_t> column_ids;
1306
31
    std::set<uint64_t> filter_column_ids;
1307
1308
31
    auto process_access_paths = [](const orc::Type* orc_field,
1309
31
                                   const std::vector<TColumnAccessPath>& access_paths,
1310
31
                                   std::set<uint64_t>& out_ids) {
1311
19
        process_nested_access_paths(
1312
19
                orc_field, access_paths, out_ids,
1313
19
                [](const orc::Type* type) { return type->getColumnId(); },
1314
19
                [](const orc::Type* type) { return type->getMaximumColumnId(); },
1315
19
                IcebergOrcNestedColumnUtils::extract_nested_column_ids);
1316
19
    };
1317
1318
    // The Iceberg schema-mapping root is a StructNode whose registered children are the real
1319
    // table columns. When present, resolve each column by name through it so the column-id set
1320
    // stays consistent with the schema-mapping decision (BY_ID or BY_NAME/name-mapping);
1321
    // otherwise fall back to matching by Iceberg field id.
1322
31
    const auto* struct_node =
1323
31
            dynamic_cast<const TableSchemaChangeHelper::StructNode*>(table_info_node.get());
1324
1325
46
    for (const auto* slot : tuple_descriptor->slots()) {
1326
46
        const orc::Type* orc_field = nullptr;
1327
46
        if (struct_node != nullptr) {
1328
            // Synthesized/metadata slots (e.g. the TopN global row-id or the $row_id column) are
1329
            // never registered as children, so check membership before querying: calling
1330
            // children_column_exists() on an unregistered name DCHECK-aborts in debug builds and
1331
            // throws std::out_of_range from .at() in release builds.
1332
33
            if (struct_node->get_children().contains(slot->col_name()) &&
1333
33
                struct_node->children_column_exists(slot->col_name())) {
1334
                // Select the physical child resolved by the shared schema-mapping pass. Hidden
1335
                // equality keys and projected columns must obey the same BY_NAME decision for
1336
                // partial-id ORC files.
1337
31
                const auto& file_column_name =
1338
31
                        struct_node->children_file_column_name(slot->col_name());
1339
40
                for (uint64_t i = 0; i < orc_type->getSubtypeCount(); ++i) {
1340
40
                    if (orc_type->getFieldName(i) == file_column_name) {
1341
31
                        orc_field = orc_type->getSubtype(i);
1342
31
                        break;
1343
31
                    }
1344
40
                }
1345
31
                DORIS_CHECK(orc_field != nullptr);
1346
31
            }
1347
33
        } else {
1348
13
            auto it = iceberg_id_to_orc_type_map.find(slot->col_unique_id());
1349
13
            if (it != iceberg_id_to_orc_type_map.end()) {
1350
13
                orc_field = it->second;
1351
13
            }
1352
13
        }
1353
46
        if (orc_field == nullptr) {
1354
2
            continue;
1355
2
        }
1356
1357
44
        if ((slot->col_type() != TYPE_STRUCT && slot->col_type() != TYPE_ARRAY &&
1358
44
             slot->col_type() != TYPE_MAP)) {
1359
31
            column_ids.insert(orc_field->getColumnId());
1360
31
            if (slot->is_predicate()) {
1361
0
                filter_column_ids.insert(orc_field->getColumnId());
1362
0
            }
1363
31
            continue;
1364
31
        }
1365
1366
13
        const auto& all_access_paths = slot->all_access_paths();
1367
13
        process_access_paths(orc_field, all_access_paths, column_ids);
1368
1369
13
        const auto& predicate_access_paths = slot->predicate_access_paths();
1370
13
        if (!predicate_access_paths.empty()) {
1371
6
            process_access_paths(orc_field, predicate_access_paths, filter_column_ids);
1372
6
        }
1373
13
    }
1374
1375
31
    return {std::move(column_ids), std::move(filter_column_ids)};
1376
31
}
1377
1378
// ============================================================================
1379
// IcebergOrcReader: _read_position_delete_file
1380
// ============================================================================
1381
Status IcebergOrcReader::_read_position_delete_file(const TFileRangeDesc* delete_range,
1382
0
                                                    DeleteFile* position_delete) {
1383
0
    OrcReader orc_delete_reader(get_profile(), get_state(), get_scan_params(), *delete_range,
1384
0
                                READ_DELETE_FILE_BATCH_SIZE, get_state()->timezone(), get_io_ctx(),
1385
0
                                _meta_cache);
1386
0
    OrcInitContext delete_ctx;
1387
0
    delete_ctx.column_names = delete_file_col_names;
1388
0
    delete_ctx.col_name_to_block_idx =
1389
0
            const_cast<std::unordered_map<std::string, uint32_t>*>(&DELETE_COL_NAME_TO_BLOCK_IDX);
1390
0
    RETURN_IF_ERROR(orc_delete_reader.init_reader(&delete_ctx));
1391
1392
0
    bool eof = false;
1393
0
    DataTypePtr data_type_file_path {new DataTypeString};
1394
0
    DataTypePtr data_type_pos {new DataTypeInt64};
1395
0
    while (!eof) {
1396
0
        Block block = {{data_type_file_path, ICEBERG_FILE_PATH}, {data_type_pos, ICEBERG_ROW_POS}};
1397
1398
0
        size_t read_rows = 0;
1399
0
        RETURN_IF_ERROR(orc_delete_reader.get_next_block(&block, &read_rows, &eof));
1400
1401
0
        RETURN_IF_ERROR(_gen_position_delete_file_range(block, position_delete, read_rows, false));
1402
0
    }
1403
0
    return Status::OK();
1404
0
}
1405
1406
} // namespace doris