Coverage Report

Created: 2026-07-25 10:26

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
33
#include "common/compiler_util.h" // IWYU pragma: keep
34
#include "common/consts.h"
35
#include "common/status.h"
36
#include "core/assert_cast.h"
37
#include "core/block/block.h"
38
#include "core/block/column_with_type_and_name.h"
39
#include "core/column/column.h"
40
#include "core/column/column_nullable.h"
41
#include "core/column/column_string.h"
42
#include "core/column/column_vector.h"
43
#include "core/data_type/data_type_factory.hpp"
44
#include "core/data_type/data_type_nullable.h"
45
#include "core/data_type/define_primitive_type.h"
46
#include "core/data_type/primitive_type.h"
47
#include "core/string_ref.h"
48
#include "exprs/aggregate/aggregate_function.h"
49
#include "format/format_common.h"
50
#include "format/generic_reader.h"
51
#include "format/orc/vorc_reader.h"
52
#include "format/parquet/schema_desc.h"
53
#include "format/parquet/vparquet_column_chunk_reader.h"
54
#include "format/table/deletion_vector_reader.h"
55
#include "format/table/iceberg/iceberg_orc_nested_column_utils.h"
56
#include "format/table/iceberg/iceberg_parquet_nested_column_utils.h"
57
#include "format/table/iceberg_scan_semantics.h"
58
#include "format/table/nested_column_access_helper.h"
59
#include "format/table/table_schema_change_helper.h"
60
#include "runtime/runtime_state.h"
61
#include "util/coding.h"
62
#include "util/string_util.h"
63
64
namespace cctz {
65
class time_zone;
66
} // namespace cctz
67
namespace doris {
68
class RowDescriptor;
69
class SlotDescriptor;
70
class TupleDescriptor;
71
72
namespace io {
73
struct IOContext;
74
} // namespace io
75
class VExprContext;
76
} // namespace doris
77
78
namespace doris {
79
const std::string IcebergOrcReader::ICEBERG_ORC_ATTRIBUTE = "iceberg.id";
80
81
namespace {
82
83
20
bool orc_subtree_has_iceberg_id(const orc::Type* type, const std::string& attribute) {
84
20
    if (type->hasAttributeKey(attribute)) {
85
6
        return true;
86
6
    }
87
20
    for (uint64_t idx = 0; idx < type->getSubtypeCount(); ++idx) {
88
12
        if (orc_subtree_has_iceberg_id(type->getSubtype(idx), attribute)) {
89
6
            return true;
90
6
        }
91
12
    }
92
8
    return false;
93
14
}
94
95
} // namespace
96
97
bool IcebergTableReader::_is_fully_dictionary_encoded(
98
8
        const tparquet::ColumnMetaData& column_metadata) {
99
14
    const auto is_dictionary_encoding = [](tparquet::Encoding::type encoding) {
100
14
        return encoding == tparquet::Encoding::PLAIN_DICTIONARY ||
101
14
               encoding == tparquet::Encoding::RLE_DICTIONARY;
102
14
    };
103
12
    const auto is_data_page = [](tparquet::PageType::type page_type) {
104
12
        return page_type == tparquet::PageType::DATA_PAGE ||
105
12
               page_type == tparquet::PageType::DATA_PAGE_V2;
106
12
    };
107
8
    const auto is_level_encoding = [](tparquet::Encoding::type encoding) {
108
2
        return encoding == tparquet::Encoding::RLE || encoding == tparquet::Encoding::BIT_PACKED;
109
2
    };
110
111
    // A column chunk may have a dictionary page but still contain plain-encoded data pages.
112
    // Only treat it as dictionary-coded when all data pages are dictionary encoded.
113
8
    if (column_metadata.__isset.encoding_stats) {
114
7
        bool has_data_page_stats = false;
115
12
        for (const tparquet::PageEncodingStats& enc_stat : column_metadata.encoding_stats) {
116
12
            if (is_data_page(enc_stat.page_type) && enc_stat.count > 0) {
117
8
                has_data_page_stats = true;
118
8
                if (!is_dictionary_encoding(enc_stat.encoding)) {
119
2
                    return false;
120
2
                }
121
8
            }
122
12
        }
123
5
        if (has_data_page_stats) {
124
4
            return true;
125
4
        }
126
5
    }
127
128
2
    bool has_dict_encoding = false;
129
2
    bool has_nondict_encoding = false;
130
3
    for (const tparquet::Encoding::type& encoding : column_metadata.encodings) {
131
3
        if (is_dictionary_encoding(encoding)) {
132
1
            has_dict_encoding = true;
133
1
        }
134
135
3
        if (!is_dictionary_encoding(encoding) && !is_level_encoding(encoding)) {
136
2
            has_nondict_encoding = true;
137
2
            break;
138
2
        }
139
3
    }
140
2
    if (!has_dict_encoding || has_nondict_encoding) {
141
2
        return false;
142
2
    }
143
144
0
    return true;
145
2
}
146
147
// ============================================================================
148
// IcebergParquetReader: on_before_init_reader (Parquet-specific schema matching)
149
// ============================================================================
150
10
Status IcebergParquetReader::on_before_init_reader(ReaderInitContext* ctx) {
151
10
    _column_descs = ctx->column_descs;
152
10
    _fill_col_name_to_block_idx = ctx->col_name_to_block_idx;
153
10
    _file_format = Fileformat::PARQUET;
154
155
    // Get file metadata schema first (available because _open_file() already ran)
156
10
    const FieldDescriptor* field_desc = nullptr;
157
10
    RETURN_IF_ERROR(this->get_file_metadata_schema(&field_desc));
158
10
    DCHECK(field_desc != nullptr);
159
160
    // Build table_info_node by field_id or name matching.
161
    // This must happen BEFORE column classification so we can use children_column_exists
162
    // to check if a column exists in the file (by field ID, not name).
163
10
    if (!get_scan_params().__isset.history_schema_info ||
164
10
        get_scan_params().history_schema_info.empty()) [[unlikely]] {
165
1
        RETURN_IF_ERROR(BuildTableInfoUtil::by_parquet_name(ctx->tuple_descriptor, *field_desc,
166
1
                                                            ctx->table_info_node));
167
9
    } else {
168
9
        RETURN_IF_ERROR(BuildTableInfoUtil::by_parquet_field_id_with_name_mapping(
169
9
                get_scan_params().history_schema_info.front().root_field, *field_desc,
170
9
                ctx->table_info_node, supports_iceberg_scan_semantics_v1(&get_scan_params())));
171
9
    }
172
173
10
    std::unordered_set<std::string> partition_col_names;
174
10
    if (ctx->range->__isset.columns_from_path_keys) {
175
0
        partition_col_names.insert(ctx->range->columns_from_path_keys.begin(),
176
0
                                   ctx->range->columns_from_path_keys.end());
177
0
    }
178
179
    // Single pass: classify columns, detect $row_id, handle partition fallback.
180
10
    bool has_partition_from_path = false;
181
12
    for (const auto& desc : *ctx->column_descs) {
182
12
        if (desc.category == ColumnCategory::SYNTHESIZED) {
183
0
            if (desc.name == BeConsts::ICEBERG_ROWID_COL) {
184
0
                this->register_synthesized_column_handler(
185
0
                        BeConsts::ICEBERG_ROWID_COL, [this](Block* block, size_t rows) -> Status {
186
0
                            return _fill_iceberg_row_id(block, rows);
187
0
                        });
188
0
                continue;
189
0
            } else if (desc.name.starts_with(BeConsts::GLOBAL_ROWID_COL)) {
190
0
                auto topn_row_id_column_iter = _create_topn_row_id_column_iterator();
191
0
                this->register_synthesized_column_handler(
192
0
                        desc.name,
193
0
                        [iter = std::move(topn_row_id_column_iter), this, &desc](
194
0
                                Block* block, size_t rows) -> Status {
195
0
                            return fill_topn_row_id(iter, desc.name, block, rows);
196
0
                        });
197
0
                continue;
198
0
            }
199
12
        } else if (desc.category == ColumnCategory::PARTITION_KEY) {
200
0
            bool has_partition_value = partition_col_names.contains(desc.name);
201
0
            bool exists_in_file = ctx->table_info_node->children_column_exists(desc.name);
202
0
            if (!has_partition_value || exists_in_file) {
203
                // Keep PARTITION_KEY category stable for scan planning, but still read
204
                // from file when the column exists there.
205
0
                ctx->column_names.push_back(desc.name);
206
0
                continue;
207
0
            }
208
0
            has_partition_from_path = true;
209
12
        } else if (desc.category == ColumnCategory::REGULAR) {
210
12
            ctx->column_names.push_back(desc.name);
211
12
        } else if (desc.category == ColumnCategory::GENERATED) {
212
0
            _init_row_lineage_columns();
213
0
            if (desc.name == ROW_LINEAGE_ROW_ID) {
214
0
                ctx->column_names.push_back(desc.name);
215
0
                this->register_generated_column_handler(
216
0
                        ROW_LINEAGE_ROW_ID, [this](Block* block, size_t rows) -> Status {
217
0
                            return _fill_row_lineage_row_id(block, rows);
218
0
                        });
219
0
                continue;
220
0
            } else if (desc.name == ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER) {
221
0
                ctx->column_names.push_back(desc.name);
222
0
                this->register_generated_column_handler(
223
0
                        ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER,
224
0
                        [this](Block* block, size_t rows) -> Status {
225
0
                            return _fill_row_lineage_last_updated_sequence_number(block, rows);
226
0
                        });
227
0
                continue;
228
0
            }
229
0
        }
230
12
    }
231
232
    // Set up partition value extraction if any partition columns need filling from path
233
10
    if (has_partition_from_path) {
234
0
        RETURN_IF_ERROR(_extract_partition_values(*ctx->range, ctx->tuple_descriptor,
235
0
                                                  _fill_partition_values,
236
0
                                                  &_fill_partition_value_is_null));
237
0
    }
238
239
10
    _all_required_col_names = ctx->column_names;
240
241
    // Create column IDs from field descriptor
242
10
    auto column_id_result =
243
10
            _create_column_ids(field_desc, ctx->tuple_descriptor, ctx->table_info_node);
244
10
    ctx->column_ids = std::move(column_id_result.column_ids);
245
10
    ctx->filter_column_ids = std::move(column_id_result.filter_column_ids);
246
247
    // Build field_id -> block_column_name mapping for equality delete filtering.
248
    // This was previously done in init_reader() column matching (pre-CRTP refactoring).
249
12
    for (const auto* slot : ctx->tuple_descriptor->slots()) {
250
12
        _id_to_block_column_name.emplace(slot->col_unique_id(), slot->col_name());
251
12
    }
252
253
    // Process delete files (must happen before _do_init_reader so expand col IDs are included)
254
10
    RETURN_IF_ERROR(_init_row_filters());
255
256
    // Add expand column IDs for equality delete and remap expand column names
257
    // to match master's behavior:
258
    // - Use field_id to find the actual file column name in Parquet schema
259
    // - Prefix with __equality_delete_column__ to avoid name conflicts
260
    // - Correctly map table_col_name → file_col_name in table_info_node
261
10
    const static std::string EQ_DELETE_PRE = "__equality_delete_column__";
262
10
    std::unordered_map<int, const FieldSchema*> field_id_to_file_column;
263
10
    bool all_file_columns_have_field_ids = true;
264
10
    bool any_file_column_has_field_id = false;
265
36
    for (int i = 0; i < field_desc->size(); ++i) {
266
26
        const auto* field_schema = field_desc->get_column(i);
267
26
        if (field_schema) {
268
26
            if (field_schema->field_id < 0) {
269
9
                all_file_columns_have_field_ids = false;
270
17
            } else {
271
17
                any_file_column_has_field_id = true;
272
17
                field_id_to_file_column[field_schema->field_id] = field_schema;
273
17
            }
274
26
        }
275
26
    }
276
10
    const bool use_field_ids_for_hidden_keys =
277
10
            supports_iceberg_scan_semantics_v1(&get_scan_params())
278
10
                    ? any_file_column_has_field_id
279
10
                    : all_file_columns_have_field_ids;
280
10
    const auto find_file_column_by_name = [&](const std::string& name) -> const FieldSchema* {
281
9
        for (int j = 0; j < field_desc->size(); ++j) {
282
8
            const auto* candidate = field_desc->get_column(j);
283
8
            if (candidate != nullptr && iequal(candidate->name, name)) {
284
2
                return candidate;
285
2
            }
286
8
        }
287
1
        return nullptr;
288
3
    };
289
290
    // Rebuild _expand_col_names with proper file-column-based names
291
10
    std::vector<std::string> new_expand_col_names;
292
10
    DORIS_CHECK(_expand_col_names.size() == _expand_col_field_ids.size());
293
10
    DORIS_CHECK(_expand_col_names.size() == _expand_columns.size());
294
17
    for (size_t i = 0; i < _expand_col_names.size(); ++i) {
295
7
        const auto& old_name = _expand_col_names[i];
296
7
        const int32_t field_id = _expand_col_field_ids[i];
297
298
7
        const FieldSchema* file_column = nullptr;
299
7
        if (use_field_ids_for_hidden_keys) {
300
4
            auto id_it = field_id_to_file_column.find(field_id);
301
4
            if (id_it != field_id_to_file_column.end()) {
302
1
                file_column = id_it->second;
303
1
            }
304
4
        } else {
305
3
            const auto* table_field = _find_schema_field(field_id);
306
3
            const bool authoritative_name_mapping =
307
3
                    table_field != nullptr && table_field->__isset.name_mapping &&
308
3
                    table_field->__isset.name_mapping_is_authoritative &&
309
3
                    table_field->name_mapping_is_authoritative;
310
3
            if (table_field != nullptr && table_field->__isset.name_mapping) {
311
2
                for (const auto& mapped_name : table_field->name_mapping) {
312
1
                    file_column = find_file_column_by_name(mapped_name);
313
1
                    if (file_column != nullptr) {
314
1
                        break;
315
1
                    }
316
1
                }
317
2
            }
318
3
            if (file_column == nullptr && !authoritative_name_mapping && table_field != nullptr &&
319
3
                table_field->__isset.name) {
320
1
                file_column = find_file_column_by_name(table_field->name);
321
1
            }
322
3
            if (file_column == nullptr && !authoritative_name_mapping) {
323
                // A schema-history fallback may carry a name from after the target snapshot. The
324
                // delete-file key name is target-relative, but it must never override an explicit
325
                // authoritative Iceberg name mapping.
326
1
                file_column = find_file_column_by_name(old_name);
327
1
            }
328
3
        }
329
330
7
        const std::string file_col_name = file_column == nullptr ? old_name : file_column->name;
331
7
        std::string table_col_name = EQ_DELETE_PRE + std::to_string(field_id) + "_" + file_col_name;
332
333
        // Update _id_to_block_column_name
334
7
        if (field_id >= 0) {
335
7
            _id_to_block_column_name[field_id] = table_col_name;
336
7
        }
337
338
        // Update _expand_columns name
339
7
        _expand_columns[i].name = table_col_name;
340
341
7
        if (file_column == nullptr) {
342
4
            RETURN_IF_ERROR(_register_missing_equality_delete_column(field_id, table_col_name,
343
4
                                                                     _expand_columns[i].type));
344
            // The old data file predates this equality key. Keep it in the expand block so the
345
            // synthesized-column hook can materialize its logical initial default before reader
346
            // filtering, but do not advertise it to Parquet as a physical child.
347
4
            new_expand_col_names.push_back(table_col_name);
348
4
            continue;
349
4
        }
350
351
3
        new_expand_col_names.push_back(table_col_name);
352
353
        // Add column IDs
354
3
        ctx->column_ids.insert(file_column->get_column_id());
355
356
        // Register in table_info_node: table_col_name → file_col_name
357
3
        ctx->column_names.push_back(table_col_name);
358
3
        ctx->table_info_node->add_children(table_col_name, file_col_name,
359
3
                                           TableSchemaChangeHelper::ConstNode::get_instance());
360
3
    }
361
10
    _expand_col_names = std::move(new_expand_col_names);
362
363
    // Enable group filtering for Iceberg
364
10
    _filter_groups = true;
365
366
10
    return Status::OK();
367
10
}
368
369
// ============================================================================
370
// IcebergParquetReader: _create_column_ids
371
// ============================================================================
372
ColumnIdResult IcebergParquetReader::_create_column_ids(
373
        const FieldDescriptor* field_desc, const TupleDescriptor* tuple_descriptor,
374
17
        const std::shared_ptr<TableSchemaChangeHelper::Node>& table_info_node) {
375
17
    auto* mutable_field_desc = const_cast<FieldDescriptor*>(field_desc);
376
17
    mutable_field_desc->assign_ids();
377
378
17
    std::unordered_map<int, const FieldSchema*> iceberg_id_to_field_schema_map;
379
93
    for (int i = 0; i < field_desc->size(); ++i) {
380
76
        auto field_schema = field_desc->get_column(i);
381
76
        if (!field_schema) continue;
382
76
        int iceberg_id = field_schema->field_id;
383
76
        iceberg_id_to_field_schema_map[iceberg_id] = field_schema;
384
76
    }
385
386
17
    std::set<uint64_t> column_ids;
387
17
    std::set<uint64_t> filter_column_ids;
388
389
17
    auto process_access_paths = [](const FieldSchema* parquet_field,
390
17
                                   const std::vector<TColumnAccessPath>& access_paths,
391
17
                                   std::set<uint64_t>& out_ids) {
392
17
        process_nested_access_paths(
393
17
                parquet_field, access_paths, out_ids,
394
17
                [](const FieldSchema* field) { return field->get_column_id(); },
395
17
                [](const FieldSchema* field) { return field->get_max_column_id(); },
396
17
                IcebergParquetNestedColumnUtils::extract_nested_column_ids);
397
17
    };
398
399
    // The Iceberg schema-mapping root is a StructNode whose registered children are the real
400
    // table columns. When present, resolve each column by name through it so the column-id set
401
    // stays consistent with the schema-mapping decision (BY_ID or BY_NAME/name-mapping);
402
    // otherwise fall back to matching by Iceberg field id.
403
17
    const auto* struct_node =
404
17
            dynamic_cast<const TableSchemaChangeHelper::StructNode*>(table_info_node.get());
405
406
27
    for (const auto* slot : tuple_descriptor->slots()) {
407
27
        const FieldSchema* field_schema = nullptr;
408
27
        if (struct_node != nullptr) {
409
            // Synthesized/metadata slots (e.g. the TopN global row-id or the $row_id column) are
410
            // never registered as children, so check membership before querying: calling
411
            // children_column_exists() on an unregistered name DCHECK-aborts in debug builds and
412
            // throws std::out_of_range from .at() in release builds.
413
14
            if (struct_node->get_children().contains(slot->col_name()) &&
414
14
                struct_node->children_column_exists(slot->col_name())) {
415
                // Use the physical child selected by the schema-mapping pass. This keeps partial-id
416
                // files in BY_NAME mode from binding a projected column through an unrelated stale
417
                // field id.
418
12
                const auto& file_column_name =
419
12
                        struct_node->children_file_column_name(slot->col_name());
420
17
                for (int i = 0; i < field_desc->size(); ++i) {
421
17
                    const auto* candidate = field_desc->get_column(i);
422
17
                    if (candidate != nullptr && candidate->name == file_column_name) {
423
12
                        field_schema = candidate;
424
12
                        break;
425
12
                    }
426
17
                }
427
12
                DORIS_CHECK(field_schema != nullptr);
428
12
            }
429
14
        } else {
430
13
            auto it = iceberg_id_to_field_schema_map.find(slot->col_unique_id());
431
13
            if (it != iceberg_id_to_field_schema_map.end()) {
432
13
                field_schema = it->second;
433
13
            }
434
13
        }
435
27
        if (field_schema == nullptr) {
436
2
            continue;
437
2
        }
438
439
25
        if ((slot->col_type() != TYPE_STRUCT && slot->col_type() != TYPE_ARRAY &&
440
25
             slot->col_type() != TYPE_MAP)) {
441
14
            column_ids.insert(field_schema->column_id);
442
14
            if (slot->is_predicate()) {
443
0
                filter_column_ids.insert(field_schema->column_id);
444
0
            }
445
14
            continue;
446
14
        }
447
448
11
        const auto& all_access_paths = slot->all_access_paths();
449
11
        process_access_paths(field_schema, all_access_paths, column_ids);
450
451
11
        const auto& predicate_access_paths = slot->predicate_access_paths();
452
11
        if (!predicate_access_paths.empty()) {
453
6
            process_access_paths(field_schema, predicate_access_paths, filter_column_ids);
454
6
        }
455
11
    }
456
17
    return {std::move(column_ids), std::move(filter_column_ids)};
457
17
}
458
459
// ============================================================================
460
// IcebergParquetReader: _read_position_delete_file
461
// ============================================================================
462
Status IcebergParquetReader::_read_position_delete_file(const TFileRangeDesc* delete_range,
463
1
                                                        DeleteFile* position_delete) {
464
1
    ParquetReader parquet_delete_reader(get_profile(), get_scan_params(), *delete_range,
465
1
                                        READ_DELETE_FILE_BATCH_SIZE, &get_state()->timezone_obj(),
466
1
                                        get_io_ctx(), get_state(), _meta_cache);
467
    // The delete file range has size=-1 (read whole file). We must disable
468
    // row group filtering before init; otherwise _do_init_reader returns EndOfFile
469
    // when _filter_groups && _range_size < 0.
470
1
    ParquetInitContext delete_ctx;
471
1
    delete_ctx.filter_groups = false;
472
1
    delete_ctx.column_names = delete_file_col_names;
473
1
    delete_ctx.col_name_to_block_idx =
474
1
            const_cast<std::unordered_map<std::string, uint32_t>*>(&DELETE_COL_NAME_TO_BLOCK_IDX);
475
1
    RETURN_IF_ERROR(parquet_delete_reader.init_reader(&delete_ctx));
476
477
0
    const tparquet::FileMetaData* meta_data = parquet_delete_reader.get_meta_data();
478
0
    bool dictionary_coded = true;
479
0
    for (const auto& row_group : meta_data->row_groups) {
480
0
        const auto& column_chunk = row_group.columns[ICEBERG_FILE_PATH_INDEX];
481
0
        if (!(column_chunk.__isset.meta_data && has_dict_page(column_chunk.meta_data))) {
482
0
            dictionary_coded = false;
483
0
            break;
484
0
        }
485
0
    }
486
0
    DataTypePtr data_type_file_path = make_nullable(std::make_shared<DataTypeString>());
487
0
    DataTypePtr data_type_pos = make_nullable(std::make_shared<DataTypeInt64>());
488
0
    bool eof = false;
489
0
    while (!eof) {
490
0
        Block block = {
491
0
                dictionary_coded
492
0
                        ? ColumnWithTypeAndName {ColumnNullable::create(ColumnDictI32::create(),
493
0
                                                                        ColumnUInt8::create()),
494
0
                                                 data_type_file_path, ICEBERG_FILE_PATH}
495
0
                        : ColumnWithTypeAndName {data_type_file_path, ICEBERG_FILE_PATH},
496
497
0
                {data_type_pos, ICEBERG_ROW_POS}};
498
0
        size_t read_rows = 0;
499
0
        RETURN_IF_ERROR(parquet_delete_reader.get_next_block(&block, &read_rows, &eof));
500
501
0
        if (read_rows <= 0) {
502
0
            break;
503
0
        }
504
0
        RETURN_IF_ERROR(_gen_position_delete_file_range(block, position_delete, read_rows,
505
0
                                                        dictionary_coded));
506
0
    }
507
0
    return Status::OK();
508
0
};
509
510
// ============================================================================
511
// IcebergOrcReader: on_before_init_reader (ORC-specific schema matching)
512
// ============================================================================
513
9
Status IcebergOrcReader::on_before_init_reader(ReaderInitContext* ctx) {
514
9
    _column_descs = ctx->column_descs;
515
9
    _fill_col_name_to_block_idx = ctx->col_name_to_block_idx;
516
9
    _file_format = Fileformat::ORC;
517
518
    // Get ORC file type first (available because _create_file_reader() already ran)
519
9
    const orc::Type* orc_type_ptr = nullptr;
520
9
    RETURN_IF_ERROR(this->get_file_type(&orc_type_ptr));
521
522
    // Build table_info_node by field_id or name matching.
523
    // This must happen BEFORE column classification so we can use children_column_exists
524
    // to check if a column exists in the file (by field ID, not name).
525
9
    if (!get_scan_params().__isset.history_schema_info ||
526
9
        get_scan_params().history_schema_info.empty()) [[unlikely]] {
527
1
        RETURN_IF_ERROR(BuildTableInfoUtil::by_orc_name(ctx->tuple_descriptor, orc_type_ptr,
528
1
                                                        ctx->table_info_node));
529
8
    } else {
530
8
        RETURN_IF_ERROR(BuildTableInfoUtil::by_orc_field_id_with_name_mapping(
531
8
                get_scan_params().history_schema_info.front().root_field, orc_type_ptr,
532
8
                ICEBERG_ORC_ATTRIBUTE, ctx->table_info_node,
533
8
                supports_iceberg_scan_semantics_v1(&get_scan_params())));
534
8
    }
535
536
9
    std::unordered_set<std::string> partition_col_names;
537
9
    if (ctx->range->__isset.columns_from_path_keys) {
538
0
        partition_col_names.insert(ctx->range->columns_from_path_keys.begin(),
539
0
                                   ctx->range->columns_from_path_keys.end());
540
0
    }
541
542
    // Single pass: classify columns, detect $row_id, handle partition fallback.
543
9
    bool has_partition_from_path = false;
544
12
    for (const auto& desc : *ctx->column_descs) {
545
12
        if (desc.category == ColumnCategory::SYNTHESIZED) {
546
0
            if (desc.name == BeConsts::ICEBERG_ROWID_COL) {
547
0
                this->register_synthesized_column_handler(
548
0
                        BeConsts::ICEBERG_ROWID_COL, [this](Block* block, size_t rows) -> Status {
549
0
                            return _fill_iceberg_row_id(block, rows);
550
0
                        });
551
0
                continue;
552
0
            } else if (desc.name.starts_with(BeConsts::GLOBAL_ROWID_COL)) {
553
0
                auto topn_row_id_column_iter = _create_topn_row_id_column_iterator();
554
0
                this->register_synthesized_column_handler(
555
0
                        desc.name,
556
0
                        [iter = std::move(topn_row_id_column_iter), this, &desc](
557
0
                                Block* block, size_t rows) -> Status {
558
0
                            return fill_topn_row_id(iter, desc.name, block, rows);
559
0
                        });
560
0
                continue;
561
0
            }
562
12
        } else if (desc.category == ColumnCategory::PARTITION_KEY) {
563
0
            bool has_partition_value = partition_col_names.contains(desc.name);
564
0
            bool exists_in_file = ctx->table_info_node->children_column_exists(desc.name);
565
0
            if (!has_partition_value || exists_in_file) {
566
0
                ctx->column_names.push_back(desc.name);
567
0
                continue;
568
0
            }
569
0
            has_partition_from_path = true;
570
12
        } else if (desc.category == ColumnCategory::REGULAR) {
571
12
            ctx->column_names.push_back(desc.name);
572
12
        } else if (desc.category == ColumnCategory::GENERATED) {
573
0
            _init_row_lineage_columns();
574
0
            if (desc.name == ROW_LINEAGE_ROW_ID) {
575
0
                ctx->column_names.push_back(desc.name);
576
0
                this->register_generated_column_handler(
577
0
                        ROW_LINEAGE_ROW_ID, [this](Block* block, size_t rows) -> Status {
578
0
                            return _fill_row_lineage_row_id(block, rows);
579
0
                        });
580
0
                continue;
581
0
            } else if (desc.name == ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER) {
582
0
                ctx->column_names.push_back(desc.name);
583
0
                this->register_generated_column_handler(
584
0
                        ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER,
585
0
                        [this](Block* block, size_t rows) -> Status {
586
0
                            return _fill_row_lineage_last_updated_sequence_number(block, rows);
587
0
                        });
588
0
                continue;
589
0
            }
590
0
        }
591
12
    }
592
593
9
    if (has_partition_from_path) {
594
0
        RETURN_IF_ERROR(_extract_partition_values(*ctx->range, ctx->tuple_descriptor,
595
0
                                                  _fill_partition_values,
596
0
                                                  &_fill_partition_value_is_null));
597
0
    }
598
599
9
    _all_required_col_names = ctx->column_names;
600
601
    // Create column IDs from ORC type
602
9
    auto column_id_result =
603
9
            _create_column_ids(orc_type_ptr, ctx->tuple_descriptor, ctx->table_info_node);
604
9
    ctx->column_ids = std::move(column_id_result.column_ids);
605
9
    ctx->filter_column_ids = std::move(column_id_result.filter_column_ids);
606
607
    // Build field_id -> block_column_name mapping for equality delete filtering.
608
12
    for (const auto* slot : ctx->tuple_descriptor->slots()) {
609
12
        _id_to_block_column_name.emplace(slot->col_unique_id(), slot->col_name());
610
12
    }
611
612
    // Process delete files (must happen before _do_init_reader so expand col IDs are included)
613
9
    RETURN_IF_ERROR(_init_row_filters());
614
615
    // Add expand column IDs for equality delete and remap expand column names
616
    // (matching master's behavior with __equality_delete_column__ prefix)
617
9
    const static std::string EQ_DELETE_PRE = "__equality_delete_column__";
618
9
    std::unordered_map<int, const orc::Type*> field_id_to_file_column;
619
9
    bool all_file_columns_have_field_ids = true;
620
31
    for (uint64_t i = 0; i < orc_type_ptr->getSubtypeCount(); ++i) {
621
22
        const orc::Type* sub_type = orc_type_ptr->getSubtype(i);
622
22
        if (sub_type->hasAttributeKey(ICEBERG_ORC_ATTRIBUTE)) {
623
15
            int fid = std::stoi(sub_type->getAttributeValue(ICEBERG_ORC_ATTRIBUTE));
624
15
            field_id_to_file_column[fid] = sub_type;
625
15
        } else {
626
7
            all_file_columns_have_field_ids = false;
627
7
        }
628
22
    }
629
9
    const bool use_field_ids_for_hidden_keys =
630
9
            supports_iceberg_scan_semantics_v1(&get_scan_params())
631
9
                    ? orc_subtree_has_iceberg_id(orc_type_ptr, ICEBERG_ORC_ATTRIBUTE)
632
9
                    : all_file_columns_have_field_ids;
633
9
    const auto find_file_column_by_name = [&](const std::string& name) -> const orc::Type* {
634
6
        for (uint64_t j = 0; j < orc_type_ptr->getSubtypeCount(); ++j) {
635
5
            if (iequal(orc_type_ptr->getFieldName(j), name)) {
636
1
                return orc_type_ptr->getSubtype(j);
637
1
            }
638
5
        }
639
1
        return nullptr;
640
2
    };
641
642
9
    std::vector<std::string> new_expand_col_names;
643
9
    DORIS_CHECK(_expand_col_names.size() == _expand_col_field_ids.size());
644
9
    DORIS_CHECK(_expand_col_names.size() == _expand_columns.size());
645
16
    for (size_t i = 0; i < _expand_col_names.size(); ++i) {
646
7
        const auto& old_name = _expand_col_names[i];
647
7
        const int32_t field_id = _expand_col_field_ids[i];
648
649
7
        const orc::Type* file_column = nullptr;
650
7
        if (use_field_ids_for_hidden_keys) {
651
5
            auto id_it = field_id_to_file_column.find(field_id);
652
5
            if (id_it != field_id_to_file_column.end()) {
653
1
                file_column = id_it->second;
654
1
            }
655
5
        } else {
656
2
            const auto* table_field = _find_schema_field(field_id);
657
2
            const bool authoritative_name_mapping =
658
2
                    table_field != nullptr && table_field->__isset.name_mapping &&
659
2
                    table_field->__isset.name_mapping_is_authoritative &&
660
2
                    table_field->name_mapping_is_authoritative;
661
2
            if (table_field != nullptr && table_field->__isset.name_mapping) {
662
1
                for (const auto& mapped_name : table_field->name_mapping) {
663
0
                    file_column = find_file_column_by_name(mapped_name);
664
0
                    if (file_column != nullptr) {
665
0
                        break;
666
0
                    }
667
0
                }
668
1
            }
669
2
            if (file_column == nullptr && !authoritative_name_mapping && table_field != nullptr &&
670
2
                table_field->__isset.name) {
671
1
                file_column = find_file_column_by_name(table_field->name);
672
1
            }
673
2
            if (file_column == nullptr && !authoritative_name_mapping) {
674
1
                file_column = find_file_column_by_name(old_name);
675
1
            }
676
2
        }
677
678
7
        std::string file_col_name = old_name;
679
7
        if (file_column != nullptr) {
680
5
            for (uint64_t j = 0; j < orc_type_ptr->getSubtypeCount(); ++j) {
681
5
                if (orc_type_ptr->getSubtype(j) == file_column) {
682
2
                    file_col_name = orc_type_ptr->getFieldName(j);
683
2
                    break;
684
2
                }
685
5
            }
686
2
        }
687
7
        std::string table_col_name = EQ_DELETE_PRE + std::to_string(field_id) + "_" + file_col_name;
688
689
7
        if (field_id >= 0) {
690
7
            _id_to_block_column_name[field_id] = table_col_name;
691
7
        }
692
7
        _expand_columns[i].name = table_col_name;
693
7
        if (file_column == nullptr) {
694
5
            RETURN_IF_ERROR(_register_missing_equality_delete_column(field_id, table_col_name,
695
5
                                                                     _expand_columns[i].type));
696
            // The old data file predates this equality key. Keep it in the expand block so the
697
            // synthesized-column hook can materialize its logical initial default before ORC's
698
            // block-size checks. Adding it to column_names/table_info_node would mark it as an
699
            // existing ORC child and make OrcReader read a column that is not present in the file.
700
5
            new_expand_col_names.push_back(table_col_name);
701
5
            continue;
702
5
        }
703
2
        new_expand_col_names.push_back(table_col_name);
704
705
        // Add column IDs
706
2
        ctx->column_ids.insert(file_column->getColumnId());
707
708
2
        ctx->column_names.push_back(table_col_name);
709
2
        ctx->table_info_node->add_children(table_col_name, file_col_name,
710
2
                                           TableSchemaChangeHelper::ConstNode::get_instance());
711
2
    }
712
9
    _expand_col_names = std::move(new_expand_col_names);
713
714
9
    return Status::OK();
715
9
}
716
717
// ============================================================================
718
// IcebergOrcReader: _create_column_ids
719
// ============================================================================
720
ColumnIdResult IcebergOrcReader::_create_column_ids(
721
        const orc::Type* orc_type, const TupleDescriptor* tuple_descriptor,
722
16
        const std::shared_ptr<TableSchemaChangeHelper::Node>& table_info_node) {
723
16
    std::unordered_map<int, const orc::Type*> iceberg_id_to_orc_type_map;
724
88
    for (uint64_t i = 0; i < orc_type->getSubtypeCount(); ++i) {
725
72
        const auto* orc_sub_type = orc_type->getSubtype(i);
726
72
        if (!orc_sub_type) {
727
0
            continue;
728
0
        }
729
72
        if (!orc_sub_type->hasAttributeKey(ICEBERG_ORC_ATTRIBUTE)) {
730
8
            continue;
731
8
        }
732
64
        int iceberg_id = std::stoi(orc_sub_type->getAttributeValue(ICEBERG_ORC_ATTRIBUTE));
733
64
        iceberg_id_to_orc_type_map[iceberg_id] = orc_sub_type;
734
64
    }
735
736
16
    std::set<uint64_t> column_ids;
737
16
    std::set<uint64_t> filter_column_ids;
738
739
16
    auto process_access_paths = [](const orc::Type* orc_field,
740
16
                                   const std::vector<TColumnAccessPath>& access_paths,
741
17
                                   std::set<uint64_t>& out_ids) {
742
17
        process_nested_access_paths(
743
17
                orc_field, access_paths, out_ids,
744
17
                [](const orc::Type* type) { return type->getColumnId(); },
745
17
                [](const orc::Type* type) { return type->getMaximumColumnId(); },
746
17
                IcebergOrcNestedColumnUtils::extract_nested_column_ids);
747
17
    };
748
749
    // The Iceberg schema-mapping root is a StructNode whose registered children are the real
750
    // table columns. When present, resolve each column by name through it so the column-id set
751
    // stays consistent with the schema-mapping decision (BY_ID or BY_NAME/name-mapping);
752
    // otherwise fall back to matching by Iceberg field id.
753
16
    const auto* struct_node =
754
16
            dynamic_cast<const TableSchemaChangeHelper::StructNode*>(table_info_node.get());
755
756
27
    for (const auto* slot : tuple_descriptor->slots()) {
757
27
        const orc::Type* orc_field = nullptr;
758
27
        if (struct_node != nullptr) {
759
            // Synthesized/metadata slots (e.g. the TopN global row-id or the $row_id column) are
760
            // never registered as children, so check membership before querying: calling
761
            // children_column_exists() on an unregistered name DCHECK-aborts in debug builds and
762
            // throws std::out_of_range from .at() in release builds.
763
14
            if (struct_node->get_children().contains(slot->col_name()) &&
764
14
                struct_node->children_column_exists(slot->col_name())) {
765
                // Select the physical child resolved by the shared schema-mapping pass. Hidden
766
                // equality keys and projected columns must obey the same BY_NAME decision for
767
                // partial-id ORC files.
768
12
                const auto& file_column_name =
769
12
                        struct_node->children_file_column_name(slot->col_name());
770
16
                for (uint64_t i = 0; i < orc_type->getSubtypeCount(); ++i) {
771
16
                    if (orc_type->getFieldName(i) == file_column_name) {
772
12
                        orc_field = orc_type->getSubtype(i);
773
12
                        break;
774
12
                    }
775
16
                }
776
12
                DORIS_CHECK(orc_field != nullptr);
777
12
            }
778
14
        } else {
779
13
            auto it = iceberg_id_to_orc_type_map.find(slot->col_unique_id());
780
13
            if (it != iceberg_id_to_orc_type_map.end()) {
781
13
                orc_field = it->second;
782
13
            }
783
13
        }
784
27
        if (orc_field == nullptr) {
785
2
            continue;
786
2
        }
787
788
25
        if ((slot->col_type() != TYPE_STRUCT && slot->col_type() != TYPE_ARRAY &&
789
25
             slot->col_type() != TYPE_MAP)) {
790
14
            column_ids.insert(orc_field->getColumnId());
791
14
            if (slot->is_predicate()) {
792
0
                filter_column_ids.insert(orc_field->getColumnId());
793
0
            }
794
14
            continue;
795
14
        }
796
797
11
        const auto& all_access_paths = slot->all_access_paths();
798
11
        process_access_paths(orc_field, all_access_paths, column_ids);
799
800
11
        const auto& predicate_access_paths = slot->predicate_access_paths();
801
11
        if (!predicate_access_paths.empty()) {
802
6
            process_access_paths(orc_field, predicate_access_paths, filter_column_ids);
803
6
        }
804
11
    }
805
806
16
    return {std::move(column_ids), std::move(filter_column_ids)};
807
16
}
808
809
// ============================================================================
810
// IcebergOrcReader: _read_position_delete_file
811
// ============================================================================
812
Status IcebergOrcReader::_read_position_delete_file(const TFileRangeDesc* delete_range,
813
0
                                                    DeleteFile* position_delete) {
814
0
    OrcReader orc_delete_reader(get_profile(), get_state(), get_scan_params(), *delete_range,
815
0
                                READ_DELETE_FILE_BATCH_SIZE, get_state()->timezone(), get_io_ctx(),
816
0
                                _meta_cache);
817
0
    OrcInitContext delete_ctx;
818
0
    delete_ctx.column_names = delete_file_col_names;
819
0
    delete_ctx.col_name_to_block_idx =
820
0
            const_cast<std::unordered_map<std::string, uint32_t>*>(&DELETE_COL_NAME_TO_BLOCK_IDX);
821
0
    RETURN_IF_ERROR(orc_delete_reader.init_reader(&delete_ctx));
822
823
0
    bool eof = false;
824
0
    DataTypePtr data_type_file_path {new DataTypeString};
825
0
    DataTypePtr data_type_pos {new DataTypeInt64};
826
0
    while (!eof) {
827
0
        Block block = {{data_type_file_path, ICEBERG_FILE_PATH}, {data_type_pos, ICEBERG_ROW_POS}};
828
829
0
        size_t read_rows = 0;
830
0
        RETURN_IF_ERROR(orc_delete_reader.get_next_block(&block, &read_rows, &eof));
831
832
0
        RETURN_IF_ERROR(_gen_position_delete_file_range(block, position_delete, read_rows, false));
833
0
    }
834
0
    return Status::OK();
835
0
}
836
837
} // namespace doris