Coverage Report

Created: 2026-07-23 14:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format_v2/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_v2/table/iceberg_reader.h"
19
20
#include <algorithm>
21
#include <memory>
22
#include <sstream>
23
#include <utility>
24
25
#include "common/cast_set.h"
26
#include "common/consts.h"
27
#include "core/assert_cast.h"
28
#include "core/block/block.h"
29
#include "core/column/column_const.h"
30
#include "core/column/column_nullable.h"
31
#include "core/column/column_string.h"
32
#include "core/column/column_struct.h"
33
#include "core/column/column_vector.h"
34
#include "core/data_type/data_type_number.h"
35
#include "core/data_type/define_primitive_type.h"
36
#include "core/field.h"
37
#include "exprs/vliteral.h"
38
#include "exprs/vslot_ref.h"
39
#include "format/table/deletion_vector_reader.h"
40
#include "format_v2/expr/cast.h"
41
#include "format_v2/expr/equality_delete_predicate.h"
42
#include "format_v2/orc/orc_reader.h"
43
#include "format_v2/parquet/parquet_reader.h"
44
#include "format_v2/parquet/reader/column_reader.h"
45
#include "format_v2/table_reader.h"
46
#include "io/file_factory.h"
47
#include "util/debug_points.h"
48
#include "util/url_coding.h"
49
50
namespace doris::format::iceberg {
51
52
static constexpr const char* ROW_LINEAGE_ROW_ID = "_row_id";
53
static constexpr int32_t ROW_LINEAGE_ROW_ID_FIELD_ID = 2147483540;
54
55
template <typename T>
56
0
static std::string join_values_for_debug(const std::vector<T>& values) {
57
0
    std::ostringstream out;
58
0
    out << "[";
59
0
    for (size_t idx = 0; idx < values.size(); ++idx) {
60
0
        if (idx > 0) {
61
0
            out << ", ";
62
0
        }
63
0
        out << values[idx];
64
0
    }
65
0
    out << "]";
66
0
    return out.str();
67
0
}
68
69
1
static bool is_projected_row_lineage_row_id(const format::ColumnDefinition& column) {
70
    // Iceberg row lineage columns can be bound by field id when a mapper has already been built,
71
    // but customize_file_scan_request() is also exercised directly by scan-request tests before the
72
    // mapper exists. In that path, inspect the projected table schema so row-position dependencies
73
    // are still added for `_row_id`.
74
1
    return column.name == ROW_LINEAGE_ROW_ID ||
75
1
           (column.has_identifier_field_id() &&
76
0
            column.get_identifier_field_id() == ROW_LINEAGE_ROW_ID_FIELD_ID);
77
1
}
78
79
43
static bool is_projected_iceberg_rowid(const format::ColumnDefinition& column) {
80
43
    return column.name == BeConsts::ICEBERG_ROWID_COL;
81
43
}
82
83
static Status build_missing_equality_delete_key_expr(const format::ColumnDefinition& table_field,
84
                                                     const DataTypePtr& delete_key_type,
85
7
                                                     VExprSPtr* key_expr) {
86
7
    DORIS_CHECK(delete_key_type != nullptr);
87
7
    DORIS_CHECK(key_expr != nullptr);
88
7
    if (!table_field.initial_default_value.has_value()) {
89
        // A newly added optional field without an initial default is logically NULL in older
90
        // files. EqualityDeletePredicate treats NULL == NULL as a match.
91
2
        *key_expr = VLiteral::create_shared(make_nullable(delete_key_type), Field());
92
2
        return Status::OK();
93
2
    }
94
95
5
    VExprSPtr literal;
96
5
    if (table_field.initial_default_value_is_base64 ||
97
5
        table_field.type->get_primitive_type() == TYPE_VARBINARY) {
98
        // New FE versions mark every Iceberg UUID/BINARY/FIXED default as Base64 regardless of its
99
        // Doris mapping. Keep the VARBINARY fallback for scan descriptors produced before that
100
        // marker existed. Decode before parsing so STRING/CHAR and VARBINARY all compare against
101
        // the raw bytes stored in equality-delete files.
102
3
        std::string decoded_default;
103
3
        if (!base64_decode(*table_field.initial_default_value, &decoded_default)) {
104
0
            return Status::InvalidArgument("Invalid Base64 Iceberg initial default for field {}",
105
0
                                           table_field.name);
106
0
        }
107
3
        if (table_field.type->get_primitive_type() == TYPE_VARBINARY) {
108
1
            const auto initial_default =
109
1
                    Field::create_field<TYPE_VARBINARY>(StringView(decoded_default));
110
            // VLiteral must copy the borrowed StringView while decoded_default is alive; UUID and
111
            // long FIXED defaults otherwise retain a pointer into freed decode storage.
112
1
            literal = VLiteral::create_shared(table_field.type, initial_default);
113
2
        } else {
114
2
            DORIS_CHECK(is_string_type(table_field.type->get_primitive_type()));
115
2
            literal = VLiteral::create_shared(table_field.type,
116
2
                                              Field::create_field<TYPE_STRING>(decoded_default));
117
2
        }
118
3
    } else {
119
        // An added field's initial default is its logical value in every older data file that lacks
120
        // the physical column. FE normalizes the string for the current Doris table type.
121
2
        Field initial_default;
122
2
        RETURN_IF_ERROR(table_field.type->get_serde()->from_fe_string(
123
2
                *table_field.initial_default_value, initial_default));
124
2
        literal = VLiteral::create_shared(table_field.type, initial_default);
125
2
    }
126
127
5
    DORIS_CHECK(literal != nullptr);
128
5
    if (table_field.type->equals(*delete_key_type)) {
129
1
        *key_expr = std::move(literal);
130
1
        return Status::OK();
131
1
    }
132
4
    auto cast_expr = Cast::create_shared(delete_key_type);
133
4
    cast_expr->add_child(std::move(literal));
134
4
    *key_expr = std::move(cast_expr);
135
4
    return Status::OK();
136
5
}
137
138
0
static std::string iceberg_delete_file_debug_string(const TIcebergDeleteFileDesc& delete_file) {
139
0
    std::ostringstream out;
140
0
    out << "TIcebergDeleteFileDesc{path=" << (delete_file.__isset.path ? delete_file.path : "null")
141
0
        << ", content=" << (delete_file.__isset.content ? delete_file.content : -1)
142
0
        << ", file_format="
143
0
        << (delete_file.__isset.file_format ? static_cast<int>(delete_file.file_format) : -1)
144
0
        << ", position_lower_bound="
145
0
        << (delete_file.__isset.position_lower_bound ? delete_file.position_lower_bound : -1)
146
0
        << ", position_upper_bound="
147
0
        << (delete_file.__isset.position_upper_bound ? delete_file.position_upper_bound : -1)
148
0
        << ", field_ids="
149
0
        << (delete_file.__isset.field_ids ? join_values_for_debug(delete_file.field_ids) : "[]")
150
0
        << ", content_offset="
151
0
        << (delete_file.__isset.content_offset ? delete_file.content_offset : -1)
152
0
        << ", content_size_in_bytes="
153
0
        << (delete_file.__isset.content_size_in_bytes ? delete_file.content_size_in_bytes : -1)
154
0
        << "}";
155
0
    return out.str();
156
0
}
157
158
static std::string iceberg_delete_files_debug_string(
159
0
        const std::vector<TIcebergDeleteFileDesc>& delete_files) {
160
0
    std::ostringstream out;
161
0
    out << "[";
162
0
    for (size_t idx = 0; idx < delete_files.size(); ++idx) {
163
0
        if (idx > 0) {
164
0
            out << ", ";
165
0
        }
166
0
        out << iceberg_delete_file_debug_string(delete_files[idx]);
167
0
    }
168
0
    out << "]";
169
0
    return out.str();
170
0
}
171
172
0
static std::string iceberg_params_debug_string(const std::optional<TIcebergFileDesc>& params) {
173
0
    if (!params.has_value()) {
174
0
        return "null";
175
0
    }
176
0
    const auto& iceberg_params = *params;
177
0
    std::ostringstream out;
178
0
    out << "TIcebergFileDesc{format_version="
179
0
        << (iceberg_params.__isset.format_version ? iceberg_params.format_version : -1)
180
0
        << ", content=" << (iceberg_params.__isset.content ? iceberg_params.content : -1)
181
0
        << ", original_file_path="
182
0
        << (iceberg_params.__isset.original_file_path ? iceberg_params.original_file_path : "null")
183
0
        << ", row_count=" << (iceberg_params.__isset.row_count ? iceberg_params.row_count : -1)
184
0
        << ", partition_spec_id="
185
0
        << (iceberg_params.__isset.partition_spec_id ? iceberg_params.partition_spec_id : 0)
186
0
        << ", has_partition_data_json=" << iceberg_params.__isset.partition_data_json
187
0
        << ", first_row_id="
188
0
        << (iceberg_params.__isset.first_row_id ? iceberg_params.first_row_id : -1)
189
0
        << ", last_updated_sequence_number="
190
0
        << (iceberg_params.__isset.last_updated_sequence_number
191
0
                    ? iceberg_params.last_updated_sequence_number
192
0
                    : -1)
193
0
        << ", delete_file_count="
194
0
        << (iceberg_params.__isset.delete_files ? iceberg_params.delete_files.size() : 0)
195
0
        << ", delete_files="
196
0
        << (iceberg_params.__isset.delete_files
197
0
                    ? iceberg_delete_files_debug_string(iceberg_params.delete_files)
198
0
                    : "[]")
199
0
        << ", has_serialized_split=" << iceberg_params.__isset.serialized_split << "}";
200
0
    return out.str();
201
0
}
202
203
IcebergTableReader::PositionDeleteRowsCollector::PositionDeleteRowsCollector(
204
        PositionDeleteFile* rows_by_data_file)
205
12
        : _rows_by_data_file(rows_by_data_file) {
206
12
    DORIS_CHECK(_rows_by_data_file != nullptr);
207
12
}
208
209
Status IcebergTableReader::PositionDeleteRowsCollector::collect(const Block& block,
210
22
                                                                size_t read_rows) {
211
22
    if (read_rows == 0) {
212
10
        return Status::OK();
213
10
    }
214
12
    const auto& file_path_column_ptr =
215
12
            block.get_by_position(ICEBERG_FILE_PATH_BLOCK_POSITION).column;
216
12
    const auto& pos_column_ptr = block.get_by_position(ICEBERG_ROW_POS_BLOCK_POSITION).column;
217
12
    if (const auto* nullable_column = check_and_get_column<ColumnNullable>(*file_path_column_ptr);
218
12
        nullable_column != nullptr && nullable_column->has_null(0, read_rows)) {
219
1
        return Status::Corruption("Iceberg position delete column file_path contains null values");
220
1
    }
221
11
    if (const auto* nullable_column = check_and_get_column<ColumnNullable>(*pos_column_ptr);
222
11
        nullable_column != nullptr && nullable_column->has_null(0, read_rows)) {
223
1
        return Status::Corruption("Iceberg position delete column pos contains null values");
224
1
    }
225
10
    const auto& file_path_column =
226
10
            assert_cast<const ColumnString&>(*remove_nullable(file_path_column_ptr));
227
10
    const auto& pos_column = assert_cast<const ColumnInt64&>(*remove_nullable(pos_column_ptr));
228
26
    for (size_t row = 0; row < read_rows; ++row) {
229
16
        const auto file_path = file_path_column.get_data_at(row).to_string();
230
16
        (*_rows_by_data_file)[file_path].push_back(pos_column.get_element(row));
231
16
    }
232
10
    return Status::OK();
233
11
}
234
235
55
Status IcebergTableReader::prepare_split(const format::SplitReadOptions& options) {
236
55
    {
237
55
        SCOPED_TIMER(_profile.total_timer);
238
55
        SCOPED_TIMER(_profile.prepare_split_timer);
239
55
        _row_lineage_columns = {};
240
55
        _iceberg_params.reset();
241
55
        _delete_predicates_initialized = false;
242
55
        _position_delete_rows_storage.clear();
243
55
        _equality_delete_filters.clear();
244
55
        _split_cache = options.cache;
245
55
        if (options.current_range.__isset.table_format_params &&
246
55
            options.current_range.table_format_params.__isset.iceberg_params) {
247
53
            const auto& iceberg_params = options.current_range.table_format_params.iceberg_params;
248
53
            _iceberg_params = iceberg_params;
249
53
            if (iceberg_params.__isset.first_row_id) {
250
9
                _row_lineage_columns.first_row_id = iceberg_params.first_row_id;
251
9
            }
252
53
            if (iceberg_params.__isset.last_updated_sequence_number) {
253
6
                _row_lineage_columns.last_updated_sequence_number =
254
6
                        iceberg_params.last_updated_sequence_number;
255
6
            }
256
53
        }
257
55
    }
258
55
    RETURN_IF_ERROR(TableReader::prepare_split(options));
259
52
    SCOPED_TIMER(_profile.total_timer);
260
52
    SCOPED_TIMER(_profile.prepare_split_timer);
261
52
    if (current_split_pruned()) {
262
0
        return Status::OK();
263
0
    }
264
    // Iceberg data files are immutable once referenced by a snapshot; updates create new data files
265
    // at new paths instead of overwriting existing files. This lets the Parquet V2 reader use page
266
    // cache when the scan range does not carry an mtime, without extending V1's path::0 behavior to
267
    // mutable Hive/local files.
268
52
    mark_current_data_file_immutable();
269
52
    if (_is_table_level_count_active()) {
270
1
        return Status::OK();
271
1
    }
272
51
    DBUG_EXECUTE_IF("IcebergTableReader.prepare_split.before_delete_file_scan",
273
51
                    DBUG_RUN_CALLBACK());
274
51
    RETURN_IF_ERROR(_init_delete_predicates(options.current_range.table_format_params));
275
49
    return Status::OK();
276
51
}
277
278
0
std::string IcebergTableReader::debug_string() const {
279
0
    size_t position_delete_file_count = 0;
280
0
    size_t equality_delete_file_count = 0;
281
0
    size_t deletion_vector_file_count = 0;
282
0
    if (_iceberg_params.has_value() && _iceberg_params->__isset.delete_files) {
283
0
        for (const auto& delete_file : _iceberg_params->delete_files) {
284
0
            if (!delete_file.__isset.content) {
285
0
                continue;
286
0
            }
287
0
            if (delete_file.content == POSITION_DELETE) {
288
0
                ++position_delete_file_count;
289
0
            } else if (delete_file.content == EQUALITY_DELETE) {
290
0
                ++equality_delete_file_count;
291
0
            } else if (delete_file.content == DELETION_VECTOR) {
292
0
                ++deletion_vector_file_count;
293
0
            }
294
0
        }
295
0
    }
296
297
0
    std::ostringstream equality_filters;
298
0
    equality_filters << "[";
299
0
    for (size_t idx = 0; idx < _equality_delete_filters.size(); ++idx) {
300
0
        if (idx > 0) {
301
0
            equality_filters << ", ";
302
0
        }
303
0
        const auto& filter = _equality_delete_filters[idx];
304
0
        equality_filters << "EqualityDeleteFilter{field_ids="
305
0
                         << join_values_for_debug(filter.field_ids) << ", key_types=[";
306
0
        for (size_t type_idx = 0; type_idx < filter.key_types.size(); ++type_idx) {
307
0
            if (type_idx > 0) {
308
0
                equality_filters << ", ";
309
0
            }
310
0
            equality_filters << (filter.key_types[type_idx] == nullptr
311
0
                                         ? "null"
312
0
                                         : filter.key_types[type_idx]->get_name());
313
0
        }
314
0
        equality_filters << "], delete_block_rows=" << filter.delete_block.rows()
315
0
                         << ", delete_block_columns=" << filter.delete_block.columns() << "}";
316
0
    }
317
0
    equality_filters << "]";
318
319
0
    std::ostringstream out;
320
0
    out << "IcebergTableReader{base=" << format::TableReader::debug_string()
321
0
        << ", iceberg_params=" << iceberg_params_debug_string(_iceberg_params)
322
0
        << ", row_lineage_first_row_id=" << _row_lineage_columns.first_row_id
323
0
        << ", row_lineage_last_updated_sequence_number="
324
0
        << _row_lineage_columns.last_updated_sequence_number
325
0
        << ", need_row_lineage_row_id=" << _need_row_lineage_row_id()
326
0
        << ", need_iceberg_rowid=" << _need_iceberg_rowid()
327
0
        << ", row_position_block_position=" << _row_position_block_position
328
0
        << ", delete_predicates_initialized=" << _delete_predicates_initialized
329
0
        << ", position_delete_file_count=" << position_delete_file_count
330
0
        << ", equality_delete_file_count=" << equality_delete_file_count
331
0
        << ", deletion_vector_file_count=" << deletion_vector_file_count
332
0
        << ", position_delete_rows_storage_count=" << _position_delete_rows_storage.size()
333
0
        << ", equality_delete_filter_count=" << _equality_delete_filters.size()
334
0
        << ", equality_delete_filters=" << equality_filters.str() << "}";
335
0
    return out.str();
336
0
}
337
338
41
Status IcebergTableReader::materialize_virtual_columns(Block* table_block) {
339
100
    for (size_t column_idx = 0; column_idx < _data_reader.column_mapper->mappings().size();
340
59
         ++column_idx) {
341
59
        const auto& mapping = _data_reader.column_mapper->mappings()[column_idx];
342
59
        switch (mapping.virtual_column_type) {
343
9
        case format::TableVirtualColumnType::ROW_ID:
344
9
            RETURN_IF_ERROR(_materialize_row_lineage_row_id(table_block, column_idx));
345
9
            break;
346
9
        case format::TableVirtualColumnType::LAST_UPDATED_SEQUENCE_NUMBER:
347
8
            RETURN_IF_ERROR(
348
8
                    _materialize_row_lineage_last_updated_sequence_number(table_block, column_idx));
349
8
            break;
350
8
        case format::TableVirtualColumnType::ICEBERG_ROWID:
351
1
            RETURN_IF_ERROR(_materialize_iceberg_rowid(table_block, column_idx));
352
1
            break;
353
41
        case format::TableVirtualColumnType::INVALID:
354
41
            break;
355
59
        }
356
59
    }
357
41
    return Status::OK();
358
41
}
359
360
48
Status IcebergTableReader::customize_file_scan_request(format::FileScanRequest* file_request) {
361
48
    RETURN_IF_ERROR(TableReader::customize_file_scan_request(file_request));
362
48
    if ((_row_lineage_columns.first_row_id >= 0 && _need_row_lineage_row_id()) ||
363
48
        _need_iceberg_rowid()) {
364
9
        RETURN_IF_ERROR(_append_row_position_output_column(file_request));
365
9
    }
366
48
    RETURN_IF_ERROR(_append_equality_delete_predicates(file_request));
367
48
    return Status::OK();
368
48
}
369
370
47
bool IcebergTableReader::_supports_aggregate_pushdown(TPushAggOp::type agg_type) const {
371
47
    if (!TableReader::_supports_aggregate_pushdown(agg_type)) {
372
47
        return false;
373
47
    }
374
0
    return _equality_delete_filters.empty();
375
47
}
376
377
Status IcebergTableReader::_parse_deletion_vector_file(const TTableFormatFileDesc& t_desc,
378
                                                       DeleteFileDesc* desc,
379
62
                                                       bool* has_delete_file) {
380
62
    DORIS_CHECK(desc != nullptr);
381
62
    DORIS_CHECK(has_delete_file != nullptr);
382
62
    *has_delete_file = false;
383
62
    if (!t_desc.__isset.iceberg_params) {
384
2
        return Status::OK();
385
2
    }
386
60
    const auto& iceberg_params = t_desc.iceberg_params;
387
60
    if (!iceberg_params.__isset.format_version ||
388
60
        iceberg_params.format_version < MIN_SUPPORT_DELETE_FILES_VERSION ||
389
60
        !iceberg_params.__isset.delete_files || iceberg_params.delete_files.empty()) {
390
16
        return Status::OK();
391
16
    }
392
393
44
    const TIcebergDeleteFileDesc* deletion_vector = nullptr;
394
46
    for (const auto& delete_file : iceberg_params.delete_files) {
395
46
        if (!delete_file.__isset.content || delete_file.content != DELETION_VECTOR) {
396
31
            continue;
397
31
        }
398
15
        if (deletion_vector != nullptr) {
399
1
            return Status::DataQualityError("This iceberg data file has multiple DVs.");
400
1
        }
401
14
        deletion_vector = &delete_file;
402
14
    }
403
43
    if (deletion_vector == nullptr) {
404
30
        return Status::OK();
405
30
    }
406
13
    size_t bytes_read = 0;
407
13
    RETURN_IF_ERROR(validate_iceberg_deletion_vector_descriptor(*deletion_vector, bytes_read));
408
409
11
    const std::string data_file_path = iceberg_params.__isset.original_file_path
410
11
                                               ? iceberg_params.original_file_path
411
11
                                               : _data_file_path();
412
11
    desc->key = build_iceberg_deletion_vector_cache_key(data_file_path, *deletion_vector);
413
11
    desc->path = deletion_vector->path;
414
11
    desc->start_offset = deletion_vector->content_offset;
415
11
    desc->size = static_cast<int64_t>(bytes_read);
416
11
    desc->file_size = -1;
417
11
    desc->format = DeleteFileDesc::Format::ICEBERG;
418
11
    *has_delete_file = true;
419
11
    return Status::OK();
420
13
}
421
422
51
Status IcebergTableReader::_init_delete_predicates(const TTableFormatFileDesc& t_desc) {
423
51
    if (!t_desc.__isset.iceberg_params || _delete_predicates_initialized) {
424
2
        _delete_predicates_initialized = true;
425
2
        return Status::OK();
426
2
    }
427
49
    const auto& iceberg_params = t_desc.iceberg_params;
428
49
    if (!iceberg_params.__isset.format_version ||
429
49
        iceberg_params.format_version < MIN_SUPPORT_DELETE_FILES_VERSION ||
430
49
        !iceberg_params.__isset.delete_files || iceberg_params.delete_files.empty()) {
431
16
        _delete_predicates_initialized = true;
432
16
        return Status::OK();
433
16
    }
434
435
33
    std::vector<TIcebergDeleteFileDesc> position_delete_files;
436
33
    std::vector<TIcebergDeleteFileDesc> equality_delete_files;
437
34
    for (const auto& delete_file : iceberg_params.delete_files) {
438
34
        if (!delete_file.__isset.content) {
439
0
            continue;
440
0
        }
441
34
        if (delete_file.content == POSITION_DELETE) {
442
14
            position_delete_files.push_back(delete_file);
443
20
        } else if (delete_file.content == EQUALITY_DELETE) {
444
17
            equality_delete_files.push_back(delete_file);
445
17
        }
446
34
    }
447
    // Per Iceberg scan planning, position delete files apply only when there is no deletion vector
448
    // for the data file. DVs and position deletes now intentionally use different in-memory
449
    // representations, so use the Roaring pointer as the DV sentinel.
450
33
    if (_deletion_vector != nullptr) {
451
3
        position_delete_files.clear();
452
3
    }
453
    // Initialize position and equality delete predicates. Position delete files contain row
454
    // positions of deleted rows, which can be directly added to `_delete_rows`. Equality delete
455
    // files contain values of deleted rows, which require reading the files and building
456
    // predicates for later filtering.
457
33
    if (!position_delete_files.empty()) {
458
13
        RETURN_IF_ERROR(_init_position_delete_rows(position_delete_files));
459
13
    }
460
31
    if (!equality_delete_files.empty()) {
461
17
        RETURN_IF_ERROR(_init_equality_delete_predicates(equality_delete_files));
462
17
    }
463
464
31
    _delete_predicates_initialized = true;
465
31
    return Status::OK();
466
31
}
467
468
std::shared_ptr<io::FileSystemProperties> IcebergTableReader::_delete_file_system_properties(
469
28
        const TFileScanRangeParams& scan_params) {
470
28
    auto system_properties = std::make_shared<io::FileSystemProperties>();
471
28
    system_properties->system_type =
472
28
            scan_params.__isset.file_type ? scan_params.file_type : TFileType::FILE_LOCAL;
473
28
    system_properties->properties = scan_params.properties;
474
28
    system_properties->hdfs_params = scan_params.hdfs_params;
475
28
    if (scan_params.__isset.broker_addresses) {
476
0
        system_properties->broker_addresses.assign(scan_params.broker_addresses.begin(),
477
0
                                                   scan_params.broker_addresses.end());
478
0
    }
479
28
    return system_properties;
480
28
}
481
482
std::unique_ptr<io::FileDescription> IcebergTableReader::_delete_file_description(
483
28
        const TFileRangeDesc& range) {
484
28
    auto file_description = std::make_unique<io::FileDescription>();
485
28
    file_description->path = range.path;
486
28
    file_description->file_size = range.__isset.file_size ? range.file_size : -1;
487
28
    file_description->range_start_offset = range.__isset.start_offset ? range.start_offset : 0;
488
28
    file_description->range_size = range.__isset.size ? range.size : -1;
489
    // Iceberg delete files follow the same immutable-file contract as data files: a snapshot
490
    // references a fixed object and later changes publish a new file rather than replacing it.
491
28
    file_description->is_immutable = true;
492
28
    if (range.__isset.fs_name) {
493
4
        file_description->fs_name = range.fs_name;
494
4
    }
495
28
    return file_description;
496
28
}
497
498
12
std::string IcebergTableReader::_data_file_path() const {
499
12
    if (_iceberg_params.has_value() && _iceberg_params->__isset.original_file_path) {
500
11
        return _iceberg_params->original_file_path;
501
11
    }
502
1
    DORIS_CHECK(_current_task != nullptr);
503
1
    DORIS_CHECK(_current_task->data_file != nullptr);
504
1
    return _current_task->data_file->path;
505
12
}
506
507
9
Status IcebergTableReader::_append_row_position_output_column(format::FileScanRequest* request) {
508
9
    const auto row_position_column_id = format::LocalColumnId(format::ROW_POSITION_COLUMN_ID);
509
9
    _append_file_scan_column(request, row_position_column_id, &request->non_predicate_columns);
510
9
    _row_position_block_position = request->local_positions.at(row_position_column_id).value();
511
9
    return Status::OK();
512
9
}
513
514
const format::ColumnDefinition* IcebergTableReader::_find_equality_delete_data_field(
515
17
        const EqualityDeleteFilter& filter, size_t key_idx) const {
516
17
    DORIS_CHECK(key_idx < filter.field_ids.size());
517
17
    DORIS_CHECK(key_idx < filter.field_names.size());
518
17
    if (mapping_mode() != format::TableColumnMappingMode::BY_NAME) {
519
14
        const int field_id = filter.field_ids[key_idx];
520
14
        const auto field_it = std::ranges::find_if(
521
15
                _data_reader.file_schema, [field_id](const format::ColumnDefinition& field) {
522
15
                    return field.has_identifier_field_id() &&
523
15
                           field.get_identifier_field_id() == field_id;
524
15
                });
525
14
        return field_it == _data_reader.file_schema.end() ? nullptr : &*field_it;
526
14
    }
527
528
    // Equality keys are hidden scan dependencies and need not appear in the query projection.
529
    // Resolve their current name and aliases from the full table schema supplied by FE, falling
530
    // back to the delete-file name when history metadata is unavailable. Reuse ColumnMapper's
531
    // exact BY_NAME rules so case, string identifiers, and aliases on either side stay consistent.
532
3
    auto table_field = _find_equality_delete_table_field(filter, key_idx);
533
3
    return format::find_column_by_name(*table_field, _data_reader.file_schema);
534
17
}
535
536
std::optional<format::ColumnDefinition> IcebergTableReader::_find_equality_delete_table_field(
537
10
        const EqualityDeleteFilter& filter, size_t key_idx) const {
538
10
    DORIS_CHECK(key_idx < filter.field_ids.size());
539
10
    DORIS_CHECK(key_idx < filter.field_names.size());
540
10
    const int field_id = filter.field_ids[key_idx];
541
10
    auto table_field = _find_current_table_column_by_field_id(field_id, filter.key_types[key_idx]);
542
10
    if (!table_field.has_value()) {
543
4
        const auto projected_field = std::ranges::find_if(
544
4
                _projected_columns, [field_id](const format::ColumnDefinition& field) {
545
4
                    return field.has_identifier_field_id() &&
546
4
                           field.get_identifier_field_id() == field_id;
547
4
                });
548
4
        if (projected_field != _projected_columns.end()) {
549
            // Older scan descriptors and focused unit tests may omit history_schema_info. Keep the
550
            // projected metadata as a compatibility fallback, but never require projection when
551
            // the complete current schema is available.
552
1
            table_field = *projected_field;
553
1
        }
554
4
    }
555
10
    if (!table_field.has_value()) {
556
3
        table_field = format::ColumnDefinition {
557
3
                .identifier = {},
558
3
                .name = filter.field_names[key_idx],
559
3
                .type = filter.key_types[key_idx],
560
3
        };
561
3
    }
562
10
    return table_field;
563
10
}
564
565
std::string IcebergTableReader::_delete_file_cache_key(const char* prefix,
566
30
                                                       const std::string& path) const {
567
30
    DORIS_CHECK(prefix != nullptr);
568
30
    std::string fs_name;
569
30
    if (_current_task != nullptr && _current_task->data_file != nullptr) {
570
30
        fs_name = _current_task->data_file->fs_name;
571
30
    }
572
    // Delete descriptors can reuse the same path text in different filesystem namespaces. Encode
573
    // both variable-length strings so neither an fs/path boundary nor equality field-id suffixes
574
    // can be reinterpreted as path content; scan-level credentials/properties are shared here.
575
30
    std::ostringstream key;
576
30
    key << prefix << fs_name.size() << ':' << fs_name << ':' << path.size() << ':' << path;
577
30
    return key.str();
578
30
}
579
580
void IcebergTableReader::_append_equality_delete_row_count_carrier(
581
7
        format::FileScanRequest* request) {
582
7
    DORIS_CHECK(request != nullptr);
583
    // Columnar readers establish a filter batch's row count from predicate columns. If all
584
    // equality keys are missing, the predicate consists only of NULL literals and the filter block
585
    // would otherwise have zero rows. Use the virtual row-position column as the carrier instead
586
    // of an arbitrary physical column. For example, a data file may start with an unsupported
587
    // TIME_MILLIS leaf while the query projects only a supported `id`; selecting that TIME leaf as
588
    // a hidden carrier would make Parquet reject a column the query never requested. Row position
589
    // has one value per input row in both Parquet and ORC, is already used by delete predicates,
590
    // and is explicitly excluded from physical logical-type validation.
591
7
    _append_file_scan_column(request, format::LocalColumnId(format::ROW_POSITION_COLUMN_ID),
592
7
                             &request->predicate_columns);
593
7
}
594
595
48
Status IcebergTableReader::_append_equality_delete_predicates(format::FileScanRequest* request) {
596
48
    DORIS_CHECK(request != nullptr);
597
48
    for (const auto& filter : _equality_delete_filters) {
598
17
        auto delete_predicate =
599
17
                std::make_shared<EqualityDeletePredicate>(filter.delete_block, filter.field_ids);
600
17
        DCHECK_EQ(filter.field_ids.size(), filter.key_types.size());
601
17
        bool has_missing_key = false;
602
34
        for (size_t idx = 0; idx < filter.field_ids.size(); ++idx) {
603
17
            const auto* field = _find_equality_delete_data_field(filter, idx);
604
17
            if (field == nullptr) {
605
7
                auto table_field = _find_equality_delete_table_field(filter, idx);
606
7
                DORIS_CHECK(table_field.has_value());
607
7
                VExprSPtr key_expr;
608
7
                RETURN_IF_ERROR(build_missing_equality_delete_key_expr(
609
7
                        *table_field, filter.key_types[idx], &key_expr));
610
7
                delete_predicate->add_child(key_expr);
611
7
                has_missing_key = true;
612
7
                continue;
613
7
            }
614
10
            const auto field_column_id = format::LocalColumnId(field->file_local_id());
615
10
            _append_file_scan_column(request, field_column_id, &request->predicate_columns);
616
10
            const auto block_position = request->local_positions.at(field_column_id).value();
617
10
            auto slot = VSlotRef::create_shared(cast_set<int>(block_position),
618
10
                                                cast_set<int>(block_position), -1, field->type,
619
10
                                                field->name);
620
10
            if (field->type->equals(*filter.key_types[idx])) {
621
9
                delete_predicate->add_child(std::move(slot));
622
9
            } else {
623
1
                auto cast_expr = Cast::create_shared(filter.key_types[idx]);
624
1
                cast_expr->add_child(std::move(slot));
625
1
                delete_predicate->add_child(std::move(cast_expr));
626
1
            }
627
10
        }
628
17
        if (has_missing_key && request->predicate_columns.empty()) {
629
7
            _append_equality_delete_row_count_carrier(request);
630
7
        }
631
17
        request->delete_conjuncts.push_back(
632
17
                VExprContext::create_shared(std::move(delete_predicate)));
633
17
    }
634
48
    return Status::OK();
635
48
}
636
637
Status IcebergTableReader::_create_delete_file_reader(const TIcebergDeleteFileDesc& delete_file,
638
                                                      const TFileScanRangeParams& scan_params,
639
                                                      IcebergDeleteFileIOContext* delete_io_ctx,
640
28
                                                      std::unique_ptr<format::FileReader>* reader) {
641
28
    DORIS_CHECK(delete_io_ctx != nullptr);
642
28
    DORIS_CHECK(reader != nullptr);
643
28
    if (!delete_file.__isset.file_format) {
644
0
        return Status::InternalError("Iceberg delete file is missing file format");
645
0
    }
646
28
    if (delete_file.file_format != TFileFormatType::FORMAT_PARQUET &&
647
28
        delete_file.file_format != TFileFormatType::FORMAT_ORC) {
648
0
        return Status::NotSupported("Unsupported Iceberg delete file format {}",
649
0
                                    delete_file.file_format);
650
0
    }
651
28
    auto delete_range = build_iceberg_delete_file_range(delete_file.path);
652
28
    if (_current_task != nullptr && _current_task->data_file != nullptr &&
653
28
        !_current_task->data_file->fs_name.empty()) {
654
4
        delete_range.__set_fs_name(_current_task->data_file->fs_name);
655
4
    }
656
28
    auto system_properties = _delete_file_system_properties(scan_params);
657
28
    auto file_description = _delete_file_description(delete_range);
658
28
    std::shared_ptr<io::IOContext> io_ctx(&delete_io_ctx->io_ctx, [](io::IOContext*) {});
659
28
    const bool enable_mapping_timestamp_tz = scan_params.__isset.enable_mapping_timestamp_tz &&
660
28
                                             scan_params.enable_mapping_timestamp_tz;
661
28
    const bool enable_mapping_varbinary =
662
28
            scan_params.__isset.enable_mapping_varbinary && scan_params.enable_mapping_varbinary;
663
28
    if (delete_file.file_format == TFileFormatType::FORMAT_PARQUET) {
664
        // Delete and data files must parse raw binary fields with the same scan-level mapping.
665
28
        *reader = std::make_unique<format::parquet::ParquetReader>(
666
28
                system_properties, file_description, io_ctx, _scanner_profile, std::nullopt,
667
28
                enable_mapping_timestamp_tz, enable_mapping_varbinary);
668
28
    } else {
669
0
        *reader = std::make_unique<format::orc::OrcReader>(system_properties, file_description,
670
0
                                                           io_ctx, _scanner_profile, std::nullopt,
671
0
                                                           enable_mapping_timestamp_tz);
672
0
    }
673
28
    RETURN_IF_ERROR((*reader)->init(_runtime_state));
674
28
    return Status::OK();
675
28
}
676
677
Status IcebergTableReader::_read_position_delete_file(const TIcebergDeleteFileDesc& delete_file,
678
                                                      const TFileScanRangeParams& scan_params,
679
                                                      IcebergDeleteFileIOContext* delete_io_ctx,
680
12
                                                      PositionDeleteRowsCollector* collector) {
681
12
    DORIS_CHECK(collector != nullptr);
682
12
    std::unique_ptr<format::FileReader> reader;
683
12
    RETURN_IF_ERROR(_create_delete_file_reader(delete_file, scan_params, delete_io_ctx, &reader));
684
12
    DORIS_CHECK(reader != nullptr);
685
686
12
    std::vector<format::ColumnDefinition> schema;
687
12
    RETURN_IF_ERROR(reader->get_schema(&schema));
688
12
    format::ColumnDefinition* file_path_field = nullptr;
689
12
    format::ColumnDefinition* pos_field = nullptr;
690
24
    for (auto& field : schema) {
691
24
        if (field.name == ICEBERG_FILE_PATH) {
692
12
            file_path_field = &field;
693
12
        } else if (field.name == ICEBERG_ROW_POS) {
694
12
            pos_field = &field;
695
12
        }
696
24
    }
697
12
    if (file_path_field == nullptr || pos_field == nullptr) {
698
0
        return Status::InternalError("Position delete file is missing required columns");
699
0
    }
700
701
12
    auto request = std::make_shared<format::FileScanRequest>();
702
12
    request->non_predicate_columns = {
703
12
            format::LocalColumnIndex::top_level(
704
12
                    format::LocalColumnId(file_path_field->file_local_id())),
705
12
            format::LocalColumnIndex::top_level(format::LocalColumnId(pos_field->file_local_id()))};
706
12
    request->local_positions = {
707
12
            {format::LocalColumnId(file_path_field->file_local_id()),
708
12
             format::LocalIndex(ICEBERG_FILE_PATH_BLOCK_POSITION)},
709
12
            {format::LocalColumnId(pos_field->file_local_id()),
710
12
             format::LocalIndex(ICEBERG_ROW_POS_BLOCK_POSITION)},
711
12
    };
712
12
    RETURN_IF_ERROR(reader->open(request));
713
714
12
    bool eof = false;
715
12
    auto build_position_delete_block = [](const format::ColumnDefinition& file_path_field,
716
22
                                          const format::ColumnDefinition& pos_field) -> Block {
717
22
        Block block;
718
22
        block.insert(
719
22
                {file_path_field.type->create_column(), file_path_field.type, ICEBERG_FILE_PATH});
720
22
        block.insert({pos_field.type->create_column(), pos_field.type, ICEBERG_ROW_POS});
721
22
        return block;
722
22
    };
723
32
    while (!eof) {
724
22
        Block block = build_position_delete_block(*file_path_field, *pos_field);
725
22
        size_t read_rows = 0;
726
22
        RETURN_IF_ERROR(reader->get_block(&block, &read_rows, &eof));
727
22
        RETURN_IF_ERROR(collector->collect(block, read_rows));
728
22
    }
729
10
    return reader->close();
730
12
}
731
732
Status IcebergTableReader::_init_position_delete_rows(
733
13
        const std::vector<TIcebergDeleteFileDesc>& delete_files) {
734
13
    DORIS_CHECK(_split_cache != nullptr);
735
13
    TFileScanRangeParams delete_scan_params =
736
13
            _scan_params == nullptr ? TFileScanRangeParams() : *_scan_params;
737
13
    format::DeleteRows position_delete_rows;
738
13
    IcebergDeleteFileIOContext delete_io_ctx(_runtime_state);
739
13
    for (const auto& delete_file : delete_files) {
740
13
        Status read_status = Status::OK();
741
        // A position delete file normally references many data files. Cache the complete
742
        // path-to-position map once; caching only the current data file would still rescan the
743
        // shared delete file for every subsequent split.
744
13
        auto* rows_by_data_file =
745
13
                _split_cache->get<PositionDeleteRowsCollector::PositionDeleteFile>(
746
13
                        _delete_file_cache_key("iceberg_v2_position_delete_", delete_file.path),
747
13
                        [&]() -> PositionDeleteRowsCollector::PositionDeleteFile* {
748
12
                            auto result = std::make_unique<
749
12
                                    PositionDeleteRowsCollector::PositionDeleteFile>();
750
12
                            PositionDeleteRowsCollector collector(result.get());
751
12
                            read_status = _read_position_delete_file(
752
12
                                    delete_file, delete_scan_params, &delete_io_ctx, &collector);
753
12
                            if (!read_status.ok()) {
754
2
                                return nullptr;
755
2
                            }
756
12
                            for (auto& [_, rows] : *result) {
757
12
                                std::ranges::sort(rows);
758
12
                            }
759
10
                            return result.release();
760
12
                        });
761
13
        RETURN_IF_ERROR(read_status);
762
11
        DORIS_CHECK(rows_by_data_file != nullptr);
763
11
        const auto rows_it = rows_by_data_file->find(_data_file_path());
764
11
        if (rows_it == rows_by_data_file->end()) {
765
0
            continue;
766
0
        }
767
11
        auto first = rows_it->second.begin();
768
11
        auto last = rows_it->second.end();
769
        // Bounds are inclusive Iceberg position statistics supplied by FE. Apply them after the
770
        // cached per-data-file vector is sorted so irrelevant positions are sliced without a scan.
771
11
        if (delete_file.__isset.position_lower_bound) {
772
1
            first = std::lower_bound(first, last, delete_file.position_lower_bound);
773
1
        }
774
11
        if (delete_file.__isset.position_upper_bound) {
775
1
            last = std::upper_bound(first, last, delete_file.position_upper_bound);
776
1
        }
777
11
        position_delete_rows.insert(position_delete_rows.end(), first, last);
778
11
    }
779
11
    if (position_delete_rows.empty()) {
780
0
        return Status::OK();
781
0
    }
782
    // Position delete files and deletion vectors both become row-position deletes for the
783
    // common TableReader DeletePredicate path. Keep the merged rows in a member vector because
784
    // DeletePredicate stores a reference to the vector used by _delete_rows.
785
11
    _position_delete_rows_storage.insert(_position_delete_rows_storage.end(),
786
11
                                         position_delete_rows.begin(), position_delete_rows.end());
787
11
    std::sort(_position_delete_rows_storage.begin(), _position_delete_rows_storage.end());
788
11
    _position_delete_rows_storage.erase(
789
11
            std::unique(_position_delete_rows_storage.begin(), _position_delete_rows_storage.end()),
790
11
            _position_delete_rows_storage.end());
791
11
    _delete_rows = &_position_delete_rows_storage;
792
11
    return Status::OK();
793
11
}
794
795
Status IcebergTableReader::_init_equality_delete_predicates(
796
17
        const std::vector<TIcebergDeleteFileDesc>& delete_files) {
797
17
    DORIS_CHECK(_split_cache != nullptr);
798
17
    TFileScanRangeParams delete_scan_params =
799
17
            _scan_params == nullptr ? TFileScanRangeParams() : *_scan_params;
800
17
    IcebergDeleteFileIOContext delete_io_ctx(_runtime_state);
801
17
    for (const auto& delete_file : delete_files) {
802
17
        RETURN_IF_ERROR(
803
17
                _read_equality_delete_file(delete_file, delete_scan_params, &delete_io_ctx));
804
17
    }
805
17
    return Status::OK();
806
17
}
807
808
Status IcebergTableReader::_resolve_equality_delete_fields(
809
        const TIcebergDeleteFileDesc& delete_file,
810
        const std::vector<format::ColumnDefinition>& schema,
811
16
        std::vector<format::ColumnDefinition>* delete_fields, EqualityDeleteFilter* result) const {
812
16
    DORIS_CHECK(delete_fields != nullptr);
813
16
    DORIS_CHECK(result != nullptr);
814
16
    for (const auto field_id : delete_file.field_ids) {
815
16
        const auto field_it =
816
16
                std::ranges::find_if(schema, [field_id](const format::ColumnDefinition& field) {
817
16
                    return field.has_identifier_field_id() &&
818
16
                           field_id == field.get_identifier_field_id();
819
16
                });
820
16
        if (field_it == schema.end()) {
821
0
            return Status::InternalError("Can not find field id {} in equality delete file {}",
822
0
                                         field_id, delete_file.path);
823
0
        }
824
16
        if (!field_it->children.empty()) {
825
0
            return Status::NotSupported(
826
0
                    "Iceberg equality delete does not support complex column {}", field_it->name);
827
0
        }
828
16
        delete_fields->push_back(*field_it);
829
16
        result->field_ids.push_back(field_id);
830
16
        result->field_names.push_back(field_it->name);
831
16
        result->key_types.push_back(field_it->type);
832
16
    }
833
16
    return Status::OK();
834
16
}
835
836
Status IcebergTableReader::_load_equality_delete_file(const TIcebergDeleteFileDesc& delete_file,
837
                                                      const TFileScanRangeParams& scan_params,
838
                                                      IcebergDeleteFileIOContext* delete_io_ctx,
839
16
                                                      EqualityDeleteFilter* result) {
840
16
    DORIS_CHECK(result != nullptr);
841
16
    std::unique_ptr<format::FileReader> reader;
842
16
    RETURN_IF_ERROR(_create_delete_file_reader(delete_file, scan_params, delete_io_ctx, &reader));
843
16
    DORIS_CHECK(reader != nullptr);
844
845
16
    std::vector<format::ColumnDefinition> schema;
846
16
    RETURN_IF_ERROR(reader->get_schema(&schema));
847
16
    std::vector<format::ColumnDefinition> delete_fields;
848
16
    RETURN_IF_ERROR(_resolve_equality_delete_fields(delete_file, schema, &delete_fields, result));
849
850
16
    auto request = std::make_shared<format::FileScanRequest>();
851
16
    Block delete_block_template;
852
32
    for (size_t idx = 0; idx < delete_fields.size(); ++idx) {
853
16
        const auto& delete_field = delete_fields[idx];
854
16
        const auto local_column_id = format::LocalColumnId(delete_field.file_local_id());
855
16
        request->non_predicate_columns.push_back(
856
16
                format::LocalColumnIndex::top_level(local_column_id));
857
16
        request->local_positions.emplace(local_column_id, format::LocalIndex(idx));
858
16
        delete_block_template.insert(
859
16
                {delete_field.type->create_column(), delete_field.type, delete_field.name});
860
16
    }
861
16
    RETURN_IF_ERROR(reader->open(request));
862
863
16
    MutableBlock mutable_delete_block(delete_block_template.clone_empty());
864
16
    bool eof = false;
865
48
    while (!eof) {
866
32
        Block block = delete_block_template.clone_empty();
867
32
        size_t read_rows = 0;
868
32
        RETURN_IF_ERROR(reader->get_block(&block, &read_rows, &eof));
869
32
        if (read_rows > 0) {
870
16
            RETURN_IF_ERROR(mutable_delete_block.merge(block));
871
16
        }
872
32
    }
873
16
    RETURN_IF_ERROR(reader->close());
874
16
    result->delete_block = mutable_delete_block.to_block();
875
16
    return Status::OK();
876
16
}
877
878
Status IcebergTableReader::_read_equality_delete_file(const TIcebergDeleteFileDesc& delete_file,
879
                                                      const TFileScanRangeParams& scan_params,
880
17
                                                      IcebergDeleteFileIOContext* delete_io_ctx) {
881
17
    if (!delete_file.__isset.field_ids || delete_file.field_ids.empty()) {
882
0
        return Status::InternalError("Iceberg equality delete file is missing field ids");
883
0
    }
884
17
    std::ostringstream cache_key;
885
17
    cache_key << _delete_file_cache_key("iceberg_v2_equality_delete_", delete_file.path);
886
17
    cache_key << ':' << delete_file.field_ids.size();
887
17
    for (const auto field_id : delete_file.field_ids) {
888
17
        cache_key << ':' << field_id;
889
17
    }
890
17
    Status read_status = Status::OK();
891
    // Include the ordered equality ids in the key because the same physical delete file can be
892
    // projected with different key layouts. The cached block and its key metadata are immutable
893
    // after construction and therefore safe to copy into each split-local predicate.
894
17
    auto* cached_filter = _split_cache->get<EqualityDeleteFilter>(
895
17
            cache_key.str(), [&]() -> EqualityDeleteFilter* {
896
16
                auto result = std::make_unique<EqualityDeleteFilter>();
897
16
                read_status = _load_equality_delete_file(delete_file, scan_params, delete_io_ctx,
898
16
                                                         result.get());
899
16
                if (!read_status.ok()) {
900
0
                    return nullptr;
901
0
                }
902
16
                return result.release();
903
16
            });
904
17
    RETURN_IF_ERROR(read_status);
905
17
    DORIS_CHECK(cached_filter != nullptr);
906
17
    _equality_delete_filters.push_back(*cached_filter);
907
17
    return Status::OK();
908
17
}
909
910
9
Status IcebergTableReader::_materialize_row_lineage_row_id(Block* table_block, size_t column_idx) {
911
9
    if (_row_lineage_columns.first_row_id < 0) {
912
2
        return Status::OK();
913
2
    }
914
7
    DORIS_CHECK(_row_position_block_position < _data_reader.block_template.columns());
915
7
    const auto& row_position_column = assert_cast<const ColumnInt64&>(
916
7
            *_data_reader.block_template.get_by_position(_row_position_block_position).column);
917
7
    DORIS_CHECK(row_position_column.size() == table_block->rows());
918
7
    auto column = IColumn::mutate(
919
7
            table_block->get_by_position(column_idx).column->convert_to_full_column_if_const());
920
7
    auto* nullable_column = assert_cast<ColumnNullable*>(column.get());
921
7
    auto& null_map = nullable_column->get_null_map_data();
922
7
    auto& data = assert_cast<ColumnInt64&>(*nullable_column->get_nested_column_ptr()).get_data();
923
7
    DORIS_CHECK(null_map.size() == row_position_column.size());
924
7
    DORIS_CHECK(data.size() == row_position_column.size());
925
23
    for (size_t row = 0; row < row_position_column.size(); ++row) {
926
16
        if (null_map[row]) {
927
10
            null_map[row] = 0;
928
10
            data[row] = _row_lineage_columns.first_row_id + row_position_column.get_element(row);
929
10
        }
930
16
    }
931
7
    table_block->replace_by_position(column_idx, std::move(column));
932
7
    return Status::OK();
933
9
}
934
935
1
Status IcebergTableReader::_materialize_iceberg_rowid(Block* table_block, size_t column_idx) {
936
1
    DORIS_CHECK(_row_position_block_position < _data_reader.block_template.columns());
937
1
    const auto& row_position_column = assert_cast<const ColumnInt64&>(
938
1
            *_data_reader.block_template.get_by_position(_row_position_block_position).column);
939
1
    DORIS_CHECK(row_position_column.size() == table_block->rows());
940
941
1
    const auto& type = table_block->get_by_position(column_idx).type;
942
1
    auto column = type->create_column();
943
1
    auto* nullable_column = check_and_get_column<ColumnNullable>(column.get());
944
1
    auto* struct_column = nullable_column != nullptr
945
1
                                  ? check_and_get_column<ColumnStruct>(
946
1
                                            nullable_column->get_nested_column_ptr().get())
947
1
                                  : check_and_get_column<ColumnStruct>(column.get());
948
1
    DORIS_CHECK(struct_column != nullptr);
949
1
    DORIS_CHECK(struct_column->tuple_size() >= 4);
950
951
1
    const auto rows = row_position_column.size();
952
1
    const auto file_path = _data_file_path();
953
1
    const int32_t partition_spec_id =
954
1
            _iceberg_params.has_value() && _iceberg_params->__isset.partition_spec_id
955
1
                    ? _iceberg_params->partition_spec_id
956
1
                    : 0;
957
1
    const std::string partition_data_json =
958
1
            _iceberg_params.has_value() && _iceberg_params->__isset.partition_data_json
959
1
                    ? _iceberg_params->partition_data_json
960
1
                    : "";
961
962
1
    auto& file_path_column = struct_column->get_column(0);
963
1
    auto& row_pos_column = struct_column->get_column(1);
964
1
    auto& spec_id_column = struct_column->get_column(2);
965
1
    auto& partition_data_column = struct_column->get_column(3);
966
1
    file_path_column.reserve(rows);
967
1
    row_pos_column.reserve(rows);
968
1
    spec_id_column.reserve(rows);
969
1
    partition_data_column.reserve(rows);
970
3
    for (size_t row = 0; row < rows; ++row) {
971
2
        file_path_column.insert_data(file_path.data(), file_path.size());
972
2
        const int64_t row_pos = row_position_column.get_element(row);
973
2
        row_pos_column.insert_data(reinterpret_cast<const char*>(&row_pos), sizeof(row_pos));
974
2
        spec_id_column.insert_data(reinterpret_cast<const char*>(&partition_spec_id),
975
2
                                   sizeof(partition_spec_id));
976
2
        partition_data_column.insert_data(partition_data_json.data(), partition_data_json.size());
977
2
    }
978
1
    if (nullable_column != nullptr) {
979
1
        nullable_column->get_null_map_data().resize_fill(rows, 0);
980
1
    }
981
1
    table_block->replace_by_position(column_idx, std::move(column));
982
1
    return Status::OK();
983
1
}
984
985
Status IcebergTableReader::_materialize_row_lineage_last_updated_sequence_number(
986
8
        Block* table_block, size_t column_idx) {
987
8
    if (_row_lineage_columns.last_updated_sequence_number < 0) {
988
2
        return Status::OK();
989
2
    }
990
6
    auto column = IColumn::mutate(
991
6
            table_block->get_by_position(column_idx).column->convert_to_full_column_if_const());
992
6
    auto* nullable_column = assert_cast<ColumnNullable*>(column.get());
993
6
    auto& null_map = nullable_column->get_null_map_data();
994
6
    auto& data = assert_cast<ColumnInt64&>(*nullable_column->get_nested_column_ptr()).get_data();
995
6
    DORIS_CHECK(null_map.size() == table_block->rows());
996
6
    DORIS_CHECK(data.size() == table_block->rows());
997
20
    for (size_t row = 0; row < table_block->rows(); ++row) {
998
14
        if (null_map[row]) {
999
8
            null_map[row] = 0;
1000
8
            data[row] = _row_lineage_columns.last_updated_sequence_number;
1001
8
        }
1002
14
    }
1003
6
    table_block->replace_by_position(column_idx, std::move(column));
1004
6
    return Status::OK();
1005
8
}
1006
1007
8
bool IcebergTableReader::_need_row_lineage_row_id() const {
1008
8
    if (_data_reader.column_mapper != nullptr) {
1009
7
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
1010
7
            if (mapping.virtual_column_type == format::TableVirtualColumnType::ROW_ID) {
1011
7
                return true;
1012
7
            }
1013
7
        }
1014
7
    }
1015
1
    return std::ranges::any_of(_projected_columns, is_projected_row_lineage_row_id);
1016
8
}
1017
1018
40
bool IcebergTableReader::_need_iceberg_rowid() const {
1019
40
    if (_data_reader.column_mapper != nullptr) {
1020
44
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
1021
44
            if (mapping.virtual_column_type == format::TableVirtualColumnType::ICEBERG_ROWID) {
1022
1
                return true;
1023
1
            }
1024
44
        }
1025
40
    }
1026
39
    return std::ranges::any_of(_projected_columns, is_projected_iceberg_rowid);
1027
40
}
1028
1029
} // namespace doris::format::iceberg