Coverage Report

Created: 2026-07-29 00:27

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
392
bool orc_subtree_has_iceberg_id(const orc::Type* type, const std::string& attribute) {
84
392
    if (type->hasAttributeKey(attribute)) {
85
193
        return true;
86
193
    }
87
205
    for (uint64_t idx = 0; idx < type->getSubtypeCount(); ++idx) {
88
199
        if (orc_subtree_has_iceberg_id(type->getSubtype(idx), attribute)) {
89
193
            return true;
90
193
        }
91
199
    }
92
6
    return false;
93
199
}
94
95
} // namespace
96
97
bool IcebergTableReader::_is_fully_dictionary_encoded(
98
9
        const tparquet::ColumnMetaData& column_metadata) {
99
15
    const auto is_dictionary_encoding = [](tparquet::Encoding::type encoding) {
100
15
        return encoding == tparquet::Encoding::PLAIN_DICTIONARY ||
101
15
               encoding == tparquet::Encoding::RLE_DICTIONARY;
102
15
    };
103
13
    const auto is_data_page = [](tparquet::PageType::type page_type) {
104
13
        return page_type == tparquet::PageType::DATA_PAGE ||
105
13
               page_type == tparquet::PageType::DATA_PAGE_V2;
106
13
    };
107
9
    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
9
    if (column_metadata.__isset.encoding_stats) {
114
8
        bool has_data_page_stats = false;
115
13
        for (const tparquet::PageEncodingStats& enc_stat : column_metadata.encoding_stats) {
116
13
            if (is_data_page(enc_stat.page_type) && enc_stat.count > 0) {
117
9
                has_data_page_stats = true;
118
9
                if (!is_dictionary_encoding(enc_stat.encoding)) {
119
3
                    return false;
120
3
                }
121
9
            }
122
13
        }
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
399
Status IcebergParquetReader::on_before_init_reader(ReaderInitContext* ctx) {
151
399
    _column_descs = ctx->column_descs;
152
399
    _fill_col_name_to_block_idx = ctx->col_name_to_block_idx;
153
399
    _file_format = Fileformat::PARQUET;
154
155
    // Get file metadata schema first (available because _open_file() already ran)
156
399
    const FieldDescriptor* field_desc = nullptr;
157
399
    RETURN_IF_ERROR(this->get_file_metadata_schema(&field_desc));
158
399
    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
399
    if (!get_scan_params().__isset.history_schema_info ||
164
399
        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
398
    } else {
168
398
        RETURN_IF_ERROR(BuildTableInfoUtil::by_parquet_field_id_with_name_mapping(
169
398
                get_scan_params().history_schema_info.front().root_field, *field_desc,
170
398
                ctx->table_info_node, supports_iceberg_scan_semantics_v1(&get_scan_params())));
171
398
    }
172
173
399
    std::unordered_set<std::string> partition_col_names;
174
399
    if (ctx->range->__isset.columns_from_path_keys) {
175
36
        partition_col_names.insert(ctx->range->columns_from_path_keys.begin(),
176
36
                                   ctx->range->columns_from_path_keys.end());
177
36
    }
178
179
    // Single pass: classify columns, detect $row_id, handle partition fallback.
180
399
    bool has_partition_from_path = false;
181
1.83k
    for (const auto& desc : *ctx->column_descs) {
182
1.83k
        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
1.83k
        } else if (desc.category == ColumnCategory::PARTITION_KEY) {
200
46
            bool has_partition_value = partition_col_names.contains(desc.name);
201
46
            bool exists_in_file = ctx->table_info_node->children_column_exists(desc.name);
202
46
            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
46
                ctx->column_names.push_back(desc.name);
206
46
                continue;
207
46
            }
208
0
            has_partition_from_path = true;
209
1.80k
        } else if (desc.category == ColumnCategory::REGULAR) {
210
1.80k
            ctx->column_names.push_back(desc.name);
211
18.4E
        } 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
1.83k
    }
231
232
    // Set up partition value extraction if any partition columns need filling from path
233
399
    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
399
    _all_required_col_names = ctx->column_names;
240
241
    // Create column IDs from field descriptor
242
399
    auto column_id_result =
243
399
            _create_column_ids(field_desc, ctx->tuple_descriptor, ctx->table_info_node);
244
399
    ctx->column_ids = std::move(column_id_result.column_ids);
245
399
    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
1.83k
    for (const auto* slot : ctx->tuple_descriptor->slots()) {
250
1.83k
        _id_to_block_column_name.emplace(slot->col_unique_id(), slot->col_name());
251
1.83k
    }
252
253
    // Process delete files (must happen before _do_init_reader so expand col IDs are included)
254
399
    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
396
    const static std::string EQ_DELETE_PRE = "__equality_delete_column__";
262
396
    std::unordered_map<int, const FieldSchema*> field_id_to_file_column;
263
396
    bool all_file_columns_have_field_ids = true;
264
396
    bool any_file_column_has_field_id = false;
265
5.73k
    for (int i = 0; i < field_desc->size(); ++i) {
266
5.33k
        const auto* field_schema = field_desc->get_column(i);
267
5.33k
        if (field_schema) {
268
5.32k
            if (field_schema->field_id < 0) {
269
9
                all_file_columns_have_field_ids = false;
270
5.31k
            } else {
271
5.31k
                any_file_column_has_field_id = true;
272
5.31k
                field_id_to_file_column[field_schema->field_id] = field_schema;
273
5.31k
            }
274
5.32k
        }
275
5.33k
    }
276
396
    const bool use_field_ids_for_hidden_keys =
277
396
            supports_iceberg_scan_semantics_v1(&get_scan_params())
278
396
                    ? any_file_column_has_field_id
279
396
                    : all_file_columns_have_field_ids;
280
396
    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
396
    std::vector<std::string> new_expand_col_names;
292
396
    DORIS_CHECK(_expand_col_names.size() == _expand_col_field_ids.size());
293
396
    DORIS_CHECK(_expand_col_names.size() == _expand_columns.size());
294
403
    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
396
    _expand_col_names = std::move(new_expand_col_names);
362
363
    // Enable group filtering for Iceberg
364
396
    _filter_groups = true;
365
366
396
    return Status::OK();
367
396
}
368
369
// ============================================================================
370
// IcebergParquetReader: _create_column_ids
371
// ============================================================================
372
ColumnIdResult IcebergParquetReader::_create_column_ids(
373
        const FieldDescriptor* field_desc, const TupleDescriptor* tuple_descriptor,
374
403
        const std::shared_ptr<TableSchemaChangeHelper::Node>& table_info_node) {
375
403
    auto* mutable_field_desc = const_cast<FieldDescriptor*>(field_desc);
376
403
    mutable_field_desc->assign_ids();
377
378
403
    std::unordered_map<int, const FieldSchema*> iceberg_id_to_field_schema_map;
379
5.84k
    for (int i = 0; i < field_desc->size(); ++i) {
380
5.44k
        auto field_schema = field_desc->get_column(i);
381
5.44k
        if (!field_schema) continue;
382
5.44k
        int iceberg_id = field_schema->field_id;
383
5.44k
        iceberg_id_to_field_schema_map[iceberg_id] = field_schema;
384
5.44k
    }
385
386
403
    std::set<uint64_t> column_ids;
387
403
    std::set<uint64_t> filter_column_ids;
388
389
403
    auto process_access_paths = [](const FieldSchema* parquet_field,
390
403
                                   const std::vector<TColumnAccessPath>& access_paths,
391
403
                                   std::set<uint64_t>& out_ids) {
392
383
        process_nested_access_paths(
393
383
                parquet_field, access_paths, out_ids,
394
392
                [](const FieldSchema* field) { return field->get_column_id(); },
395
383
                [](const FieldSchema* field) { return field->get_max_column_id(); },
396
383
                IcebergParquetNestedColumnUtils::extract_nested_column_ids);
397
383
    };
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
403
    const auto* struct_node =
404
403
            dynamic_cast<const TableSchemaChangeHelper::StructNode*>(table_info_node.get());
405
406
1.85k
    for (const auto* slot : tuple_descriptor->slots()) {
407
1.85k
        const FieldSchema* field_schema = nullptr;
408
1.85k
        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
1.84k
            if (struct_node->get_children().contains(slot->col_name()) &&
414
1.85k
                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
1.75k
                const auto& file_column_name =
419
1.75k
                        struct_node->children_file_column_name(slot->col_name());
420
10.9k
                for (int i = 0; i < field_desc->size(); ++i) {
421
10.9k
                    const auto* candidate = field_desc->get_column(i);
422
10.9k
                    if (candidate != nullptr && candidate->name == file_column_name) {
423
1.75k
                        field_schema = candidate;
424
1.75k
                        break;
425
1.75k
                    }
426
10.9k
                }
427
1.75k
                DORIS_CHECK(field_schema != nullptr);
428
1.75k
            }
429
1.84k
        } else {
430
6
            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
6
        }
435
1.85k
        if (field_schema == nullptr) {
436
107
            continue;
437
107
        }
438
439
1.74k
        if ((slot->col_type() != TYPE_STRUCT && slot->col_type() != TYPE_ARRAY &&
440
1.74k
             slot->col_type() != TYPE_MAP)) {
441
1.40k
            column_ids.insert(field_schema->column_id);
442
1.40k
            if (slot->is_predicate()) {
443
133
                filter_column_ids.insert(field_schema->column_id);
444
133
            }
445
1.40k
            continue;
446
1.40k
        }
447
448
339
        const auto& all_access_paths = slot->all_access_paths();
449
339
        process_access_paths(field_schema, all_access_paths, column_ids);
450
451
339
        const auto& predicate_access_paths = slot->predicate_access_paths();
452
339
        if (!predicate_access_paths.empty()) {
453
48
            process_access_paths(field_schema, predicate_access_paths, filter_column_ids);
454
48
        }
455
339
    }
456
403
    return {std::move(column_ids), std::move(filter_column_ids)};
457
403
}
458
459
// ============================================================================
460
// IcebergParquetReader: _read_position_delete_file
461
// ============================================================================
462
Status IcebergParquetReader::_read_position_delete_file(const TFileRangeDesc* delete_range,
463
4
                                                        DeleteFile* position_delete) {
464
4
    ParquetReader parquet_delete_reader(get_profile(), get_scan_params(), *delete_range,
465
4
                                        READ_DELETE_FILE_BATCH_SIZE, &get_state()->timezone_obj(),
466
4
                                        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
4
    ParquetInitContext delete_ctx;
471
4
    delete_ctx.filter_groups = false;
472
4
    delete_ctx.column_names = delete_file_col_names;
473
4
    delete_ctx.col_name_to_block_idx =
474
4
            const_cast<std::unordered_map<std::string, uint32_t>*>(&DELETE_COL_NAME_TO_BLOCK_IDX);
475
4
    RETURN_IF_ERROR(parquet_delete_reader.init_reader(&delete_ctx));
476
477
3
    const tparquet::FileMetaData* meta_data = parquet_delete_reader.get_meta_data();
478
3
    bool dictionary_coded = true;
479
3
    for (const auto& row_group : meta_data->row_groups) {
480
3
        const auto& column_chunk = row_group.columns[ICEBERG_FILE_PATH_INDEX];
481
3
        if (!(column_chunk.__isset.meta_data && has_dict_page(column_chunk.meta_data))) {
482
2
            dictionary_coded = false;
483
2
            break;
484
2
        }
485
3
    }
486
3
    DataTypePtr data_type_file_path = make_nullable(std::make_shared<DataTypeString>());
487
3
    DataTypePtr data_type_pos = make_nullable(std::make_shared<DataTypeInt64>());
488
3
    bool eof = false;
489
6
    while (!eof) {
490
3
        Block block = {
491
3
                dictionary_coded
492
3
                        ? ColumnWithTypeAndName {ColumnNullable::create(ColumnDictI32::create(),
493
1
                                                                        ColumnUInt8::create()),
494
1
                                                 data_type_file_path, ICEBERG_FILE_PATH}
495
3
                        : ColumnWithTypeAndName {data_type_file_path, ICEBERG_FILE_PATH},
496
497
3
                {data_type_pos, ICEBERG_ROW_POS}};
498
3
        size_t read_rows = 0;
499
3
        RETURN_IF_ERROR(parquet_delete_reader.get_next_block(&block, &read_rows, &eof));
500
501
3
        if (read_rows <= 0) {
502
0
            break;
503
0
        }
504
3
        RETURN_IF_ERROR(_gen_position_delete_file_range(block, position_delete, read_rows,
505
3
                                                        dictionary_coded));
506
3
    }
507
3
    return Status::OK();
508
3
};
509
510
// ============================================================================
511
// IcebergOrcReader: on_before_init_reader (ORC-specific schema matching)
512
// ============================================================================
513
197
Status IcebergOrcReader::on_before_init_reader(ReaderInitContext* ctx) {
514
197
    _column_descs = ctx->column_descs;
515
197
    _fill_col_name_to_block_idx = ctx->col_name_to_block_idx;
516
197
    _file_format = Fileformat::ORC;
517
518
    // Get ORC file type first (available because _create_file_reader() already ran)
519
197
    const orc::Type* orc_type_ptr = nullptr;
520
197
    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
197
    if (!get_scan_params().__isset.history_schema_info ||
526
197
        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
196
    } else {
530
196
        RETURN_IF_ERROR(BuildTableInfoUtil::by_orc_field_id_with_name_mapping(
531
196
                get_scan_params().history_schema_info.front().root_field, orc_type_ptr,
532
196
                ICEBERG_ORC_ATTRIBUTE, ctx->table_info_node,
533
196
                supports_iceberg_scan_semantics_v1(&get_scan_params())));
534
196
    }
535
536
197
    std::unordered_set<std::string> partition_col_names;
537
197
    if (ctx->range->__isset.columns_from_path_keys) {
538
17
        partition_col_names.insert(ctx->range->columns_from_path_keys.begin(),
539
17
                                   ctx->range->columns_from_path_keys.end());
540
17
    }
541
542
    // Single pass: classify columns, detect $row_id, handle partition fallback.
543
197
    bool has_partition_from_path = false;
544
596
    for (const auto& desc : *ctx->column_descs) {
545
596
        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
596
        } else if (desc.category == ColumnCategory::PARTITION_KEY) {
563
22
            bool has_partition_value = partition_col_names.contains(desc.name);
564
22
            bool exists_in_file = ctx->table_info_node->children_column_exists(desc.name);
565
22
            if (!has_partition_value || exists_in_file) {
566
22
                ctx->column_names.push_back(desc.name);
567
22
                continue;
568
22
            }
569
0
            has_partition_from_path = true;
570
574
        } else if (desc.category == ColumnCategory::REGULAR) {
571
569
            ctx->column_names.push_back(desc.name);
572
569
        } 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
596
    }
592
593
197
    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
197
    _all_required_col_names = ctx->column_names;
600
601
    // Create column IDs from ORC type
602
197
    auto column_id_result =
603
197
            _create_column_ids(orc_type_ptr, ctx->tuple_descriptor, ctx->table_info_node);
604
197
    ctx->column_ids = std::move(column_id_result.column_ids);
605
197
    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
590
    for (const auto* slot : ctx->tuple_descriptor->slots()) {
609
590
        _id_to_block_column_name.emplace(slot->col_unique_id(), slot->col_name());
610
590
    }
611
612
    // Process delete files (must happen before _do_init_reader so expand col IDs are included)
613
197
    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
197
    const static std::string EQ_DELETE_PRE = "__equality_delete_column__";
618
197
    std::unordered_map<int, const orc::Type*> field_id_to_file_column;
619
197
    bool all_file_columns_have_field_ids = true;
620
1.45k
    for (uint64_t i = 0; i < orc_type_ptr->getSubtypeCount(); ++i) {
621
1.25k
        const orc::Type* sub_type = orc_type_ptr->getSubtype(i);
622
1.25k
        if (sub_type->hasAttributeKey(ICEBERG_ORC_ATTRIBUTE)) {
623
1.25k
            int fid = std::stoi(sub_type->getAttributeValue(ICEBERG_ORC_ATTRIBUTE));
624
1.25k
            field_id_to_file_column[fid] = sub_type;
625
1.25k
        } else {
626
2
            all_file_columns_have_field_ids = false;
627
2
        }
628
1.25k
    }
629
197
    const bool use_field_ids_for_hidden_keys =
630
197
            supports_iceberg_scan_semantics_v1(&get_scan_params())
631
197
                    ? orc_subtree_has_iceberg_id(orc_type_ptr, ICEBERG_ORC_ATTRIBUTE)
632
197
                    : all_file_columns_have_field_ids;
633
197
    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
197
    std::vector<std::string> new_expand_col_names;
643
197
    DORIS_CHECK(_expand_col_names.size() == _expand_col_field_ids.size());
644
197
    DORIS_CHECK(_expand_col_names.size() == _expand_columns.size());
645
204
    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
197
    _expand_col_names = std::move(new_expand_col_names);
713
714
197
    return Status::OK();
715
197
}
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
195
        const std::shared_ptr<TableSchemaChangeHelper::Node>& table_info_node) {
723
195
    std::unordered_map<int, const orc::Type*> iceberg_id_to_orc_type_map;
724
1.49k
    for (uint64_t i = 0; i < orc_type->getSubtypeCount(); ++i) {
725
1.29k
        const auto* orc_sub_type = orc_type->getSubtype(i);
726
1.29k
        if (!orc_sub_type) {
727
0
            continue;
728
0
        }
729
1.29k
        if (!orc_sub_type->hasAttributeKey(ICEBERG_ORC_ATTRIBUTE)) {
730
8
            continue;
731
8
        }
732
1.29k
        int iceberg_id = std::stoi(orc_sub_type->getAttributeValue(ICEBERG_ORC_ATTRIBUTE));
733
1.29k
        iceberg_id_to_orc_type_map[iceberg_id] = orc_sub_type;
734
1.29k
    }
735
736
195
    std::set<uint64_t> column_ids;
737
195
    std::set<uint64_t> filter_column_ids;
738
739
195
    auto process_access_paths = [](const orc::Type* orc_field,
740
195
                                   const std::vector<TColumnAccessPath>& access_paths,
741
195
                                   std::set<uint64_t>& out_ids) {
742
112
        process_nested_access_paths(
743
112
                orc_field, access_paths, out_ids,
744
115
                [](const orc::Type* type) { return type->getColumnId(); },
745
112
                [](const orc::Type* type) { return type->getMaximumColumnId(); },
746
112
                IcebergOrcNestedColumnUtils::extract_nested_column_ids);
747
112
    };
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
195
    const auto* struct_node =
754
195
            dynamic_cast<const TableSchemaChangeHelper::StructNode*>(table_info_node.get());
755
756
607
    for (const auto* slot : tuple_descriptor->slots()) {
757
607
        const orc::Type* orc_field = nullptr;
758
607
        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
595
            if (struct_node->get_children().contains(slot->col_name()) &&
764
597
                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
498
                const auto& file_column_name =
769
498
                        struct_node->children_file_column_name(slot->col_name());
770
1.86k
                for (uint64_t i = 0; i < orc_type->getSubtypeCount(); ++i) {
771
1.86k
                    if (orc_type->getFieldName(i) == file_column_name) {
772
501
                        orc_field = orc_type->getSubtype(i);
773
501
                        break;
774
501
                    }
775
1.86k
                }
776
498
                DORIS_CHECK(orc_field != nullptr);
777
498
            }
778
595
        } else {
779
12
            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
12
        }
784
607
        if (orc_field == nullptr) {
785
110
            continue;
786
110
        }
787
788
497
        if ((slot->col_type() != TYPE_STRUCT && slot->col_type() != TYPE_ARRAY &&
789
497
             slot->col_type() != TYPE_MAP)) {
790
405
            column_ids.insert(orc_field->getColumnId());
791
405
            if (slot->is_predicate()) {
792
65
                filter_column_ids.insert(orc_field->getColumnId());
793
65
            }
794
405
            continue;
795
405
        }
796
797
92
        const auto& all_access_paths = slot->all_access_paths();
798
92
        process_access_paths(orc_field, all_access_paths, column_ids);
799
800
92
        const auto& predicate_access_paths = slot->predicate_access_paths();
801
92
        if (!predicate_access_paths.empty()) {
802
15
            process_access_paths(orc_field, predicate_access_paths, filter_column_ids);
803
15
        }
804
92
    }
805
806
195
    return {std::move(column_ids), std::move(filter_column_ids)};
807
195
}
808
809
// ============================================================================
810
// IcebergOrcReader: _read_position_delete_file
811
// ============================================================================
812
Status IcebergOrcReader::_read_position_delete_file(const TFileRangeDesc* delete_range,
813
6
                                                    DeleteFile* position_delete) {
814
6
    OrcReader orc_delete_reader(get_profile(), get_state(), get_scan_params(), *delete_range,
815
6
                                READ_DELETE_FILE_BATCH_SIZE, get_state()->timezone(), get_io_ctx(),
816
6
                                _meta_cache);
817
6
    OrcInitContext delete_ctx;
818
6
    delete_ctx.column_names = delete_file_col_names;
819
6
    delete_ctx.col_name_to_block_idx =
820
6
            const_cast<std::unordered_map<std::string, uint32_t>*>(&DELETE_COL_NAME_TO_BLOCK_IDX);
821
6
    RETURN_IF_ERROR(orc_delete_reader.init_reader(&delete_ctx));
822
823
6
    bool eof = false;
824
6
    DataTypePtr data_type_file_path {new DataTypeString};
825
6
    DataTypePtr data_type_pos {new DataTypeInt64};
826
18
    while (!eof) {
827
12
        Block block = {{data_type_file_path, ICEBERG_FILE_PATH}, {data_type_pos, ICEBERG_ROW_POS}};
828
829
12
        size_t read_rows = 0;
830
12
        RETURN_IF_ERROR(orc_delete_reader.get_next_block(&block, &read_rows, &eof));
831
832
12
        RETURN_IF_ERROR(_gen_position_delete_file_range(block, position_delete, read_rows, false));
833
12
    }
834
6
    return Status::OK();
835
6
}
836
837
} // namespace doris