Coverage Report

Created: 2026-08-07 11:59

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 <gen_cpp/Exprs_types.h>
21
#include <rapidjson/document.h>
22
#include <rapidjson/stringbuffer.h>
23
#include <rapidjson/writer.h>
24
25
#include <algorithm>
26
#include <deque>
27
#include <memory>
28
#include <sstream>
29
#include <string_view>
30
#include <utility>
31
32
#include "common/cast_set.h"
33
#include "common/consts.h"
34
#include "core/assert_cast.h"
35
#include "core/block/block.h"
36
#include "core/column/column_const.h"
37
#include "core/column/column_nullable.h"
38
#include "core/column/column_string.h"
39
#include "core/column/column_struct.h"
40
#include "core/column/column_vector.h"
41
#include "core/data_type/data_type_array.h"
42
#include "core/data_type/data_type_map.h"
43
#include "core/data_type/data_type_number.h"
44
#include "core/data_type/data_type_struct.h"
45
#include "core/data_type/define_primitive_type.h"
46
#include "core/field.h"
47
#include "exprs/vliteral.h"
48
#include "exprs/vslot_ref.h"
49
#include "format/table/deletion_vector_reader.h"
50
#include "format_v2/expr/cast.h"
51
#include "format_v2/expr/equality_delete_predicate.h"
52
#include "format_v2/orc/orc_reader.h"
53
#include "format_v2/parquet/parquet_reader.h"
54
#include "format_v2/parquet/reader/column_reader.h"
55
#include "format_v2/table_reader.h"
56
#include "io/file_factory.h"
57
#include "util/debug_points.h"
58
#include "util/string_util.h"
59
#include "util/url_coding.h"
60
61
namespace doris::format::iceberg {
62
63
static constexpr const char* ROW_LINEAGE_ROW_ID = "_row_id";
64
static constexpr int32_t ROW_LINEAGE_ROW_ID_FIELD_ID = 2147483540;
65
66
template <typename T>
67
0
static std::string join_values_for_debug(const std::vector<T>& values) {
68
0
    std::ostringstream out;
69
0
    out << "[";
70
0
    for (size_t idx = 0; idx < values.size(); ++idx) {
71
0
        if (idx > 0) {
72
0
            out << ", ";
73
0
        }
74
0
        out << values[idx];
75
0
    }
76
0
    out << "]";
77
0
    return out.str();
78
0
}
79
80
1
static bool is_projected_row_lineage_row_id(const format::ColumnDefinition& column) {
81
    // Iceberg row lineage columns can be bound by field id when a mapper has already been built,
82
    // but customize_file_scan_request() is also exercised directly by scan-request tests before the
83
    // mapper exists. In that path, inspect the projected table schema so row-position dependencies
84
    // are still added for `_row_id`.
85
1
    return column.name == ROW_LINEAGE_ROW_ID ||
86
1
           (column.has_identifier_field_id() &&
87
0
            column.get_identifier_field_id() == ROW_LINEAGE_ROW_ID_FIELD_ID);
88
1
}
89
90
67
static bool is_projected_iceberg_rowid(const format::ColumnDefinition& column) {
91
67
    return column.name == BeConsts::ICEBERG_ROWID_COL;
92
67
}
93
94
36
static int iceberg_hex_value(char value) {
95
36
    if (value >= '0' && value <= '9') {
96
24
        return value - '0';
97
24
    }
98
12
    if (value >= 'a' && value <= 'f') {
99
12
        return value - 'a' + 10;
100
12
    }
101
0
    if (value >= 'A' && value <= 'F') {
102
0
        return value - 'A' + 10;
103
0
    }
104
0
    return -1;
105
0
}
106
107
1
static Status decode_iceberg_hex(std::string_view encoded, std::string* decoded) {
108
1
    DORIS_CHECK(decoded != nullptr);
109
1
    if ((encoded.size() & 1U) != 0) {
110
0
        return Status::InvalidArgument("Invalid odd-length Iceberg binary default");
111
0
    }
112
1
    decoded->resize(encoded.size() / 2);
113
19
    for (size_t index = 0; index < encoded.size(); index += 2) {
114
18
        const int high = iceberg_hex_value(encoded[index]);
115
18
        const int low = iceberg_hex_value(encoded[index + 1]);
116
18
        if (high < 0 || low < 0) {
117
0
            return Status::InvalidArgument("Invalid hexadecimal Iceberg binary default");
118
0
        }
119
18
        (*decoded)[index / 2] = static_cast<char>((high << 4) | low);
120
18
    }
121
1
    return Status::OK();
122
1
}
123
124
1
static Status decode_iceberg_json_binary(std::string_view encoded, std::string* decoded) {
125
1
    DORIS_CHECK(decoded != nullptr);
126
1
    const bool is_uuid = encoded.size() == 36 && encoded[8] == '-' && encoded[13] == '-' &&
127
1
                         encoded[18] == '-' && encoded[23] == '-';
128
1
    if (!is_uuid) {
129
1
        return decode_iceberg_hex(encoded, decoded);
130
1
    }
131
132
0
    std::string uuid_hex;
133
0
    uuid_hex.reserve(32);
134
0
    for (size_t index = 0; index < encoded.size(); ++index) {
135
0
        if (index != 8 && index != 13 && index != 18 && index != 23) {
136
0
            uuid_hex.push_back(encoded[index]);
137
0
        }
138
0
    }
139
0
    return decode_iceberg_hex(uuid_hex, decoded);
140
1
}
141
142
3
static std::string iceberg_json_scalar_text(const rapidjson::Value& value) {
143
3
    if (value.IsString()) {
144
2
        return {value.GetString(), value.GetStringLength()};
145
2
    }
146
1
    rapidjson::StringBuffer buffer;
147
1
    rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
148
1
    value.Accept(writer);
149
1
    return {buffer.GetString(), buffer.GetSize()};
150
3
}
151
152
1
static void normalize_iceberg_json_timestamp(PrimitiveType primitive_type, std::string* value) {
153
1
    if (primitive_type != TYPE_DATETIME && primitive_type != TYPE_DATETIMEV2 &&
154
1
        primitive_type != TYPE_TIMESTAMPTZ) {
155
1
        return;
156
1
    }
157
0
    if (const size_t separator = value->find('T'); separator != std::string::npos) {
158
0
        (*value)[separator] = ' ';
159
0
    }
160
0
    if (primitive_type == TYPE_TIMESTAMPTZ) {
161
0
        return;
162
0
    }
163
0
    if (value->ends_with('Z')) {
164
0
        value->pop_back();
165
0
        return;
166
0
    }
167
0
    const size_t time_start = value->find(' ');
168
0
    if (time_start == std::string::npos) {
169
0
        return;
170
0
    }
171
0
    const size_t offset = value->find_first_of("+-", time_start + 1);
172
0
    if (offset != std::string::npos) {
173
0
        value->erase(offset);
174
0
    }
175
0
}
176
177
static Status build_v2_null_default(const format::ColumnDefinition& field,
178
3
                                    const DataTypePtr& data_type, Field* result) {
179
3
    DORIS_CHECK(data_type != nullptr);
180
3
    DORIS_CHECK(result != nullptr);
181
3
    if (field.is_optional.has_value() && !*field.is_optional) {
182
0
        return Status::InvalidArgument("Required Iceberg field '{}' has a null default",
183
0
                                       field.name);
184
0
    }
185
3
    if (!data_type->is_nullable()) {
186
0
        return Status::InternalError(
187
0
                "Optional Iceberg field '{}' has a null default, but its Doris type '{}' is not "
188
0
                "nullable",
189
0
                field.name, data_type->get_name());
190
0
    }
191
3
    *result = Field();
192
3
    return Status::OK();
193
3
}
194
195
static const format::ColumnDefinition* find_v2_struct_child(const format::ColumnDefinition& field,
196
8
                                                            const std::string& name) {
197
8
    const auto exact_child = std::ranges::find_if(
198
11
            field.children, [&](const auto& candidate) { return iequal(candidate.name, name); });
199
8
    if (exact_child != field.children.end()) {
200
8
        return &*exact_child;
201
8
    }
202
0
    const auto aliased_child = std::ranges::find_if(field.children, [&](const auto& candidate) {
203
0
        return std::ranges::any_of(candidate.name_mapping,
204
0
                                   [&](const auto& alias) { return iequal(alias, name); });
205
0
    });
206
0
    return aliased_child == field.children.end() ? nullptr : &*aliased_child;
207
8
}
208
209
static Status build_v2_initial_default_field(const format::ColumnDefinition& field,
210
                                             const DataTypePtr& data_type,
211
                                             std::deque<std::string>* binary_storage,
212
                                             Field* result);
213
214
static Status build_v2_json_default_field(const format::ColumnDefinition& field,
215
                                          const DataTypePtr& data_type,
216
                                          const rapidjson::Value& json_value,
217
                                          std::deque<std::string>* binary_storage, Field* result);
218
219
static Status build_v2_json_struct_default(const format::ColumnDefinition& field,
220
                                           const DataTypePtr& value_type,
221
                                           const rapidjson::Value& json_value,
222
6
                                           std::deque<std::string>* binary_storage, Field* result) {
223
6
    if (!json_value.IsObject()) {
224
0
        return Status::InvalidArgument("Invalid Iceberg struct default for field '{}'", field.name);
225
0
    }
226
227
6
    const auto& struct_type = assert_cast<const DataTypeStruct&>(*value_type);
228
6
    Struct struct_value;
229
6
    struct_value.reserve(struct_type.get_elements().size());
230
14
    for (size_t index = 0; index < struct_type.get_elements().size(); ++index) {
231
8
        const auto* child = find_v2_struct_child(field, struct_type.get_element_name(index));
232
8
        if (child == nullptr || !child->has_identifier_field_id()) {
233
0
            return Status::InvalidArgument(
234
0
                    "Iceberg struct default for field '{}' has incomplete child metadata",
235
0
                    field.name);
236
0
        }
237
238
8
        const std::string child_id = std::to_string(child->get_identifier_field_id());
239
8
        const auto member = json_value.FindMember(child_id.c_str());
240
8
        Field child_value;
241
8
        if (member == json_value.MemberEnd()) {
242
8
            RETURN_IF_ERROR(build_v2_initial_default_field(*child, struct_type.get_element(index),
243
8
                                                           binary_storage, &child_value));
244
8
        } else {
245
0
            RETURN_IF_ERROR(build_v2_json_default_field(*child, struct_type.get_element(index),
246
0
                                                        member->value, binary_storage,
247
0
                                                        &child_value));
248
0
        }
249
8
        struct_value.push_back(std::move(child_value));
250
8
    }
251
6
    *result = Field::create_field<TYPE_STRUCT>(std::move(struct_value));
252
6
    return Status::OK();
253
6
}
254
255
// The child ColumnDefinition, recursively transported from the item TField, describes the element
256
// schema and its field-level default metadata. It cannot represent a particular list literal's
257
// length or per-position values, so the parent initial-default keeps those values in Iceberg's
258
// single-value JSON array.
259
static Status build_v2_json_array_default(const format::ColumnDefinition& field,
260
                                          const DataTypePtr& value_type,
261
                                          const rapidjson::Value& json_value,
262
1
                                          std::deque<std::string>* binary_storage, Field* result) {
263
1
    if (!json_value.IsArray() || field.children.size() != 1) {
264
0
        return Status::InvalidArgument("Invalid Iceberg list default for field '{}'", field.name);
265
0
    }
266
267
1
    const auto& array_type = assert_cast<const DataTypeArray&>(*value_type);
268
1
    Array array_value;
269
1
    array_value.reserve(json_value.Size());
270
2
    for (const auto& json_element : json_value.GetArray()) {
271
2
        Field element_value;
272
2
        RETURN_IF_ERROR(build_v2_json_default_field(field.children.front(),
273
2
                                                    array_type.get_nested_type(), json_element,
274
2
                                                    binary_storage, &element_value));
275
2
        array_value.push_back(std::move(element_value));
276
2
    }
277
1
    *result = Field::create_field<TYPE_ARRAY>(std::move(array_value));
278
1
    return Status::OK();
279
1
}
280
281
// The child ColumnDefinitions, recursively transported from the key/value TFields, describe entry
282
// schemas and field-level default metadata. They cannot represent the number, order, or concrete
283
// values of map entries, so the parent initial-default keeps the entries in Iceberg's single-value
284
// JSON key/value arrays.
285
static Status build_v2_json_map_default(const format::ColumnDefinition& field,
286
                                        const DataTypePtr& value_type,
287
                                        const rapidjson::Value& json_value,
288
1
                                        std::deque<std::string>* binary_storage, Field* result) {
289
1
    if (!json_value.IsObject() || !json_value.HasMember("keys") || !json_value["keys"].IsArray() ||
290
1
        !json_value.HasMember("values") || !json_value["values"].IsArray() ||
291
1
        field.children.size() != 2) {
292
0
        return Status::InvalidArgument("Invalid Iceberg map default for field '{}'", field.name);
293
0
    }
294
1
    const auto& keys = json_value["keys"];
295
1
    const auto& values = json_value["values"];
296
1
    if (keys.Size() != values.Size()) {
297
0
        return Status::InvalidArgument(
298
0
                "Iceberg map default for field '{}' has {} keys but {} values", field.name,
299
0
                keys.Size(), values.Size());
300
0
    }
301
302
1
    const auto& map_type = assert_cast<const DataTypeMap&>(*value_type);
303
1
    Array key_fields;
304
1
    Array value_fields;
305
1
    key_fields.reserve(keys.Size());
306
1
    value_fields.reserve(values.Size());
307
2
    for (rapidjson::SizeType index = 0; index < keys.Size(); ++index) {
308
1
        Field key_value;
309
1
        Field mapped_value;
310
1
        RETURN_IF_ERROR(build_v2_json_default_field(field.children[0], map_type.get_key_type(),
311
1
                                                    keys[index], binary_storage, &key_value));
312
1
        RETURN_IF_ERROR(build_v2_json_default_field(field.children[1], map_type.get_value_type(),
313
1
                                                    values[index], binary_storage, &mapped_value));
314
1
        key_fields.push_back(std::move(key_value));
315
1
        value_fields.push_back(std::move(mapped_value));
316
1
    }
317
1
    Map map_value;
318
1
    map_value.push_back(Field::create_field<TYPE_ARRAY>(std::move(key_fields)));
319
1
    map_value.push_back(Field::create_field<TYPE_ARRAY>(std::move(value_fields)));
320
1
    *result = Field::create_field<TYPE_MAP>(std::move(map_value));
321
1
    return Status::OK();
322
1
}
323
324
static Status build_v2_json_scalar_default(const format::ColumnDefinition& field,
325
                                           const DataTypePtr& value_type,
326
                                           const rapidjson::Value& json_value,
327
3
                                           std::deque<std::string>* binary_storage, Field* result) {
328
3
    const auto primitive_type = value_type->get_primitive_type();
329
3
    std::string serialized_value = iceberg_json_scalar_text(json_value);
330
3
    const bool binary_like =
331
3
            field.initial_default_value_is_base64 || primitive_type == TYPE_VARBINARY;
332
3
    if (binary_like) {
333
1
        if (!json_value.IsString()) {
334
0
            return Status::InvalidArgument(
335
0
                    "Iceberg binary default for field '{}' is not a JSON string", field.name);
336
0
        }
337
1
        binary_storage->emplace_back();
338
1
        RETURN_IF_ERROR(decode_iceberg_json_binary(serialized_value, &binary_storage->back()));
339
1
        if (primitive_type == TYPE_VARBINARY) {
340
0
            *result = Field::create_field<TYPE_VARBINARY>(StringView(binary_storage->back()));
341
1
        } else if (is_string_type(primitive_type)) {
342
1
            *result = Field::create_field<TYPE_STRING>(binary_storage->back());
343
1
        } else {
344
0
            return Status::InvalidArgument(
345
0
                    "Iceberg binary default for field '{}' has incompatible Doris type '{}'",
346
0
                    field.name, value_type->get_name());
347
0
        }
348
1
        return Status::OK();
349
1
    }
350
351
2
    if (is_string_type(primitive_type)) {
352
1
        if (!json_value.IsString()) {
353
0
            return Status::InvalidArgument("Iceberg string default for field '{}' is not a string",
354
0
                                           field.name);
355
0
        }
356
1
        *result = Field::create_field<TYPE_STRING>(std::move(serialized_value));
357
1
        return Status::OK();
358
1
    }
359
1
    normalize_iceberg_json_timestamp(primitive_type, &serialized_value);
360
1
    RETURN_IF_ERROR(value_type->get_serde()->from_fe_string(serialized_value, *result));
361
1
    return Status::OK();
362
1
}
363
364
static Status build_v2_json_default_field(const format::ColumnDefinition& field,
365
                                          const DataTypePtr& data_type,
366
                                          const rapidjson::Value& json_value,
367
12
                                          std::deque<std::string>* binary_storage, Field* result) {
368
12
    DORIS_CHECK(data_type != nullptr);
369
12
    DORIS_CHECK(binary_storage != nullptr);
370
12
    DORIS_CHECK(result != nullptr);
371
12
    if (json_value.IsNull()) {
372
1
        return build_v2_null_default(field, data_type, result);
373
1
    }
374
375
11
    const auto value_type = remove_nullable(data_type);
376
11
    switch (value_type->get_primitive_type()) {
377
6
    case TYPE_STRUCT:
378
6
        return build_v2_json_struct_default(field, value_type, json_value, binary_storage, result);
379
1
    case TYPE_ARRAY:
380
1
        return build_v2_json_array_default(field, value_type, json_value, binary_storage, result);
381
1
    case TYPE_MAP:
382
1
        return build_v2_json_map_default(field, value_type, json_value, binary_storage, result);
383
3
    default:
384
3
        return build_v2_json_scalar_default(field, value_type, json_value, binary_storage, result);
385
11
    }
386
11
}
387
388
static Status build_v2_initial_default_field(const format::ColumnDefinition& field,
389
                                             const DataTypePtr& data_type,
390
                                             std::deque<std::string>* binary_storage,
391
38
                                             Field* result) {
392
38
    DORIS_CHECK(data_type != nullptr);
393
38
    DORIS_CHECK(binary_storage != nullptr);
394
38
    DORIS_CHECK(result != nullptr);
395
38
    if (!field.initial_default_value.has_value()) {
396
2
        if (field.is_optional.has_value() && !*field.is_optional) {
397
0
            return Status::InvalidArgument(
398
0
                    "Required Iceberg field '{}' is missing from the data file and has no initial "
399
0
                    "default",
400
0
                    field.name);
401
0
        }
402
2
        return build_v2_null_default(field, data_type, result);
403
2
    }
404
405
36
    const auto value_type = remove_nullable(data_type);
406
36
    const auto primitive_type = value_type->get_primitive_type();
407
36
    if (is_complex_type(primitive_type)) {
408
8
        rapidjson::Document document;
409
8
        document.Parse(field.initial_default_value->data(), field.initial_default_value->size());
410
8
        if (document.HasParseError()) {
411
0
            return Status::InvalidArgument("Invalid Iceberg JSON initial default for field '{}'",
412
0
                                           field.name);
413
0
        }
414
8
        return build_v2_json_default_field(field, data_type, document, binary_storage, result);
415
8
    }
416
417
28
    if (field.initial_default_value_is_base64 || primitive_type == TYPE_VARBINARY) {
418
5
        binary_storage->emplace_back();
419
5
        if (!base64_decode(*field.initial_default_value, &binary_storage->back())) {
420
0
            return Status::InvalidArgument("Invalid Base64 Iceberg initial default for field {}",
421
0
                                           field.name);
422
0
        }
423
5
        if (primitive_type == TYPE_VARBINARY) {
424
3
            *result = Field::create_field<TYPE_VARBINARY>(StringView(binary_storage->back()));
425
3
        } else if (is_string_type(primitive_type)) {
426
2
            *result = Field::create_field<TYPE_STRING>(binary_storage->back());
427
2
        } else {
428
0
            return Status::InvalidArgument(
429
0
                    "Base64 Iceberg initial default has incompatible Doris type {} for field {}",
430
0
                    data_type->get_name(), field.name);
431
0
        }
432
5
        return Status::OK();
433
5
    }
434
435
23
    RETURN_IF_ERROR(value_type->get_serde()->from_fe_string(*field.initial_default_value, *result));
436
22
    return Status::OK();
437
23
}
438
439
static Status build_initial_default_literal(const format::ColumnDefinition& table_field,
440
30
                                            VExprSPtr* literal) {
441
30
    DORIS_CHECK(table_field.type != nullptr);
442
30
    DORIS_CHECK(table_field.initial_default_value.has_value());
443
30
    DORIS_CHECK(literal != nullptr);
444
445
30
    std::deque<std::string> binary_storage;
446
30
    Field initial_default;
447
30
    RETURN_IF_ERROR(build_v2_initial_default_field(table_field, table_field.type, &binary_storage,
448
30
                                                   &initial_default));
449
    // VLiteral inserts the Field into an owning column before binary_storage is destroyed.
450
29
    *literal = VLiteral::create_shared(table_field.type, initial_default);
451
29
    return Status::OK();
452
30
}
453
454
28
static Status build_initial_default_exprs(format::ColumnDefinition* column) {
455
28
    DORIS_CHECK(column != nullptr);
456
28
    if (column->initial_default_value.has_value()) {
457
18
        VExprSPtr literal;
458
18
        RETURN_IF_ERROR(build_initial_default_literal(*column, &literal));
459
17
        column->default_expr = VExprContext::create_shared(std::move(literal));
460
17
    }
461
27
    for (auto& child : column->children) {
462
14
        RETURN_IF_ERROR(build_initial_default_exprs(&child));
463
14
    }
464
27
    return Status::OK();
465
27
}
466
467
static Status build_missing_equality_delete_key_expr(const format::ColumnDefinition& table_field,
468
                                                     const DataTypePtr& delete_key_type,
469
                                                     bool require_complete_metadata,
470
18
                                                     VExprSPtr* key_expr) {
471
18
    DORIS_CHECK(delete_key_type != nullptr);
472
18
    DORIS_CHECK(key_expr != nullptr);
473
18
    if (!table_field.initial_default_value.has_value()) {
474
6
        if (require_complete_metadata && !table_field.is_optional.has_value()) {
475
0
            return Status::InvalidArgument(
476
0
                    "Iceberg equality delete field '{}' is missing optionality metadata",
477
0
                    table_field.name);
478
0
        }
479
6
        if (table_field.is_optional.has_value() && !*table_field.is_optional) {
480
0
            return Status::InvalidArgument("Missing required field: {}", table_field.name);
481
0
        }
482
        // A newly added optional field without an initial default is logically NULL in older
483
        // files. EqualityDeletePredicate treats NULL == NULL as a match.
484
6
        *key_expr = VLiteral::create_shared(make_nullable(delete_key_type), Field());
485
6
        return Status::OK();
486
6
    }
487
488
12
    VExprSPtr literal;
489
12
    RETURN_IF_ERROR(build_initial_default_literal(table_field, &literal));
490
12
    if (table_field.type->equals(*delete_key_type)) {
491
12
        *key_expr = std::move(literal);
492
12
        return Status::OK();
493
12
    }
494
0
    auto cast_expr = Cast::create_shared(delete_key_type);
495
0
    cast_expr->add_child(literal);
496
0
    *key_expr = std::move(cast_expr);
497
0
    return Status::OK();
498
12
}
499
500
static bool find_equality_delete_column_path(const std::vector<format::ColumnDefinition>& fields,
501
                                             int32_t field_id,
502
65
                                             std::vector<const format::ColumnDefinition*>* path) {
503
65
    DORIS_CHECK(path != nullptr);
504
65
    for (const auto& field : fields) {
505
63
        path->push_back(&field);
506
63
        if (field.has_identifier_field_id() && field.get_identifier_field_id() == field_id) {
507
48
            return true;
508
48
        }
509
15
        if (find_equality_delete_column_path(field.children, field_id, path)) {
510
13
            return true;
511
13
        }
512
2
        path->pop_back();
513
2
    }
514
4
    return false;
515
65
}
516
517
class NestedStructFieldExpr final : public VExpr {
518
public:
519
    NestedStructFieldExpr(DataTypePtr data_type, std::vector<size_t> child_indexes,
520
                          std::string expr_name)
521
29
            : VExpr(std::move(data_type), false),
522
29
              _child_indexes(std::move(child_indexes)),
523
29
              _expr_name(std::move(expr_name)) {
524
29
        _node_type = TExprNodeType::FUNCTION_CALL;
525
29
    }
526
527
    Status prepare(RuntimeState* state, const RowDescriptor& row_desc,
528
29
                   VExprContext* context) override {
529
29
        RETURN_IF_ERROR_OR_PREPARED(VExpr::prepare(state, row_desc, context));
530
29
        _prepare_finished = true;
531
29
        return Status::OK();
532
29
    }
533
534
    Status open(RuntimeState* state, VExprContext* context,
535
29
                FunctionContext::FunctionStateScope scope) override {
536
29
        RETURN_IF_ERROR_OR_PREPARED(VExpr::open(state, context, scope));
537
0
        _open_finished = true;
538
0
        return Status::OK();
539
0
    }
540
541
29
    void close(VExprContext* context, FunctionContext::FunctionStateScope scope) override {
542
29
        VExpr::close(context, scope);
543
29
    }
544
545
    Status execute_column_impl(VExprContext* context, const Block* block, const Selector* selector,
546
35
                               size_t count, ColumnPtr& result_column) const override {
547
35
        DORIS_CHECK(_children.size() == 1);
548
35
        ColumnPtr current;
549
35
        RETURN_IF_ERROR(
550
35
                _children.front()->execute_column(context, block, selector, count, current));
551
35
        current = current->convert_to_full_column_if_const();
552
553
35
        std::vector<ColumnPtr> ancestor_nullable_columns;
554
35
        std::vector<const NullMap*> ancestor_null_maps;
555
35
        for (const size_t child_index : _child_indexes) {
556
35
            if (const auto* nullable = check_and_get_column<ColumnNullable>(*current);
557
35
                nullable != nullptr) {
558
35
                ancestor_nullable_columns.push_back(current);
559
35
                ancestor_null_maps.push_back(&nullable->get_null_map_data());
560
35
                current = nullable->get_nested_column_ptr();
561
35
            }
562
35
            const auto* struct_column = check_and_get_column<ColumnStruct>(*current);
563
35
            DORIS_CHECK(struct_column != nullptr);
564
35
            DORIS_CHECK(child_index < struct_column->tuple_size());
565
35
            current = struct_column->get_column_ptr(child_index);
566
35
        }
567
35
        if (const auto* nullable = check_and_get_column<ColumnNullable>(*current);
568
35
            nullable != nullptr) {
569
35
            ancestor_nullable_columns.push_back(current);
570
35
            ancestor_null_maps.push_back(&nullable->get_null_map_data());
571
35
            current = nullable->get_nested_column_ptr();
572
35
        }
573
35
        if (ancestor_null_maps.empty()) {
574
0
            result_column = make_nullable(current);
575
0
            return Status::OK();
576
0
        }
577
578
35
        auto result = ColumnNullable::create(remove_nullable(_data_type)->create_column(),
579
35
                                             ColumnUInt8::create());
580
35
        auto& result_data = result->get_nested_column();
581
35
        auto& result_null_map = result->get_null_map_data();
582
35
        result_data.reserve(count);
583
35
        result_null_map.reserve(count);
584
102
        for (size_t row = 0; row < count; ++row) {
585
67
            const bool is_null =
586
110
                    std::ranges::any_of(ancestor_null_maps, [row](const NullMap* null_map) {
587
110
                        DORIS_CHECK(null_map != nullptr);
588
110
                        DORIS_CHECK(row < null_map->size());
589
110
                        return (*null_map)[row] != 0;
590
110
                    });
591
67
            if (is_null) {
592
24
                result_data.insert_default();
593
24
                result_null_map.push_back(1);
594
43
            } else {
595
43
                result_data.insert_from(*current, row);
596
43
                result_null_map.push_back(0);
597
43
            }
598
67
        }
599
35
        result_column = std::move(result);
600
35
        return Status::OK();
601
35
    }
602
603
35
    const std::string& expr_name() const override { return _expr_name; }
604
605
0
    Status clone_node(VExprSPtr* cloned_expr) const override {
606
0
        DORIS_CHECK(cloned_expr != nullptr);
607
0
        *cloned_expr = std::make_shared<NestedStructFieldExpr>(*this);
608
0
        return Status::OK();
609
0
    }
610
611
private:
612
    std::vector<size_t> _child_indexes;
613
    std::string _expr_name;
614
};
615
616
class AncestorNullDefaultExpr final : public VExpr {
617
public:
618
    AncestorNullDefaultExpr(DataTypePtr data_type, std::string expr_name)
619
2
            : VExpr(std::move(data_type), false), _expr_name(std::move(expr_name)) {
620
2
        _node_type = TExprNodeType::FUNCTION_CALL;
621
2
    }
622
623
    Status prepare(RuntimeState* state, const RowDescriptor& row_desc,
624
2
                   VExprContext* context) override {
625
2
        RETURN_IF_ERROR_OR_PREPARED(VExpr::prepare(state, row_desc, context));
626
2
        _prepare_finished = true;
627
2
        return Status::OK();
628
2
    }
629
630
    Status open(RuntimeState* state, VExprContext* context,
631
2
                FunctionContext::FunctionStateScope scope) override {
632
2
        RETURN_IF_ERROR_OR_PREPARED(VExpr::open(state, context, scope));
633
0
        _open_finished = true;
634
0
        return Status::OK();
635
0
    }
636
637
2
    void close(VExprContext* context, FunctionContext::FunctionStateScope scope) override {
638
2
        VExpr::close(context, scope);
639
2
    }
640
641
    Status execute_column_impl(VExprContext* context, const Block* block, const Selector* selector,
642
2
                               size_t count, ColumnPtr& result_column) const override {
643
2
        DORIS_CHECK(_children.size() == 2);
644
2
        ColumnPtr ancestor;
645
2
        RETURN_IF_ERROR(
646
2
                _children.front()->execute_column(context, block, selector, count, ancestor));
647
2
        ancestor = ancestor->convert_to_full_column_if_const();
648
2
        const NullMap* ancestor_null_map = nullptr;
649
2
        if (const auto* nullable = check_and_get_column<ColumnNullable>(*ancestor);
650
2
            nullable != nullptr) {
651
2
            ancestor_null_map = &nullable->get_null_map_data();
652
2
        }
653
654
2
        ColumnPtr default_value;
655
2
        RETURN_IF_ERROR(
656
2
                _children.back()->execute_column(context, block, selector, count, default_value));
657
2
        default_value = default_value->convert_to_full_column_if_const();
658
2
        const NullMap* default_null_map = nullptr;
659
2
        const IColumn* default_data = default_value.get();
660
2
        if (const auto* nullable = check_and_get_column<ColumnNullable>(*default_value);
661
2
            nullable != nullptr) {
662
2
            default_null_map = &nullable->get_null_map_data();
663
2
            default_data = &nullable->get_nested_column();
664
2
        }
665
666
2
        auto result = ColumnNullable::create(remove_nullable(_data_type)->create_column(),
667
2
                                             ColumnUInt8::create());
668
2
        auto& result_data = result->get_nested_column();
669
2
        auto& result_null_map = result->get_null_map_data();
670
2
        result_data.reserve(count);
671
2
        result_null_map.reserve(count);
672
8
        for (size_t row = 0; row < count; ++row) {
673
6
            const bool ancestor_is_null =
674
6
                    ancestor_null_map != nullptr && (*ancestor_null_map)[row] != 0;
675
6
            const bool default_is_null =
676
6
                    default_null_map != nullptr && (*default_null_map)[row] != 0;
677
6
            if (ancestor_is_null || default_is_null) {
678
2
                result_data.insert_default();
679
2
                result_null_map.push_back(1);
680
4
            } else {
681
4
                result_data.insert_from(*default_data, row);
682
4
                result_null_map.push_back(0);
683
4
            }
684
6
        }
685
2
        result_column = std::move(result);
686
2
        return Status::OK();
687
2
    }
688
689
2
    const std::string& expr_name() const override { return _expr_name; }
690
691
0
    Status clone_node(VExprSPtr* cloned_expr) const override {
692
0
        DORIS_CHECK(cloned_expr != nullptr);
693
0
        *cloned_expr = std::make_shared<AncestorNullDefaultExpr>(*this);
694
0
        return Status::OK();
695
0
    }
696
697
private:
698
    std::string _expr_name;
699
};
700
701
static Status build_nested_equality_delete_key_expr(
702
        const std::vector<const format::ColumnDefinition*>& path, VExprSPtr root_expr,
703
82
        VExprSPtr* key_expr) {
704
82
    DORIS_CHECK(!path.empty());
705
82
    DORIS_CHECK(root_expr != nullptr);
706
82
    DORIS_CHECK(key_expr != nullptr);
707
82
    const auto* root = path.front();
708
82
    DORIS_CHECK(root != nullptr);
709
82
    DORIS_CHECK(root->type != nullptr);
710
82
    VExprSPtr result = std::move(root_expr);
711
82
    std::vector<size_t> child_indexes;
712
82
    std::string expr_name = root->name;
713
111
    for (size_t index = 1; index < path.size(); ++index) {
714
29
        const auto* parent = path[index - 1];
715
29
        const auto* child = path[index];
716
29
        DORIS_CHECK(parent != nullptr);
717
29
        DORIS_CHECK(child != nullptr);
718
29
        DORIS_CHECK(parent->type != nullptr);
719
29
        DORIS_CHECK(child->type != nullptr);
720
29
        if (remove_nullable(parent->type)->get_primitive_type() != TYPE_STRUCT) {
721
0
            return Status::NotSupported(
722
0
                    "Iceberg equality delete field {} has non-struct ancestor {}", child->name,
723
0
                    parent->name);
724
0
        }
725
29
        const auto child_it =
726
29
                std::ranges::find_if(parent->children, [child](const auto& candidate) {
727
29
                    if (candidate.has_identifier_field_id() && child->has_identifier_field_id()) {
728
25
                        return candidate.get_identifier_field_id() ==
729
25
                               child->get_identifier_field_id();
730
25
                    }
731
4
                    return candidate.name == child->name;
732
29
                });
733
29
        DORIS_CHECK(child_it != parent->children.end());
734
29
        child_indexes.push_back(cast_set<size_t>(child_it - parent->children.begin()));
735
29
        expr_name += "." + child->name;
736
29
    }
737
82
    if (!child_indexes.empty()) {
738
29
        auto nested_field = std::make_shared<NestedStructFieldExpr>(
739
29
                make_nullable(path.back()->type), std::move(child_indexes), std::move(expr_name));
740
29
        nested_field->add_child(result);
741
29
        result = std::move(nested_field);
742
29
    }
743
82
    *key_expr = std::move(result);
744
82
    return Status::OK();
745
82
}
746
747
static Status build_equality_delete_key_expr(
748
        const std::vector<const format::ColumnDefinition*>& path, size_t block_position,
749
64
        VExprSPtr* key_expr) {
750
64
    DORIS_CHECK(!path.empty());
751
64
    const auto* root = path.front();
752
64
    DORIS_CHECK(root != nullptr);
753
64
    DORIS_CHECK(root->type != nullptr);
754
64
    VExprSPtr root_expr =
755
64
            VSlotRef::create_shared(cast_set<int>(block_position), cast_set<int>(block_position),
756
64
                                    -1, root->type, root->name);
757
64
    return build_nested_equality_delete_key_expr(path, std::move(root_expr), key_expr);
758
64
}
759
760
Status IcebergTableReader::annotate_projected_column(const TFileScanSlotInfo& slot_info,
761
                                                     format::ProjectedColumnBuildContext* context,
762
16
                                                     format::ColumnDefinition* column) const {
763
16
    RETURN_IF_ERROR(format::TableReader::annotate_projected_column(slot_info, context, column));
764
16
    DORIS_CHECK(context != nullptr);
765
16
    DORIS_CHECK(column != nullptr);
766
16
    if (!supports_iceberg_scan_semantics_v2(context->scan_params)) {
767
2
        return Status::OK();
768
2
    }
769
14
    if (!context->schema_column.has_value()) {
770
0
        return Status::OK();
771
0
    }
772
773
14
    auto& schema_column = *context->schema_column;
774
14
    RETURN_IF_ERROR(build_initial_default_exprs(&schema_column));
775
13
    column->initial_default_value = schema_column.initial_default_value;
776
13
    column->initial_default_value_is_base64 = schema_column.initial_default_value_is_base64;
777
13
    column->is_optional = schema_column.is_optional;
778
13
    if (schema_column.default_expr != nullptr) {
779
        // The Iceberg typed literal is authoritative. In particular, this replaces FE's generic
780
        // string expression for Base64-transported UUID/BINARY/FIXED defaults.
781
9
        column->default_expr = schema_column.default_expr;
782
9
    } else if (schema_column.is_optional.has_value() && !*schema_column.is_optional) {
783
        // FE's generic external-column metadata currently treats Iceberg columns as nullable. Clear
784
        // that fallback so a physically missing required field is rejected by the Iceberg mapper.
785
1
        column->default_expr = nullptr;
786
1
    }
787
13
    return Status::OK();
788
14
}
789
790
0
static std::string iceberg_delete_file_debug_string(const TIcebergDeleteFileDesc& delete_file) {
791
0
    std::ostringstream out;
792
0
    out << "TIcebergDeleteFileDesc{path=" << (delete_file.__isset.path ? delete_file.path : "null")
793
0
        << ", content=" << (delete_file.__isset.content ? delete_file.content : -1)
794
0
        << ", file_format="
795
0
        << (delete_file.__isset.file_format ? static_cast<int>(delete_file.file_format) : -1)
796
0
        << ", position_lower_bound="
797
0
        << (delete_file.__isset.position_lower_bound ? delete_file.position_lower_bound : -1)
798
0
        << ", position_upper_bound="
799
0
        << (delete_file.__isset.position_upper_bound ? delete_file.position_upper_bound : -1)
800
0
        << ", field_ids="
801
0
        << (delete_file.__isset.field_ids ? join_values_for_debug(delete_file.field_ids) : "[]")
802
0
        << ", content_offset="
803
0
        << (delete_file.__isset.content_offset ? delete_file.content_offset : -1)
804
0
        << ", content_size_in_bytes="
805
0
        << (delete_file.__isset.content_size_in_bytes ? delete_file.content_size_in_bytes : -1)
806
0
        << "}";
807
0
    return out.str();
808
0
}
809
810
static std::string iceberg_delete_files_debug_string(
811
0
        const std::vector<TIcebergDeleteFileDesc>& delete_files) {
812
0
    std::ostringstream out;
813
0
    out << "[";
814
0
    for (size_t idx = 0; idx < delete_files.size(); ++idx) {
815
0
        if (idx > 0) {
816
0
            out << ", ";
817
0
        }
818
0
        out << iceberg_delete_file_debug_string(delete_files[idx]);
819
0
    }
820
0
    out << "]";
821
0
    return out.str();
822
0
}
823
824
0
static std::string iceberg_params_debug_string(const std::optional<TIcebergFileDesc>& params) {
825
0
    if (!params.has_value()) {
826
0
        return "null";
827
0
    }
828
0
    const auto& iceberg_params = *params;
829
0
    std::ostringstream out;
830
0
    out << "TIcebergFileDesc{format_version="
831
0
        << (iceberg_params.__isset.format_version ? iceberg_params.format_version : -1)
832
0
        << ", content=" << (iceberg_params.__isset.content ? iceberg_params.content : -1)
833
0
        << ", original_file_path="
834
0
        << (iceberg_params.__isset.original_file_path ? iceberg_params.original_file_path : "null")
835
0
        << ", row_count=" << (iceberg_params.__isset.row_count ? iceberg_params.row_count : -1)
836
0
        << ", partition_spec_id="
837
0
        << (iceberg_params.__isset.partition_spec_id ? iceberg_params.partition_spec_id : 0)
838
0
        << ", has_partition_data_json=" << iceberg_params.__isset.partition_data_json
839
0
        << ", first_row_id="
840
0
        << (iceberg_params.__isset.first_row_id ? iceberg_params.first_row_id : -1)
841
0
        << ", last_updated_sequence_number="
842
0
        << (iceberg_params.__isset.last_updated_sequence_number
843
0
                    ? iceberg_params.last_updated_sequence_number
844
0
                    : -1)
845
0
        << ", delete_file_count="
846
0
        << (iceberg_params.__isset.delete_files ? iceberg_params.delete_files.size() : 0)
847
0
        << ", delete_files="
848
0
        << (iceberg_params.__isset.delete_files
849
0
                    ? iceberg_delete_files_debug_string(iceberg_params.delete_files)
850
0
                    : "[]")
851
0
        << ", has_serialized_split=" << iceberg_params.__isset.serialized_split << "}";
852
0
    return out.str();
853
0
}
854
855
IcebergTableReader::PositionDeleteRowsCollector::PositionDeleteRowsCollector(
856
        PositionDeleteFile* rows_by_data_file)
857
12
        : _rows_by_data_file(rows_by_data_file) {
858
12
    DORIS_CHECK(_rows_by_data_file != nullptr);
859
12
}
860
861
Status IcebergTableReader::PositionDeleteRowsCollector::collect(const Block& block,
862
22
                                                                size_t read_rows) {
863
22
    if (read_rows == 0) {
864
10
        return Status::OK();
865
10
    }
866
12
    const auto& file_path_column_ptr =
867
12
            block.get_by_position(ICEBERG_FILE_PATH_BLOCK_POSITION).column;
868
12
    const auto& pos_column_ptr = block.get_by_position(ICEBERG_ROW_POS_BLOCK_POSITION).column;
869
12
    if (const auto* nullable_column = check_and_get_column<ColumnNullable>(*file_path_column_ptr);
870
12
        nullable_column != nullptr && nullable_column->has_null(0, read_rows)) {
871
1
        return Status::Corruption("Iceberg position delete column file_path contains null values");
872
1
    }
873
11
    if (const auto* nullable_column = check_and_get_column<ColumnNullable>(*pos_column_ptr);
874
11
        nullable_column != nullptr && nullable_column->has_null(0, read_rows)) {
875
1
        return Status::Corruption("Iceberg position delete column pos contains null values");
876
1
    }
877
10
    const auto& file_path_column =
878
10
            assert_cast<const ColumnString&>(*remove_nullable(file_path_column_ptr));
879
10
    const auto& pos_column = assert_cast<const ColumnInt64&>(*remove_nullable(pos_column_ptr));
880
26
    for (size_t row = 0; row < read_rows; ++row) {
881
16
        const auto file_path = file_path_column.get_data_at(row).to_string();
882
16
        (*_rows_by_data_file)[file_path].push_back(pos_column.get_element(row));
883
16
    }
884
10
    return Status::OK();
885
11
}
886
887
79
Status IcebergTableReader::prepare_split(const format::SplitReadOptions& options) {
888
79
    {
889
79
        SCOPED_TIMER(_profile.total_timer);
890
79
        SCOPED_TIMER(_profile.prepare_split_timer);
891
79
        _row_lineage_columns = {};
892
79
        _iceberg_params.reset();
893
79
        _delete_predicates_initialized = false;
894
79
        _position_delete_rows_storage.clear();
895
79
        _equality_delete_filters.clear();
896
79
        _split_cache = options.cache;
897
79
        if (options.current_range.__isset.table_format_params &&
898
79
            options.current_range.table_format_params.__isset.iceberg_params) {
899
77
            const auto& iceberg_params = options.current_range.table_format_params.iceberg_params;
900
77
            _iceberg_params = iceberg_params;
901
77
            if (iceberg_params.__isset.first_row_id) {
902
9
                _row_lineage_columns.first_row_id = iceberg_params.first_row_id;
903
9
            }
904
77
            if (iceberg_params.__isset.last_updated_sequence_number) {
905
6
                _row_lineage_columns.last_updated_sequence_number =
906
6
                        iceberg_params.last_updated_sequence_number;
907
6
            }
908
77
        }
909
79
    }
910
79
    RETURN_IF_ERROR(TableReader::prepare_split(options));
911
76
    SCOPED_TIMER(_profile.total_timer);
912
76
    SCOPED_TIMER(_profile.prepare_split_timer);
913
76
    if (current_split_pruned()) {
914
0
        return Status::OK();
915
0
    }
916
    // Iceberg data files are immutable once referenced by a snapshot; updates create new data files
917
    // at new paths instead of overwriting existing files. This lets the Parquet V2 reader use page
918
    // cache when the scan range does not carry an mtime, without extending V1's path::0 behavior to
919
    // mutable Hive/local files.
920
76
    mark_current_data_file_immutable();
921
76
    if (_is_table_level_count_active()) {
922
1
        return Status::OK();
923
1
    }
924
75
    DBUG_EXECUTE_IF("IcebergTableReader.prepare_split.before_delete_file_scan",
925
75
                    DBUG_RUN_CALLBACK());
926
75
    RETURN_IF_ERROR(_init_delete_predicates(options.current_range.table_format_params));
927
73
    return Status::OK();
928
75
}
929
930
0
std::string IcebergTableReader::debug_string() const {
931
0
    size_t position_delete_file_count = 0;
932
0
    size_t equality_delete_file_count = 0;
933
0
    size_t deletion_vector_file_count = 0;
934
0
    if (_iceberg_params.has_value() && _iceberg_params->__isset.delete_files) {
935
0
        for (const auto& delete_file : _iceberg_params->delete_files) {
936
0
            if (!delete_file.__isset.content) {
937
0
                continue;
938
0
            }
939
0
            if (delete_file.content == POSITION_DELETE) {
940
0
                ++position_delete_file_count;
941
0
            } else if (delete_file.content == EQUALITY_DELETE) {
942
0
                ++equality_delete_file_count;
943
0
            } else if (delete_file.content == DELETION_VECTOR) {
944
0
                ++deletion_vector_file_count;
945
0
            }
946
0
        }
947
0
    }
948
949
0
    std::ostringstream equality_filters;
950
0
    equality_filters << "[";
951
0
    for (size_t idx = 0; idx < _equality_delete_filters.size(); ++idx) {
952
0
        if (idx > 0) {
953
0
            equality_filters << ", ";
954
0
        }
955
0
        const auto& filter = _equality_delete_filters[idx];
956
0
        equality_filters << "EqualityDeleteFilter{field_ids="
957
0
                         << join_values_for_debug(filter.field_ids) << ", key_types=[";
958
0
        for (size_t type_idx = 0; type_idx < filter.key_types.size(); ++type_idx) {
959
0
            if (type_idx > 0) {
960
0
                equality_filters << ", ";
961
0
            }
962
0
            equality_filters << (filter.key_types[type_idx] == nullptr
963
0
                                         ? "null"
964
0
                                         : filter.key_types[type_idx]->get_name());
965
0
        }
966
0
        equality_filters << "], delete_block_rows=" << filter.delete_block.rows()
967
0
                         << ", delete_block_columns=" << filter.delete_block.columns() << "}";
968
0
    }
969
0
    equality_filters << "]";
970
971
0
    std::ostringstream out;
972
0
    out << "IcebergTableReader{base=" << format::TableReader::debug_string()
973
0
        << ", iceberg_params=" << iceberg_params_debug_string(_iceberg_params)
974
0
        << ", row_lineage_first_row_id=" << _row_lineage_columns.first_row_id
975
0
        << ", row_lineage_last_updated_sequence_number="
976
0
        << _row_lineage_columns.last_updated_sequence_number
977
0
        << ", need_row_lineage_row_id=" << _need_row_lineage_row_id()
978
0
        << ", need_iceberg_rowid=" << _need_iceberg_rowid()
979
0
        << ", row_position_block_position=" << _row_position_block_position
980
0
        << ", delete_predicates_initialized=" << _delete_predicates_initialized
981
0
        << ", position_delete_file_count=" << position_delete_file_count
982
0
        << ", equality_delete_file_count=" << equality_delete_file_count
983
0
        << ", deletion_vector_file_count=" << deletion_vector_file_count
984
0
        << ", position_delete_rows_storage_count=" << _position_delete_rows_storage.size()
985
0
        << ", equality_delete_filter_count=" << _equality_delete_filters.size()
986
0
        << ", equality_delete_filters=" << equality_filters.str() << "}";
987
0
    return out.str();
988
0
}
989
990
59
Status IcebergTableReader::materialize_virtual_columns(Block* table_block) {
991
136
    for (size_t column_idx = 0; column_idx < _data_reader.column_mapper->mappings().size();
992
77
         ++column_idx) {
993
77
        const auto& mapping = _data_reader.column_mapper->mappings()[column_idx];
994
77
        switch (mapping.virtual_column_type) {
995
9
        case format::TableVirtualColumnType::ROW_ID:
996
9
            RETURN_IF_ERROR(_materialize_row_lineage_row_id(table_block, column_idx));
997
9
            break;
998
9
        case format::TableVirtualColumnType::LAST_UPDATED_SEQUENCE_NUMBER:
999
8
            RETURN_IF_ERROR(
1000
8
                    _materialize_row_lineage_last_updated_sequence_number(table_block, column_idx));
1001
8
            break;
1002
8
        case format::TableVirtualColumnType::ICEBERG_ROWID:
1003
1
            RETURN_IF_ERROR(_materialize_iceberg_rowid(table_block, column_idx));
1004
1
            break;
1005
59
        case format::TableVirtualColumnType::INVALID:
1006
59
            break;
1007
77
        }
1008
77
    }
1009
59
    return Status::OK();
1010
59
}
1011
1012
72
Status IcebergTableReader::customize_file_scan_request(format::FileScanRequest* file_request) {
1013
72
    RETURN_IF_ERROR(TableReader::customize_file_scan_request(file_request));
1014
72
    if ((_row_lineage_columns.first_row_id >= 0 && _need_row_lineage_row_id()) ||
1015
72
        _need_iceberg_rowid()) {
1016
9
        RETURN_IF_ERROR(_append_row_position_output_column(file_request));
1017
9
    }
1018
72
    RETURN_IF_ERROR(_append_equality_delete_predicates(file_request));
1019
71
    return Status::OK();
1020
72
}
1021
1022
70
bool IcebergTableReader::_supports_aggregate_pushdown(TPushAggOp::type agg_type) const {
1023
70
    if (!TableReader::_supports_aggregate_pushdown(agg_type)) {
1024
70
        return false;
1025
70
    }
1026
0
    return _equality_delete_filters.empty();
1027
70
}
1028
1029
Status IcebergTableReader::_parse_deletion_vector_file(const TTableFormatFileDesc& t_desc,
1030
                                                       DeleteFileDesc* desc,
1031
86
                                                       bool* has_delete_file) {
1032
86
    DORIS_CHECK(desc != nullptr);
1033
86
    DORIS_CHECK(has_delete_file != nullptr);
1034
86
    *has_delete_file = false;
1035
86
    if (!t_desc.__isset.iceberg_params) {
1036
2
        return Status::OK();
1037
2
    }
1038
84
    const auto& iceberg_params = t_desc.iceberg_params;
1039
84
    if (!iceberg_params.__isset.format_version ||
1040
84
        iceberg_params.format_version < MIN_SUPPORT_DELETE_FILES_VERSION ||
1041
84
        !iceberg_params.__isset.delete_files || iceberg_params.delete_files.empty()) {
1042
16
        return Status::OK();
1043
16
    }
1044
1045
68
    const TIcebergDeleteFileDesc* deletion_vector = nullptr;
1046
70
    for (const auto& delete_file : iceberg_params.delete_files) {
1047
70
        if (!delete_file.__isset.content || delete_file.content != DELETION_VECTOR) {
1048
55
            continue;
1049
55
        }
1050
15
        if (deletion_vector != nullptr) {
1051
1
            return Status::DataQualityError("This iceberg data file has multiple DVs.");
1052
1
        }
1053
14
        deletion_vector = &delete_file;
1054
14
    }
1055
67
    if (deletion_vector == nullptr) {
1056
54
        return Status::OK();
1057
54
    }
1058
13
    size_t bytes_read = 0;
1059
13
    RETURN_IF_ERROR(validate_iceberg_deletion_vector_descriptor(*deletion_vector, bytes_read));
1060
1061
11
    const std::string data_file_path = iceberg_params.__isset.original_file_path
1062
11
                                               ? iceberg_params.original_file_path
1063
11
                                               : _data_file_path();
1064
11
    desc->key = build_iceberg_deletion_vector_cache_key(data_file_path, *deletion_vector);
1065
11
    desc->path = deletion_vector->path;
1066
11
    desc->start_offset = deletion_vector->content_offset;
1067
11
    desc->size = static_cast<int64_t>(bytes_read);
1068
11
    desc->file_size = -1;
1069
11
    desc->format = DeleteFileDesc::Format::ICEBERG;
1070
11
    *has_delete_file = true;
1071
11
    return Status::OK();
1072
13
}
1073
1074
75
Status IcebergTableReader::_init_delete_predicates(const TTableFormatFileDesc& t_desc) {
1075
75
    if (!t_desc.__isset.iceberg_params || _delete_predicates_initialized) {
1076
2
        _delete_predicates_initialized = true;
1077
2
        return Status::OK();
1078
2
    }
1079
73
    const auto& iceberg_params = t_desc.iceberg_params;
1080
73
    if (!iceberg_params.__isset.format_version ||
1081
73
        iceberg_params.format_version < MIN_SUPPORT_DELETE_FILES_VERSION ||
1082
73
        !iceberg_params.__isset.delete_files || iceberg_params.delete_files.empty()) {
1083
16
        _delete_predicates_initialized = true;
1084
16
        return Status::OK();
1085
16
    }
1086
1087
57
    std::vector<TIcebergDeleteFileDesc> position_delete_files;
1088
57
    std::vector<TIcebergDeleteFileDesc> equality_delete_files;
1089
58
    for (const auto& delete_file : iceberg_params.delete_files) {
1090
58
        if (!delete_file.__isset.content) {
1091
0
            continue;
1092
0
        }
1093
58
        if (delete_file.content == POSITION_DELETE) {
1094
14
            position_delete_files.push_back(delete_file);
1095
44
        } else if (delete_file.content == EQUALITY_DELETE) {
1096
41
            equality_delete_files.push_back(delete_file);
1097
41
        }
1098
58
    }
1099
    // Per Iceberg scan planning, position delete files apply only when there is no deletion vector
1100
    // for the data file. DVs and position deletes now intentionally use different in-memory
1101
    // representations, so use the Roaring pointer as the DV sentinel.
1102
57
    if (_deletion_vector != nullptr) {
1103
3
        position_delete_files.clear();
1104
3
    }
1105
    // Initialize position and equality delete predicates. Position delete files contain row
1106
    // positions of deleted rows, which can be directly added to `_delete_rows`. Equality delete
1107
    // files contain values of deleted rows, which require reading the files and building
1108
    // predicates for later filtering.
1109
57
    if (!position_delete_files.empty()) {
1110
13
        RETURN_IF_ERROR(_init_position_delete_rows(position_delete_files));
1111
13
    }
1112
55
    if (!equality_delete_files.empty()) {
1113
41
        RETURN_IF_ERROR(_init_equality_delete_predicates(equality_delete_files));
1114
41
    }
1115
1116
55
    _delete_predicates_initialized = true;
1117
55
    return Status::OK();
1118
55
}
1119
1120
std::shared_ptr<io::FileSystemProperties> IcebergTableReader::_delete_file_system_properties(
1121
52
        const TFileScanRangeParams& scan_params) {
1122
52
    auto system_properties = std::make_shared<io::FileSystemProperties>();
1123
52
    system_properties->system_type =
1124
52
            scan_params.__isset.file_type ? scan_params.file_type : TFileType::FILE_LOCAL;
1125
52
    system_properties->properties = scan_params.properties;
1126
52
    system_properties->hdfs_params = scan_params.hdfs_params;
1127
52
    if (scan_params.__isset.broker_addresses) {
1128
0
        system_properties->broker_addresses.assign(scan_params.broker_addresses.begin(),
1129
0
                                                   scan_params.broker_addresses.end());
1130
0
    }
1131
52
    return system_properties;
1132
52
}
1133
1134
std::unique_ptr<io::FileDescription> IcebergTableReader::_delete_file_description(
1135
52
        const TFileRangeDesc& range) {
1136
52
    auto file_description = std::make_unique<io::FileDescription>();
1137
52
    file_description->path = range.path;
1138
52
    file_description->file_size = range.__isset.file_size ? range.file_size : -1;
1139
52
    file_description->range_start_offset = range.__isset.start_offset ? range.start_offset : 0;
1140
52
    file_description->range_size = range.__isset.size ? range.size : -1;
1141
    // Iceberg delete files follow the same immutable-file contract as data files: a snapshot
1142
    // references a fixed object and later changes publish a new file rather than replacing it.
1143
52
    file_description->is_immutable = true;
1144
52
    if (range.__isset.fs_name) {
1145
4
        file_description->fs_name = range.fs_name;
1146
4
    }
1147
52
    return file_description;
1148
52
}
1149
1150
12
std::string IcebergTableReader::_data_file_path() const {
1151
12
    if (_iceberg_params.has_value() && _iceberg_params->__isset.original_file_path) {
1152
11
        return _iceberg_params->original_file_path;
1153
11
    }
1154
1
    DORIS_CHECK(_current_task != nullptr);
1155
1
    DORIS_CHECK(_current_task->data_file != nullptr);
1156
1
    return _current_task->data_file->path;
1157
12
}
1158
1159
9
Status IcebergTableReader::_append_row_position_output_column(format::FileScanRequest* request) {
1160
9
    const auto row_position_column_id = format::LocalColumnId(format::ROW_POSITION_COLUMN_ID);
1161
9
    _append_file_scan_column(request, row_position_column_id, &request->non_predicate_columns);
1162
9
    _row_position_block_position = request->local_positions.at(row_position_column_id).value();
1163
9
    return Status::OK();
1164
9
}
1165
1166
Status IcebergTableReader::_find_equality_delete_data_field(
1167
        const EqualityDeleteFilter& filter, size_t key_idx,
1168
41
        EqualityDeleteColumnPath* const data_path, bool* const complete_path) const {
1169
41
    DORIS_CHECK(key_idx < filter.field_ids.size());
1170
41
    DORIS_CHECK(key_idx < filter.field_names.size());
1171
41
    DORIS_CHECK(data_path != nullptr);
1172
41
    DORIS_CHECK(complete_path != nullptr);
1173
41
    data_path->clear();
1174
41
    *complete_path = false;
1175
1176
41
    auto schema_path =
1177
41
            _find_table_column_identity_path_by_field_id(filter.field_ids[key_idx], true);
1178
41
    std::vector<const format::ColumnDefinition*> table_path;
1179
41
    if (schema_path.has_value()) {
1180
50
        for (const auto& field : *schema_path) {
1181
50
            table_path.push_back(&field);
1182
50
        }
1183
32
    } else {
1184
9
        static_cast<void>(find_equality_delete_column_path(_projected_columns,
1185
9
                                                           filter.field_ids[key_idx], &table_path));
1186
9
    }
1187
41
    if (table_path.empty() && mapping_mode() != format::TableColumnMappingMode::BY_NAME) {
1188
1
        const int field_id = filter.field_ids[key_idx];
1189
1
        *complete_path =
1190
1
                find_equality_delete_column_path(_data_reader.file_schema, field_id, data_path);
1191
1
        return Status::OK();
1192
1
    }
1193
1194
    // Equality keys are hidden scan dependencies and need not appear in the query projection.
1195
    // Reuse ColumnMapper's exact BY_NAME rules at every ancestor so a nested key keeps its
1196
    // physical path, including historical aliases for ID-less files.
1197
40
    std::optional<format::ColumnDefinition> legacy_table_field;
1198
40
    if (table_path.empty() && !supports_iceberg_scan_semantics_v2(_scan_params)) {
1199
0
        legacy_table_field.emplace();
1200
0
        legacy_table_field->name = filter.field_names[key_idx];
1201
0
        legacy_table_field->type = filter.key_types[key_idx];
1202
0
        table_path.push_back(&*legacy_table_field);
1203
0
    }
1204
40
    if (table_path.empty()) {
1205
0
        return Status::InvalidArgument(
1206
0
                "Iceberg equality delete field id {} is absent from current and historical table "
1207
0
                "schema metadata",
1208
0
                filter.field_ids[key_idx]);
1209
0
    }
1210
40
    const std::vector<format::ColumnDefinition>* candidates = &_data_reader.file_schema;
1211
74
    for (size_t index = 0; index < table_path.size(); ++index) {
1212
52
        const auto* table_field = table_path[index];
1213
52
        DORIS_CHECK(table_field != nullptr);
1214
52
        const format::ColumnDefinition* data_field = nullptr;
1215
52
        if (mapping_mode() == format::TableColumnMappingMode::BY_NAME) {
1216
15
            data_field = format::find_column_by_name(*table_field, *candidates);
1217
37
        } else {
1218
37
            DORIS_CHECK(table_field->has_identifier_field_id());
1219
37
            data_field = format::find_column_by_field_id(
1220
37
                    *table_field, *candidates,
1221
37
                    supports_iceberg_scan_semantics_v1(_scan_params) &&
1222
37
                            _format == FileFormat::PARQUET);
1223
37
        }
1224
52
        if (data_field == nullptr && mapping_mode() == format::TableColumnMappingMode::BY_NAME &&
1225
52
            index + 1 == table_path.size() && !table_field->has_name_mapping) {
1226
            // Schema-history fallback can carry a post-snapshot leaf rename when the target
1227
            // snapshot's parent has expired. Retry the delete file's original leaf name, but
1228
            // never bypass an explicit authoritative Iceberg mapping.
1229
7
            format::ColumnDefinition delete_file_field;
1230
7
            delete_file_field.name = filter.field_names[key_idx];
1231
7
            data_field = format::find_column_by_name(delete_file_field, *candidates);
1232
7
        }
1233
52
        if (data_field == nullptr) {
1234
18
            return Status::OK();
1235
18
        }
1236
34
        data_path->push_back(data_field);
1237
34
        candidates = &data_field->children;
1238
34
    }
1239
22
    *complete_path = true;
1240
22
    return Status::OK();
1241
40
}
1242
1243
Status IcebergTableReader::_find_equality_delete_table_field(
1244
        const EqualityDeleteFilter& filter, size_t key_idx,
1245
1
        format::ColumnDefinition* table_field) const {
1246
1
    DORIS_CHECK(key_idx < filter.field_ids.size());
1247
1
    DORIS_CHECK(key_idx < filter.field_names.size());
1248
1
    DORIS_CHECK(table_field != nullptr);
1249
1
    const int field_id = filter.field_ids[key_idx];
1250
1
    auto resolved = _find_table_column_by_field_id(field_id, filter.key_types[key_idx], true);
1251
1
    if (!resolved.has_value()) {
1252
1
        const auto projected_field = std::ranges::find_if(
1253
1
                _projected_columns, [field_id](const format::ColumnDefinition& field) {
1254
1
                    return field.has_identifier_field_id() &&
1255
1
                           field.get_identifier_field_id() == field_id;
1256
1
                });
1257
1
        if (projected_field != _projected_columns.end()) {
1258
            // Older scan descriptors and focused unit tests may omit history_schema_info. Keep the
1259
            // projected metadata as a compatibility fallback, but never require projection when
1260
            // the complete current schema is available.
1261
0
            resolved = *projected_field;
1262
0
        }
1263
1
    }
1264
1
    if (!resolved.has_value() && !supports_iceberg_scan_semantics_v2(_scan_params)) {
1265
0
        resolved = format::ColumnDefinition {
1266
0
                .identifier = {},
1267
0
                .name = filter.field_names[key_idx],
1268
0
                .type = filter.key_types[key_idx],
1269
0
        };
1270
0
    }
1271
1
    if (!resolved.has_value()) {
1272
1
        return Status::InvalidArgument(
1273
1
                "Iceberg equality delete field id {} is absent from current and historical table "
1274
1
                "schema metadata",
1275
1
                field_id);
1276
1
    }
1277
0
    *table_field = std::move(*resolved);
1278
0
    return Status::OK();
1279
1
}
1280
1281
std::string IcebergTableReader::_delete_file_cache_key(const char* prefix,
1282
54
                                                       const std::string& path) const {
1283
54
    DORIS_CHECK(prefix != nullptr);
1284
54
    std::string fs_name;
1285
54
    if (_current_task != nullptr && _current_task->data_file != nullptr) {
1286
54
        fs_name = _current_task->data_file->fs_name;
1287
54
    }
1288
    // Delete descriptors can reuse the same path text in different filesystem namespaces. Encode
1289
    // both variable-length strings so neither an fs/path boundary nor equality field-id suffixes
1290
    // can be reinterpreted as path content; scan-level credentials/properties are shared here.
1291
54
    std::ostringstream key;
1292
54
    key << prefix << fs_name.size() << ':' << fs_name << ':' << path.size() << ':' << path;
1293
54
    return key.str();
1294
54
}
1295
1296
void IcebergTableReader::_append_equality_delete_row_count_carrier(
1297
16
        format::FileScanRequest* request) {
1298
16
    DORIS_CHECK(request != nullptr);
1299
    // Columnar readers establish a filter batch's row count from predicate columns. If all
1300
    // equality keys are missing, the predicate consists only of NULL literals and the filter block
1301
    // would otherwise have zero rows. Use the virtual row-position column as the carrier instead
1302
    // of an arbitrary physical column. For example, a data file may start with an unsupported
1303
    // TIME_MILLIS leaf while the query projects only a supported `id`; selecting that TIME leaf as
1304
    // a hidden carrier would make Parquet reject a column the query never requested. Row position
1305
    // has one value per input row in both Parquet and ORC, is already used by delete predicates,
1306
    // and is explicitly excluded from physical logical-type validation.
1307
16
    _append_file_scan_column(request, format::LocalColumnId(format::ROW_POSITION_COLUMN_ID),
1308
16
                             &request->predicate_columns);
1309
16
}
1310
1311
Status IcebergTableReader::_build_missing_equality_delete_key_expr(
1312
        const EqualityDeleteFilter& filter, size_t key_idx,
1313
        const EqualityDeleteColumnPath& data_path, format::FileScanRequest* const request,
1314
19
        VExprSPtr* const key_expr) {
1315
19
    DORIS_CHECK(request != nullptr);
1316
19
    DORIS_CHECK(key_expr != nullptr);
1317
19
    auto table_path = _find_table_column_path_by_field_id(filter.field_ids[key_idx],
1318
19
                                                          filter.key_types[key_idx], true);
1319
19
    if (!table_path.has_value() || data_path.size() >= table_path->size()) {
1320
1
        format::ColumnDefinition table_field;
1321
1
        RETURN_IF_ERROR(_find_equality_delete_table_field(filter, key_idx, &table_field));
1322
0
        table_path = std::vector<format::ColumnDefinition> {std::move(table_field)};
1323
0
    }
1324
18
    const size_t missing_index = data_path.size() < table_path->size() ? data_path.size() : 0;
1325
18
    auto& missing_root = (*table_path)[missing_index];
1326
18
    DORIS_CHECK(missing_root.type != nullptr);
1327
18
    VExprSPtr missing_root_expr;
1328
18
    RETURN_IF_ERROR(build_missing_equality_delete_key_expr(
1329
18
            missing_root, missing_root.type, supports_iceberg_scan_semantics_v2(_scan_params),
1330
18
            &missing_root_expr));
1331
18
    std::vector<const format::ColumnDefinition*> missing_path;
1332
42
    for (size_t path_index = missing_index; path_index < table_path->size(); ++path_index) {
1333
24
        missing_path.push_back(&(*table_path)[path_index]);
1334
24
    }
1335
18
    VExprSPtr default_expr;
1336
18
    RETURN_IF_ERROR(build_nested_equality_delete_key_expr(
1337
18
            missing_path, std::move(missing_root_expr), &default_expr));
1338
18
    const auto* table_leaf = missing_path.back();
1339
18
    DORIS_CHECK(table_leaf != nullptr);
1340
18
    DORIS_CHECK(table_leaf->type != nullptr);
1341
18
    if (!table_leaf->type->equals(*filter.key_types[key_idx])) {
1342
1
        auto cast_expr = Cast::create_shared(filter.key_types[key_idx]);
1343
1
        cast_expr->add_child(default_expr);
1344
1
        default_expr = std::move(cast_expr);
1345
1
    }
1346
18
    if (data_path.empty()) {
1347
16
        *key_expr = std::move(default_expr);
1348
16
        return Status::OK();
1349
16
    }
1350
1351
2
    const auto* root = data_path.front();
1352
2
    const auto field_column_id = format::LocalColumnId(root->file_local_id());
1353
2
    _append_file_scan_column(request, field_column_id, &request->predicate_columns);
1354
2
    const auto block_position = request->local_positions.at(field_column_id).value();
1355
2
    VExprSPtr ancestor_expr;
1356
2
    RETURN_IF_ERROR(build_equality_delete_key_expr(data_path, block_position, &ancestor_expr));
1357
2
    auto combined_expr = std::make_shared<AncestorNullDefaultExpr>(
1358
2
            make_nullable(filter.key_types[key_idx]),
1359
2
            ancestor_expr->expr_name() + "." + table_leaf->name);
1360
2
    combined_expr->add_child(ancestor_expr);
1361
2
    combined_expr->add_child(default_expr);
1362
2
    *key_expr = std::move(combined_expr);
1363
2
    return Status::OK();
1364
2
}
1365
1366
72
Status IcebergTableReader::_append_equality_delete_predicates(format::FileScanRequest* request) {
1367
72
    DORIS_CHECK(request != nullptr);
1368
72
    for (const auto& filter : _equality_delete_filters) {
1369
41
        auto delete_predicate =
1370
41
                std::make_shared<EqualityDeletePredicate>(filter.delete_block, filter.field_ids);
1371
41
        DCHECK_EQ(filter.field_ids.size(), filter.key_types.size());
1372
41
        bool has_missing_key = false;
1373
81
        for (size_t idx = 0; idx < filter.field_ids.size(); ++idx) {
1374
41
            EqualityDeleteColumnPath data_path;
1375
41
            bool complete_path = false;
1376
41
            RETURN_IF_ERROR(
1377
41
                    _find_equality_delete_data_field(filter, idx, &data_path, &complete_path));
1378
41
            if (!complete_path) {
1379
19
                VExprSPtr key_expr;
1380
19
                RETURN_IF_ERROR(_build_missing_equality_delete_key_expr(filter, idx, data_path,
1381
19
                                                                        request, &key_expr));
1382
18
                delete_predicate->add_child(key_expr);
1383
18
                has_missing_key = true;
1384
18
                continue;
1385
19
            }
1386
22
            const auto* root = data_path.front();
1387
22
            const auto* field = data_path.back();
1388
22
            const auto field_column_id = format::LocalColumnId(root->file_local_id());
1389
22
            _append_file_scan_column(request, field_column_id, &request->predicate_columns);
1390
22
            const auto block_position = request->local_positions.at(field_column_id).value();
1391
22
            VExprSPtr key_expr;
1392
22
            RETURN_IF_ERROR(build_equality_delete_key_expr(data_path, block_position, &key_expr));
1393
22
            if (field->type->equals(*filter.key_types[idx])) {
1394
21
                delete_predicate->add_child(key_expr);
1395
21
            } else {
1396
1
                auto cast_expr = Cast::create_shared(filter.key_types[idx]);
1397
1
                cast_expr->add_child(key_expr);
1398
1
                delete_predicate->add_child(cast_expr);
1399
1
            }
1400
22
        }
1401
40
        if (has_missing_key && request->predicate_columns.empty()) {
1402
16
            _append_equality_delete_row_count_carrier(request);
1403
16
        }
1404
40
        request->delete_conjuncts.push_back(
1405
40
                VExprContext::create_shared(std::move(delete_predicate)));
1406
40
    }
1407
71
    return Status::OK();
1408
72
}
1409
1410
Status IcebergTableReader::_create_delete_file_reader(const TIcebergDeleteFileDesc& delete_file,
1411
                                                      const TFileScanRangeParams& scan_params,
1412
                                                      IcebergDeleteFileIOContext* delete_io_ctx,
1413
52
                                                      std::unique_ptr<format::FileReader>* reader) {
1414
52
    DORIS_CHECK(delete_io_ctx != nullptr);
1415
52
    DORIS_CHECK(reader != nullptr);
1416
52
    if (!delete_file.__isset.file_format) {
1417
0
        return Status::InternalError("Iceberg delete file is missing file format");
1418
0
    }
1419
52
    if (delete_file.file_format != TFileFormatType::FORMAT_PARQUET &&
1420
52
        delete_file.file_format != TFileFormatType::FORMAT_ORC) {
1421
0
        return Status::NotSupported("Unsupported Iceberg delete file format {}",
1422
0
                                    delete_file.file_format);
1423
0
    }
1424
52
    auto delete_range = build_iceberg_delete_file_range(delete_file.path);
1425
52
    if (_current_task != nullptr && _current_task->data_file != nullptr &&
1426
52
        !_current_task->data_file->fs_name.empty()) {
1427
4
        delete_range.__set_fs_name(_current_task->data_file->fs_name);
1428
4
    }
1429
52
    auto system_properties = _delete_file_system_properties(scan_params);
1430
52
    auto file_description = _delete_file_description(delete_range);
1431
52
    std::shared_ptr<io::IOContext> io_ctx(&delete_io_ctx->io_ctx, [](io::IOContext*) {});
1432
52
    const bool enable_mapping_timestamp_tz = scan_params.__isset.enable_mapping_timestamp_tz &&
1433
52
                                             scan_params.enable_mapping_timestamp_tz;
1434
52
    const bool enable_mapping_varbinary =
1435
52
            scan_params.__isset.enable_mapping_varbinary && scan_params.enable_mapping_varbinary;
1436
52
    if (delete_file.file_format == TFileFormatType::FORMAT_PARQUET) {
1437
        // Delete and data files must parse raw binary fields with the same scan-level mapping.
1438
42
        *reader = std::make_unique<format::parquet::ParquetReader>(
1439
42
                system_properties, file_description, io_ctx, _scanner_profile, std::nullopt,
1440
42
                enable_mapping_timestamp_tz, enable_mapping_varbinary);
1441
42
    } else {
1442
10
        *reader = std::make_unique<format::orc::OrcReader>(system_properties, file_description,
1443
10
                                                           io_ctx, _scanner_profile, std::nullopt,
1444
10
                                                           enable_mapping_timestamp_tz);
1445
10
    }
1446
52
    RETURN_IF_ERROR((*reader)->init(_runtime_state));
1447
52
    return Status::OK();
1448
52
}
1449
1450
Status IcebergTableReader::_read_position_delete_file(const TIcebergDeleteFileDesc& delete_file,
1451
                                                      const TFileScanRangeParams& scan_params,
1452
                                                      IcebergDeleteFileIOContext* delete_io_ctx,
1453
12
                                                      PositionDeleteRowsCollector* collector) {
1454
12
    DORIS_CHECK(collector != nullptr);
1455
12
    std::unique_ptr<format::FileReader> reader;
1456
12
    RETURN_IF_ERROR(_create_delete_file_reader(delete_file, scan_params, delete_io_ctx, &reader));
1457
12
    DORIS_CHECK(reader != nullptr);
1458
1459
12
    std::vector<format::ColumnDefinition> schema;
1460
12
    RETURN_IF_ERROR(reader->get_schema(&schema));
1461
12
    format::ColumnDefinition* file_path_field = nullptr;
1462
12
    format::ColumnDefinition* pos_field = nullptr;
1463
24
    for (auto& field : schema) {
1464
24
        if (field.name == ICEBERG_FILE_PATH) {
1465
12
            file_path_field = &field;
1466
12
        } else if (field.name == ICEBERG_ROW_POS) {
1467
12
            pos_field = &field;
1468
12
        }
1469
24
    }
1470
12
    if (file_path_field == nullptr || pos_field == nullptr) {
1471
0
        return Status::InternalError("Position delete file is missing required columns");
1472
0
    }
1473
1474
12
    auto request = std::make_shared<format::FileScanRequest>();
1475
12
    request->non_predicate_columns = {
1476
12
            format::LocalColumnIndex::top_level(
1477
12
                    format::LocalColumnId(file_path_field->file_local_id())),
1478
12
            format::LocalColumnIndex::top_level(format::LocalColumnId(pos_field->file_local_id()))};
1479
12
    request->local_positions = {
1480
12
            {format::LocalColumnId(file_path_field->file_local_id()),
1481
12
             format::LocalIndex(ICEBERG_FILE_PATH_BLOCK_POSITION)},
1482
12
            {format::LocalColumnId(pos_field->file_local_id()),
1483
12
             format::LocalIndex(ICEBERG_ROW_POS_BLOCK_POSITION)},
1484
12
    };
1485
12
    RETURN_IF_ERROR(reader->open(request));
1486
1487
12
    bool eof = false;
1488
12
    auto build_position_delete_block = [](const format::ColumnDefinition& file_path_field,
1489
22
                                          const format::ColumnDefinition& pos_field) -> Block {
1490
22
        Block block;
1491
22
        block.insert(
1492
22
                {file_path_field.type->create_column(), file_path_field.type, ICEBERG_FILE_PATH});
1493
22
        block.insert({pos_field.type->create_column(), pos_field.type, ICEBERG_ROW_POS});
1494
22
        return block;
1495
22
    };
1496
32
    while (!eof) {
1497
22
        Block block = build_position_delete_block(*file_path_field, *pos_field);
1498
22
        size_t read_rows = 0;
1499
22
        RETURN_IF_ERROR(reader->get_block(&block, &read_rows, &eof));
1500
22
        RETURN_IF_ERROR(collector->collect(block, read_rows));
1501
22
    }
1502
10
    return reader->close();
1503
12
}
1504
1505
Status IcebergTableReader::_init_position_delete_rows(
1506
13
        const std::vector<TIcebergDeleteFileDesc>& delete_files) {
1507
13
    DORIS_CHECK(_split_cache != nullptr);
1508
13
    TFileScanRangeParams delete_scan_params =
1509
13
            _scan_params == nullptr ? TFileScanRangeParams() : *_scan_params;
1510
13
    format::DeleteRows position_delete_rows;
1511
13
    IcebergDeleteFileIOContext delete_io_ctx(_runtime_state);
1512
13
    for (const auto& delete_file : delete_files) {
1513
13
        Status read_status = Status::OK();
1514
        // A position delete file normally references many data files. Cache the complete
1515
        // path-to-position map once; caching only the current data file would still rescan the
1516
        // shared delete file for every subsequent split.
1517
13
        auto* rows_by_data_file =
1518
13
                _split_cache->get<PositionDeleteRowsCollector::PositionDeleteFile>(
1519
13
                        _delete_file_cache_key("iceberg_v2_position_delete_", delete_file.path),
1520
13
                        [&]() -> PositionDeleteRowsCollector::PositionDeleteFile* {
1521
12
                            auto result = std::make_unique<
1522
12
                                    PositionDeleteRowsCollector::PositionDeleteFile>();
1523
12
                            PositionDeleteRowsCollector collector(result.get());
1524
12
                            read_status = _read_position_delete_file(
1525
12
                                    delete_file, delete_scan_params, &delete_io_ctx, &collector);
1526
12
                            if (!read_status.ok()) {
1527
2
                                return nullptr;
1528
2
                            }
1529
12
                            for (auto& [_, rows] : *result) {
1530
12
                                std::ranges::sort(rows);
1531
12
                            }
1532
10
                            return result.release();
1533
12
                        });
1534
13
        RETURN_IF_ERROR(read_status);
1535
11
        DORIS_CHECK(rows_by_data_file != nullptr);
1536
11
        const auto rows_it = rows_by_data_file->find(_data_file_path());
1537
11
        if (rows_it == rows_by_data_file->end()) {
1538
0
            continue;
1539
0
        }
1540
11
        auto first = rows_it->second.begin();
1541
11
        auto last = rows_it->second.end();
1542
        // Bounds are inclusive Iceberg position statistics supplied by FE. Apply them after the
1543
        // cached per-data-file vector is sorted so irrelevant positions are sliced without a scan.
1544
11
        if (delete_file.__isset.position_lower_bound) {
1545
1
            first = std::lower_bound(first, last, delete_file.position_lower_bound);
1546
1
        }
1547
11
        if (delete_file.__isset.position_upper_bound) {
1548
1
            last = std::upper_bound(first, last, delete_file.position_upper_bound);
1549
1
        }
1550
11
        position_delete_rows.insert(position_delete_rows.end(), first, last);
1551
11
    }
1552
11
    if (position_delete_rows.empty()) {
1553
0
        return Status::OK();
1554
0
    }
1555
    // Position delete files and deletion vectors both become row-position deletes for the
1556
    // common TableReader DeletePredicate path. Keep the merged rows in a member vector because
1557
    // DeletePredicate stores a reference to the vector used by _delete_rows.
1558
11
    _position_delete_rows_storage.insert(_position_delete_rows_storage.end(),
1559
11
                                         position_delete_rows.begin(), position_delete_rows.end());
1560
11
    std::sort(_position_delete_rows_storage.begin(), _position_delete_rows_storage.end());
1561
11
    _position_delete_rows_storage.erase(
1562
11
            std::unique(_position_delete_rows_storage.begin(), _position_delete_rows_storage.end()),
1563
11
            _position_delete_rows_storage.end());
1564
11
    _delete_rows = &_position_delete_rows_storage;
1565
11
    return Status::OK();
1566
11
}
1567
1568
Status IcebergTableReader::_init_equality_delete_predicates(
1569
41
        const std::vector<TIcebergDeleteFileDesc>& delete_files) {
1570
41
    DORIS_CHECK(_split_cache != nullptr);
1571
41
    TFileScanRangeParams delete_scan_params =
1572
41
            _scan_params == nullptr ? TFileScanRangeParams() : *_scan_params;
1573
41
    IcebergDeleteFileIOContext delete_io_ctx(_runtime_state);
1574
41
    for (const auto& delete_file : delete_files) {
1575
41
        RETURN_IF_ERROR(
1576
41
                _read_equality_delete_file(delete_file, delete_scan_params, &delete_io_ctx));
1577
41
    }
1578
41
    return Status::OK();
1579
41
}
1580
1581
Status IcebergTableReader::_resolve_equality_delete_fields(
1582
        const TIcebergDeleteFileDesc& delete_file,
1583
        const std::vector<format::ColumnDefinition>& schema,
1584
40
        std::vector<EqualityDeleteColumnPath>* delete_paths, EqualityDeleteFilter* result) const {
1585
40
    DORIS_CHECK(delete_paths != nullptr);
1586
40
    DORIS_CHECK(result != nullptr);
1587
40
    for (const auto field_id : delete_file.field_ids) {
1588
40
        EqualityDeleteColumnPath path;
1589
40
        if (!find_equality_delete_column_path(schema, field_id, &path)) {
1590
0
            return Status::InternalError("Can not find field id {} in equality delete file {}",
1591
0
                                         field_id, delete_file.path);
1592
0
        }
1593
40
        const auto* field = path.back();
1594
40
        if (!field->children.empty()) {
1595
0
            return Status::NotSupported(
1596
0
                    "Iceberg equality delete does not support complex column {}", field->name);
1597
0
        }
1598
40
        const auto key_type = path.size() > 1 ? make_nullable(field->type) : field->type;
1599
40
        delete_paths->push_back(std::move(path));
1600
40
        result->field_ids.push_back(field_id);
1601
40
        result->field_names.push_back(field->name);
1602
40
        result->key_types.push_back(key_type);
1603
40
    }
1604
40
    return Status::OK();
1605
40
}
1606
1607
Status IcebergTableReader::_load_equality_delete_file(const TIcebergDeleteFileDesc& delete_file,
1608
                                                      const TFileScanRangeParams& scan_params,
1609
                                                      IcebergDeleteFileIOContext* delete_io_ctx,
1610
40
                                                      EqualityDeleteFilter* result) {
1611
40
    DORIS_CHECK(result != nullptr);
1612
40
    std::unique_ptr<format::FileReader> reader;
1613
40
    RETURN_IF_ERROR(_create_delete_file_reader(delete_file, scan_params, delete_io_ctx, &reader));
1614
40
    DORIS_CHECK(reader != nullptr);
1615
1616
40
    std::vector<format::ColumnDefinition> schema;
1617
40
    RETURN_IF_ERROR(reader->get_schema(&schema));
1618
40
    std::vector<EqualityDeleteColumnPath> delete_paths;
1619
40
    RETURN_IF_ERROR(_resolve_equality_delete_fields(delete_file, schema, &delete_paths, result));
1620
1621
40
    auto request = std::make_shared<format::FileScanRequest>();
1622
40
    format::FileScanRequestBuilder request_builder(request.get());
1623
40
    for (const auto& path : delete_paths) {
1624
40
        DORIS_CHECK(!path.empty());
1625
40
        RETURN_IF_ERROR(request_builder.add_non_predicate_column(
1626
40
                format::LocalColumnId(path.front()->file_local_id())));
1627
40
    }
1628
40
    Block file_block_template;
1629
40
    std::vector<const format::ColumnDefinition*> roots(request->local_positions.size());
1630
40
    for (const auto& path : delete_paths) {
1631
40
        const auto* root = path.front();
1632
40
        const auto position =
1633
40
                request->local_positions.at(format::LocalColumnId(root->file_local_id()));
1634
40
        roots[position.value()] = root;
1635
40
    }
1636
40
    for (const auto* root : roots) {
1637
40
        DORIS_CHECK(root != nullptr);
1638
40
        file_block_template.insert({root->type->create_column(), root->type, root->name});
1639
40
    }
1640
1641
40
    std::vector<VExprContextSPtr> key_exprs;
1642
40
    key_exprs.reserve(delete_paths.size());
1643
40
    RowDescriptor row_desc;
1644
40
    for (const auto& path : delete_paths) {
1645
40
        const auto root_column_id = format::LocalColumnId(path.front()->file_local_id());
1646
40
        VExprSPtr key_expr;
1647
40
        RETURN_IF_ERROR(build_equality_delete_key_expr(
1648
40
                path, request->local_positions.at(root_column_id).value(), &key_expr));
1649
40
        auto context = VExprContext::create_shared(std::move(key_expr));
1650
40
        RETURN_IF_ERROR(context->prepare(_runtime_state, row_desc));
1651
40
        RETURN_IF_ERROR(context->open(_runtime_state));
1652
40
        key_exprs.push_back(std::move(context));
1653
40
    }
1654
40
    RETURN_IF_ERROR(reader->open(request));
1655
1656
40
    Block delete_block_template;
1657
80
    for (size_t index = 0; index < delete_paths.size(); ++index) {
1658
40
        const auto* field = delete_paths[index].back();
1659
40
        const auto& key_type = result->key_types[index];
1660
40
        delete_block_template.insert({key_type->create_column(), key_type, field->name});
1661
40
    }
1662
40
    MutableBlock mutable_delete_block(delete_block_template.clone_empty());
1663
40
    bool eof = false;
1664
120
    while (!eof) {
1665
80
        Block block = file_block_template.clone_empty();
1666
80
        size_t read_rows = 0;
1667
80
        RETURN_IF_ERROR(reader->get_block(&block, &read_rows, &eof));
1668
80
        if (read_rows > 0) {
1669
40
            Block key_block;
1670
40
            for (const auto& context : key_exprs) {
1671
40
                ColumnWithTypeAndName key;
1672
40
                RETURN_IF_ERROR(context->execute(&block, key));
1673
40
                key_block.insert(std::move(key));
1674
40
            }
1675
40
            RETURN_IF_ERROR(mutable_delete_block.merge(key_block));
1676
40
        }
1677
80
    }
1678
40
    RETURN_IF_ERROR(reader->close());
1679
40
    result->delete_block = mutable_delete_block.to_block();
1680
40
    return Status::OK();
1681
40
}
1682
1683
Status IcebergTableReader::_read_equality_delete_file(const TIcebergDeleteFileDesc& delete_file,
1684
                                                      const TFileScanRangeParams& scan_params,
1685
41
                                                      IcebergDeleteFileIOContext* delete_io_ctx) {
1686
41
    if (!delete_file.__isset.field_ids || delete_file.field_ids.empty()) {
1687
0
        return Status::InternalError("Iceberg equality delete file is missing field ids");
1688
0
    }
1689
41
    std::ostringstream cache_key;
1690
41
    cache_key << _delete_file_cache_key("iceberg_v2_equality_delete_", delete_file.path);
1691
41
    cache_key << ':' << delete_file.field_ids.size();
1692
41
    for (const auto field_id : delete_file.field_ids) {
1693
41
        cache_key << ':' << field_id;
1694
41
    }
1695
41
    Status read_status = Status::OK();
1696
    // Include the ordered equality ids in the key because the same physical delete file can be
1697
    // projected with different key layouts. The cached block and its key metadata are immutable
1698
    // after construction and therefore safe to copy into each split-local predicate.
1699
41
    auto* cached_filter = _split_cache->get<EqualityDeleteFilter>(
1700
41
            cache_key.str(), [&]() -> EqualityDeleteFilter* {
1701
40
                auto result = std::make_unique<EqualityDeleteFilter>();
1702
40
                read_status = _load_equality_delete_file(delete_file, scan_params, delete_io_ctx,
1703
40
                                                         result.get());
1704
40
                if (!read_status.ok()) {
1705
0
                    return nullptr;
1706
0
                }
1707
40
                return result.release();
1708
40
            });
1709
41
    RETURN_IF_ERROR(read_status);
1710
41
    DORIS_CHECK(cached_filter != nullptr);
1711
41
    _equality_delete_filters.push_back(*cached_filter);
1712
41
    return Status::OK();
1713
41
}
1714
1715
9
Status IcebergTableReader::_materialize_row_lineage_row_id(Block* table_block, size_t column_idx) {
1716
9
    if (_row_lineage_columns.first_row_id < 0) {
1717
2
        return Status::OK();
1718
2
    }
1719
7
    DORIS_CHECK(_row_position_block_position < _data_reader.block_template.columns());
1720
7
    const auto& row_position_column = assert_cast<const ColumnInt64&>(
1721
7
            *_data_reader.block_template.get_by_position(_row_position_block_position).column);
1722
7
    DORIS_CHECK(row_position_column.size() == table_block->rows());
1723
7
    auto column = IColumn::mutate(
1724
7
            table_block->get_by_position(column_idx).column->convert_to_full_column_if_const());
1725
7
    auto* nullable_column = assert_cast<ColumnNullable*>(column.get());
1726
7
    auto& null_map = nullable_column->get_null_map_data();
1727
7
    auto& data = assert_cast<ColumnInt64&>(*nullable_column->get_nested_column_ptr()).get_data();
1728
7
    DORIS_CHECK(null_map.size() == row_position_column.size());
1729
7
    DORIS_CHECK(data.size() == row_position_column.size());
1730
23
    for (size_t row = 0; row < row_position_column.size(); ++row) {
1731
16
        if (null_map[row]) {
1732
10
            null_map[row] = 0;
1733
10
            data[row] = _row_lineage_columns.first_row_id + row_position_column.get_element(row);
1734
10
        }
1735
16
    }
1736
7
    table_block->replace_by_position(column_idx, std::move(column));
1737
7
    return Status::OK();
1738
9
}
1739
1740
1
Status IcebergTableReader::_materialize_iceberg_rowid(Block* table_block, size_t column_idx) {
1741
1
    DORIS_CHECK(_row_position_block_position < _data_reader.block_template.columns());
1742
1
    const auto& row_position_column = assert_cast<const ColumnInt64&>(
1743
1
            *_data_reader.block_template.get_by_position(_row_position_block_position).column);
1744
1
    DORIS_CHECK(row_position_column.size() == table_block->rows());
1745
1746
1
    const auto& type = table_block->get_by_position(column_idx).type;
1747
1
    auto column = type->create_column();
1748
1
    auto* nullable_column = check_and_get_column<ColumnNullable>(column.get());
1749
1
    auto* struct_column = nullable_column != nullptr
1750
1
                                  ? check_and_get_column<ColumnStruct>(
1751
1
                                            nullable_column->get_nested_column_ptr().get())
1752
1
                                  : check_and_get_column<ColumnStruct>(column.get());
1753
1
    DORIS_CHECK(struct_column != nullptr);
1754
1
    DORIS_CHECK(struct_column->tuple_size() >= 4);
1755
1756
1
    const auto rows = row_position_column.size();
1757
1
    const auto file_path = _data_file_path();
1758
1
    const int32_t partition_spec_id =
1759
1
            _iceberg_params.has_value() && _iceberg_params->__isset.partition_spec_id
1760
1
                    ? _iceberg_params->partition_spec_id
1761
1
                    : 0;
1762
1
    const std::string partition_data_json =
1763
1
            _iceberg_params.has_value() && _iceberg_params->__isset.partition_data_json
1764
1
                    ? _iceberg_params->partition_data_json
1765
1
                    : "";
1766
1767
1
    auto& file_path_column = struct_column->get_column(0);
1768
1
    auto& row_pos_column = struct_column->get_column(1);
1769
1
    auto& spec_id_column = struct_column->get_column(2);
1770
1
    auto& partition_data_column = struct_column->get_column(3);
1771
1
    file_path_column.reserve(rows);
1772
1
    row_pos_column.reserve(rows);
1773
1
    spec_id_column.reserve(rows);
1774
1
    partition_data_column.reserve(rows);
1775
3
    for (size_t row = 0; row < rows; ++row) {
1776
2
        file_path_column.insert_data(file_path.data(), file_path.size());
1777
2
        const int64_t row_pos = row_position_column.get_element(row);
1778
2
        row_pos_column.insert_data(reinterpret_cast<const char*>(&row_pos), sizeof(row_pos));
1779
2
        spec_id_column.insert_data(reinterpret_cast<const char*>(&partition_spec_id),
1780
2
                                   sizeof(partition_spec_id));
1781
2
        partition_data_column.insert_data(partition_data_json.data(), partition_data_json.size());
1782
2
    }
1783
1
    if (nullable_column != nullptr) {
1784
1
        nullable_column->get_null_map_data().resize_fill(rows, 0);
1785
1
    }
1786
1
    table_block->replace_by_position(column_idx, std::move(column));
1787
1
    return Status::OK();
1788
1
}
1789
1790
Status IcebergTableReader::_materialize_row_lineage_last_updated_sequence_number(
1791
8
        Block* table_block, size_t column_idx) {
1792
8
    if (_row_lineage_columns.last_updated_sequence_number < 0) {
1793
2
        return Status::OK();
1794
2
    }
1795
6
    auto column = IColumn::mutate(
1796
6
            table_block->get_by_position(column_idx).column->convert_to_full_column_if_const());
1797
6
    auto* nullable_column = assert_cast<ColumnNullable*>(column.get());
1798
6
    auto& null_map = nullable_column->get_null_map_data();
1799
6
    auto& data = assert_cast<ColumnInt64&>(*nullable_column->get_nested_column_ptr()).get_data();
1800
6
    DORIS_CHECK(null_map.size() == table_block->rows());
1801
6
    DORIS_CHECK(data.size() == table_block->rows());
1802
20
    for (size_t row = 0; row < table_block->rows(); ++row) {
1803
14
        if (null_map[row]) {
1804
8
            null_map[row] = 0;
1805
8
            data[row] = _row_lineage_columns.last_updated_sequence_number;
1806
8
        }
1807
14
    }
1808
6
    table_block->replace_by_position(column_idx, std::move(column));
1809
6
    return Status::OK();
1810
8
}
1811
1812
8
bool IcebergTableReader::_need_row_lineage_row_id() const {
1813
8
    if (_data_reader.column_mapper != nullptr) {
1814
7
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
1815
7
            if (mapping.virtual_column_type == format::TableVirtualColumnType::ROW_ID) {
1816
7
                return true;
1817
7
            }
1818
7
        }
1819
7
    }
1820
1
    return std::ranges::any_of(_projected_columns, is_projected_row_lineage_row_id);
1821
8
}
1822
1823
64
bool IcebergTableReader::_need_iceberg_rowid() const {
1824
64
    if (_data_reader.column_mapper != nullptr) {
1825
68
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
1826
68
            if (mapping.virtual_column_type == format::TableVirtualColumnType::ICEBERG_ROWID) {
1827
1
                return true;
1828
1
            }
1829
68
        }
1830
64
    }
1831
63
    return std::ranges::any_of(_projected_columns, is_projected_iceberg_rowid);
1832
64
}
1833
1834
} // namespace doris::format::iceberg