Coverage Report

Created: 2026-08-02 22:14

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