Coverage Report

Created: 2026-07-19 00:39

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