Coverage Report

Created: 2026-07-21 09:51

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