Coverage Report

Created: 2026-08-04 17:23

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