Coverage Report

Created: 2026-08-07 19:43

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