Coverage Report

Created: 2026-08-07 00:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format_v2/table_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_reader.h"
19
20
#include <gen_cpp/ExternalTableSchema_types.h>
21
#include <gen_cpp/PlanNodes_types.h>
22
#include <gen_cpp/Types_types.h>
23
24
#include <algorithm>
25
#include <memory>
26
#include <ranges>
27
#include <set>
28
#include <sstream>
29
#include <utility>
30
#include <vector>
31
32
#include "common/cast_set.h"
33
#include "common/status.h"
34
#include "core/assert_cast.h"
35
#include "core/data_type/data_type_array.h"
36
#include "core/data_type/data_type_factory.hpp"
37
#include "core/data_type/data_type_map.h"
38
#include "core/data_type/data_type_struct.h"
39
#include "core/data_type/primitive_type.h"
40
#include "exprs/vexpr_context.h"
41
#include "exprs/vslot_ref.h"
42
#include "format/table/deletion_vector_reader.h"
43
#include "format/table/iceberg_delete_file_reader_helper.h"
44
#include "format/table/iceberg_scan_semantics.h"
45
#include "format/table/paimon_reader.h"
46
#include "format_v2/column_mapper.h"
47
#include "format_v2/delimited_text/csv_reader.h"
48
#include "format_v2/delimited_text/text_reader.h"
49
#include "format_v2/json/json_reader.h"
50
#include "format_v2/native/native_reader.h"
51
#include "format_v2/orc/orc_reader.h"
52
#include "format_v2/parquet/parquet_reader.h"
53
#include "runtime/file_scan_profile.h"
54
#include "storage/segment/condition_cache.h"
55
#include "util/debug_points.h"
56
#include "util/string_util.h"
57
58
namespace doris::format {
59
namespace {
60
61
template <typename T, typename Formatter>
62
61
std::string join_table_reader_debug_strings(const std::vector<T>& values, Formatter formatter) {
63
61
    std::ostringstream out;
64
61
    out << "[";
65
70
    for (size_t i = 0; i < values.size(); ++i) {
66
9
        if (i > 0) {
67
3
            out << ", ";
68
3
        }
69
9
        out << formatter(values[i]);
70
9
    }
71
61
    out << "]";
72
61
    return out.str();
73
61
}
table_reader.cpp:_ZN5doris6format12_GLOBAL__N_131join_table_reader_debug_stringsINS0_16ColumnDefinitionEZNKS0_11TableReader12debug_stringB5cxx11EvE3$_0EENSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEERKSt6vectorIT_SaISD_EET0_
Line
Count
Source
62
12
std::string join_table_reader_debug_strings(const std::vector<T>& values, Formatter formatter) {
63
12
    std::ostringstream out;
64
12
    out << "[";
65
14
    for (size_t i = 0; i < values.size(); ++i) {
66
2
        if (i > 0) {
67
1
            out << ", ";
68
1
        }
69
2
        out << formatter(values[i]);
70
2
    }
71
12
    out << "]";
72
12
    return out.str();
73
12
}
table_reader.cpp:_ZN5doris6format12_GLOBAL__N_131join_table_reader_debug_stringsINS0_11TableFilterEZNKS0_11TableReader12debug_stringB5cxx11EvE3$_1EENSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEERKSt6vectorIT_SaISD_EET0_
Line
Count
Source
62
12
std::string join_table_reader_debug_strings(const std::vector<T>& values, Formatter formatter) {
63
12
    std::ostringstream out;
64
12
    out << "[";
65
13
    for (size_t i = 0; i < values.size(); ++i) {
66
1
        if (i > 0) {
67
0
            out << ", ";
68
0
        }
69
1
        out << formatter(values[i]);
70
1
    }
71
12
    out << "]";
72
12
    return out.str();
73
12
}
table_reader.cpp:_ZN5doris6format12_GLOBAL__N_131join_table_reader_debug_stringsINS0_11GlobalIndexEZNS1_25table_filter_debug_stringB5cxx11ERKNS0_11TableFilterEE3$_0EENSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEERKSt6vectorIT_SaISF_EET0_
Line
Count
Source
62
1
std::string join_table_reader_debug_strings(const std::vector<T>& values, Formatter formatter) {
63
1
    std::ostringstream out;
64
1
    out << "[";
65
2
    for (size_t i = 0; i < values.size(); ++i) {
66
1
        if (i > 0) {
67
0
            out << ", ";
68
0
        }
69
1
        out << formatter(values[i]);
70
1
    }
71
1
    out << "]";
72
1
    return out.str();
73
1
}
table_reader.cpp:_ZN5doris6format12_GLOBAL__N_131join_table_reader_debug_stringsISt10shared_ptrINS_12VExprContextEEZNKS0_11TableReader12debug_stringB5cxx11EvE3$_2EENSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEERKSt6vectorIT_SaISF_EET0_
Line
Count
Source
62
12
std::string join_table_reader_debug_strings(const std::vector<T>& values, Formatter formatter) {
63
12
    std::ostringstream out;
64
12
    out << "[";
65
13
    for (size_t i = 0; i < values.size(); ++i) {
66
1
        if (i > 0) {
67
0
            out << ", ";
68
0
        }
69
1
        out << formatter(values[i]);
70
1
    }
71
12
    out << "]";
72
12
    return out.str();
73
12
}
table_reader.cpp:_ZN5doris6format12_GLOBAL__N_131join_table_reader_debug_stringsINS0_16ColumnDefinitionEZNKS0_11TableReader12debug_stringB5cxx11EvE3$_3EENSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEERKSt6vectorIT_SaISD_EET0_
Line
Count
Source
62
12
std::string join_table_reader_debug_strings(const std::vector<T>& values, Formatter formatter) {
63
12
    std::ostringstream out;
64
12
    out << "[";
65
14
    for (size_t i = 0; i < values.size(); ++i) {
66
2
        if (i > 0) {
67
1
            out << ", ";
68
1
        }
69
2
        out << formatter(values[i]);
70
2
    }
71
12
    out << "]";
72
12
    return out.str();
73
12
}
table_reader.cpp:_ZN5doris6format12_GLOBAL__N_131join_table_reader_debug_stringsINS0_11TableReader15FileBlockColumnEZNKS3_12debug_stringB5cxx11EvE3$_4EENSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEERKSt6vectorIT_SaISD_EET0_
Line
Count
Source
62
12
std::string join_table_reader_debug_strings(const std::vector<T>& values, Formatter formatter) {
63
12
    std::ostringstream out;
64
12
    out << "[";
65
14
    for (size_t i = 0; i < values.size(); ++i) {
66
2
        if (i > 0) {
67
1
            out << ", ";
68
1
        }
69
2
        out << formatter(values[i]);
70
2
    }
71
12
    out << "]";
72
12
    return out.str();
73
12
}
74
75
12
std::string file_format_to_string(FileFormat format) {
76
12
    switch (format) {
77
5
    case FileFormat::PARQUET:
78
5
        return "PARQUET";
79
1
    case FileFormat::ORC:
80
1
        return "ORC";
81
1
    case FileFormat::CSV:
82
1
        return "CSV";
83
1
    case FileFormat::JSON:
84
1
        return "JSON";
85
1
    case FileFormat::TEXT:
86
1
        return "TEXT";
87
1
    case FileFormat::JNI:
88
1
        return "JNI";
89
1
    case FileFormat::NATIVE:
90
1
        return "NATIVE";
91
1
    case FileFormat::ARROW:
92
1
        return "ARROW";
93
0
    case FileFormat::WAL:
94
0
        return "WAL";
95
12
    }
96
0
    return "UNKNOWN";
97
12
}
98
99
12
std::string push_down_agg_to_string(TPushAggOp::type op) {
100
12
    switch (op) {
101
8
    case TPushAggOp::NONE:
102
8
        return "NONE";
103
1
    case TPushAggOp::COUNT:
104
1
        return "COUNT";
105
1
    case TPushAggOp::MINMAX:
106
1
        return "MINMAX";
107
1
    case TPushAggOp::MIX:
108
1
        return "MIX";
109
1
    case TPushAggOp::COUNT_ON_INDEX:
110
1
        return "COUNT_ON_INDEX";
111
12
    }
112
0
    return "UNKNOWN";
113
12
}
114
115
12
std::string current_file_debug_string(const std::unique_ptr<ScanTask>& task) {
116
12
    if (task == nullptr || task->data_file == nullptr) {
117
11
        return "null";
118
11
    }
119
1
    const auto& file = *task->data_file;
120
1
    std::ostringstream out;
121
1
    out << "FileDescription{path=" << file.path << ", file_size=" << file.file_size
122
1
        << ", range_start_offset=" << file.range_start_offset << ", range_size=" << file.range_size
123
1
        << ", mtime=" << file.mtime << ", fs_name=" << file.fs_name
124
1
        << ", is_immutable=" << file.is_immutable
125
1
        << ", file_cache_admission=" << file.file_cache_admission << "}";
126
1
    return out.str();
127
12
}
128
129
12
std::string partition_values_debug_string(const std::map<std::string, Field>& partition_values) {
130
12
    std::ostringstream out;
131
12
    out << "{";
132
12
    size_t idx = 0;
133
12
    for (const auto& [key, _] : partition_values) {
134
1
        if (idx++ > 0) {
135
0
            out << ", ";
136
0
        }
137
1
        out << key;
138
1
    }
139
12
    out << "}";
140
12
    return out.str();
141
12
}
142
143
241
const schema::external::TField* get_field_ptr(const schema::external::TFieldPtr& field_ptr) {
144
241
    if (!field_ptr.__isset.field_ptr || field_ptr.field_ptr == nullptr) {
145
0
        return nullptr;
146
0
    }
147
241
    return field_ptr.field_ptr.get();
148
241
}
149
150
const schema::external::TField* find_external_field_by_id(
151
0
        const schema::external::TStructField* root, int32_t field_id) {
152
0
    if (root == nullptr || !root->__isset.fields) {
153
0
        return nullptr;
154
0
    }
155
0
    for (const auto& field_ptr : root->fields) {
156
0
        const auto* field = get_field_ptr(field_ptr);
157
0
        if (field == nullptr) {
158
0
            continue;
159
0
        }
160
0
        if (field->__isset.id && field->id == field_id) {
161
0
            return field;
162
0
        }
163
0
        if (!field->__isset.nestedField) {
164
0
            continue;
165
0
        }
166
0
        if (field->nestedField.__isset.struct_field) {
167
0
            if (const auto* result =
168
0
                        find_external_field_by_id(&field->nestedField.struct_field, field_id);
169
0
                result != nullptr) {
170
0
                return result;
171
0
            }
172
0
        } else if (field->nestedField.__isset.array_field &&
173
0
                   field->nestedField.array_field.__isset.item_field) {
174
0
            const auto* child = get_field_ptr(field->nestedField.array_field.item_field);
175
0
            if (child != nullptr) {
176
0
                schema::external::TStructField child_root;
177
0
                child_root.__set_fields({field->nestedField.array_field.item_field});
178
0
                if (const auto* result = find_external_field_by_id(&child_root, field_id);
179
0
                    result != nullptr) {
180
0
                    return result;
181
0
                }
182
0
            }
183
0
        } else if (field->nestedField.__isset.map_field) {
184
0
            schema::external::TStructField child_root;
185
0
            std::vector<schema::external::TFieldPtr> children;
186
0
            if (field->nestedField.map_field.__isset.key_field) {
187
0
                children.push_back(field->nestedField.map_field.key_field);
188
0
            }
189
0
            if (field->nestedField.map_field.__isset.value_field) {
190
0
                children.push_back(field->nestedField.map_field.value_field);
191
0
            }
192
0
            child_root.__set_fields(children);
193
0
            if (const auto* result = find_external_field_by_id(&child_root, field_id);
194
0
                result != nullptr) {
195
0
                return result;
196
0
            }
197
0
        }
198
0
    }
199
0
    return nullptr;
200
0
}
201
202
bool find_external_field_path_by_id(const schema::external::TField* field, int32_t field_id,
203
49
                                    std::vector<const schema::external::TField*>* const path) {
204
49
    DORIS_CHECK(path != nullptr);
205
49
    DORIS_CHECK(field != nullptr);
206
49
    path->push_back(field);
207
49
    if (field->__isset.id && field->id == field_id) {
208
18
        return true;
209
18
    }
210
31
    if (field->__isset.nestedField && field->nestedField.__isset.struct_field &&
211
31
        field->nestedField.struct_field.__isset.fields) {
212
12
        for (const auto& child_ptr : field->nestedField.struct_field.fields) {
213
12
            const auto* child = get_field_ptr(child_ptr);
214
12
            if (child != nullptr && find_external_field_path_by_id(child, field_id, path)) {
215
8
                return true;
216
8
            }
217
12
        }
218
8
    }
219
23
    path->pop_back();
220
23
    return false;
221
31
}
222
223
std::optional<std::vector<const schema::external::TField*>> find_external_struct_field_path_by_id(
224
19
        const schema::external::TSchema& schema, int32_t field_id) {
225
19
    if (!schema.__isset.root_field || !schema.root_field.__isset.fields) {
226
0
        return std::nullopt;
227
0
    }
228
19
    std::vector<const schema::external::TField*> path;
229
37
    for (const auto& field_ptr : schema.root_field.fields) {
230
37
        const auto* field = get_field_ptr(field_ptr);
231
37
        if (field != nullptr && find_external_field_path_by_id(field, field_id, &path)) {
232
18
            return path;
233
18
        }
234
37
    }
235
1
    return std::nullopt;
236
19
}
237
238
bool find_column_identity_path_by_id(const std::vector<ColumnDefinition>& fields, int32_t field_id,
239
94
                                     std::vector<ColumnDefinition>* path) {
240
94
    DORIS_CHECK(path != nullptr);
241
94
    for (const auto& field : fields) {
242
89
        path->push_back(field);
243
89
        if (field.has_identifier_field_id() && field.get_identifier_field_id() == field_id) {
244
32
            return true;
245
32
        }
246
57
        if (find_column_identity_path_by_id(field.children, field_id, path)) {
247
18
            return true;
248
18
        }
249
39
        path->pop_back();
250
39
    }
251
44
    return false;
252
94
}
253
254
131
ColumnDefinition build_schema_identity_from_external_field(const schema::external::TField& field) {
255
131
    ColumnDefinition identity;
256
131
    if (field.__isset.id) {
257
131
        identity.identifier = Field::create_field<TYPE_INT>(field.id);
258
131
    }
259
131
    identity.name = field.__isset.name ? field.name : "";
260
131
    identity.name_mapping =
261
131
            field.__isset.name_mapping ? field.name_mapping : std::vector<std::string> {};
262
131
    identity.has_name_mapping =
263
131
            field.__isset.name_mapping_is_authoritative && field.name_mapping_is_authoritative;
264
131
    if (!field.__isset.nestedField) {
265
111
        return identity;
266
111
    }
267
20
    if (field.nestedField.__isset.struct_field && field.nestedField.struct_field.__isset.fields) {
268
26
        for (const auto& child_ptr : field.nestedField.struct_field.fields) {
269
26
            if (const auto* child = get_field_ptr(child_ptr); child != nullptr) {
270
26
                identity.children.push_back(build_schema_identity_from_external_field(*child));
271
26
            }
272
26
        }
273
20
    } else if (field.nestedField.__isset.array_field &&
274
0
               field.nestedField.array_field.__isset.item_field) {
275
0
        if (const auto* child = get_field_ptr(field.nestedField.array_field.item_field);
276
0
            child != nullptr) {
277
0
            identity.children.push_back(build_schema_identity_from_external_field(*child));
278
0
            identity.children.back().name = "element";
279
0
        }
280
0
    } else if (field.nestedField.__isset.map_field) {
281
0
        if (field.nestedField.map_field.__isset.key_field) {
282
0
            if (const auto* child = get_field_ptr(field.nestedField.map_field.key_field);
283
0
                child != nullptr) {
284
0
                identity.children.push_back(build_schema_identity_from_external_field(*child));
285
0
                identity.children.back().name = "key";
286
0
            }
287
0
        }
288
0
        if (field.nestedField.map_field.__isset.value_field) {
289
0
            if (const auto* child = get_field_ptr(field.nestedField.map_field.value_field);
290
0
                child != nullptr) {
291
0
                identity.children.push_back(build_schema_identity_from_external_field(*child));
292
0
                identity.children.back().name = "value";
293
0
            }
294
0
        }
295
0
    }
296
20
    return identity;
297
131
}
298
299
const ColumnDefinition* find_identity_child(const ColumnDefinition& projected_child,
300
2
                                            const ColumnDefinition& identity_parent) {
301
2
    const auto child_it = std::ranges::find_if(
302
3
            identity_parent.children, [&](const ColumnDefinition& identity_child) {
303
3
                if (projected_child.has_identifier_field_id() &&
304
3
                    identity_child.has_identifier_field_id()) {
305
3
                    return projected_child.get_identifier_field_id() ==
306
3
                           identity_child.get_identifier_field_id();
307
3
                }
308
0
                if (to_lower(projected_child.name) == to_lower(identity_child.name)) {
309
0
                    return true;
310
0
                }
311
0
                return std::ranges::any_of(
312
0
                        identity_child.name_mapping, [&](const std::string& alias) {
313
0
                            return to_lower(projected_child.name) == to_lower(alias);
314
0
                        });
315
0
            });
316
2
    return child_it == identity_parent.children.end() ? nullptr : &*child_it;
317
2
}
318
319
38
void attach_full_schema_identity(ColumnDefinition* projected, const ColumnDefinition& identity) {
320
38
    DORIS_CHECK(projected != nullptr);
321
    // Access-path children control materialization, but wrapper discovery needs sibling IDs that
322
    // were pruned from that projection. Keep the complete identity tree on a separate channel.
323
38
    projected->identity_children = identity.children;
324
38
    for (auto& projected_child : projected->children) {
325
2
        if (const auto* identity_child = find_identity_child(projected_child, identity);
326
2
            identity_child != nullptr) {
327
2
            attach_full_schema_identity(&projected_child, *identity_child);
328
2
        }
329
2
    }
330
38
}
331
332
13
void clear_initial_default_metadata(ColumnDefinition* column) {
333
13
    DORIS_CHECK(column != nullptr);
334
13
    column->initial_default_value.reset();
335
13
    column->initial_default_value_is_base64 = false;
336
13
    for (auto& child : column->children) {
337
6
        clear_initial_default_metadata(&child);
338
6
    }
339
13
}
340
341
15
bool external_field_matches_name(const schema::external::TField& field, const std::string& name) {
342
15
    if (field.__isset.name && to_lower(field.name) == to_lower(name)) {
343
5
        return true;
344
5
    }
345
10
    return field.__isset.name_mapping &&
346
10
           std::ranges::any_of(field.name_mapping, [&](const std::string& alias) {
347
8
               return to_lower(alias) == to_lower(name);
348
8
           });
349
15
}
350
351
DataTypePtr find_struct_child_type_by_external_field(const DataTypeStruct& struct_type,
352
                                                     const schema::external::TField& field,
353
18
                                                     bool prefer_current_name) {
354
18
    if (prefer_current_name && field.__isset.name) {
355
22
        for (size_t field_idx = 0; field_idx < struct_type.get_elements().size(); ++field_idx) {
356
18
            if (to_lower(field.name) == to_lower(struct_type.get_element_name(field_idx))) {
357
11
                return struct_type.get_element(field_idx);
358
11
            }
359
18
        }
360
15
    }
361
10
    for (size_t field_idx = 0; field_idx < struct_type.get_elements().size(); ++field_idx) {
362
8
        const auto& element_name = struct_type.get_element_name(field_idx);
363
8
        if (external_field_matches_name(field, element_name)) {
364
5
            return struct_type.get_element(field_idx);
365
5
        }
366
8
    }
367
2
    return nullptr;
368
7
}
369
370
DataTypePtr restore_current_primitive_type(const schema::external::TField& field,
371
75
                                           DataTypePtr fallback_type) {
372
75
    if (!field.__isset.type) {
373
44
        return fallback_type;
374
44
    }
375
31
    DORIS_CHECK(fallback_type != nullptr);
376
31
    const auto primitive_type = thrift_to_type(field.type.type);
377
31
    if (is_complex_type(primitive_type)) {
378
2
        return fallback_type;
379
2
    }
380
    // The delete file can expose an older physical type, but initial defaults belong to the
381
    // current table field. Restore that type from FE before parsing the default and let the table
382
    // reader apply the normal promotion cast to the delete-key type.
383
29
    return DataTypeFactory::instance().create_data_type(
384
29
            primitive_type, fallback_type->is_nullable(),
385
29
            field.type.__isset.precision ? field.type.precision : 0,
386
29
            field.type.__isset.scale ? field.type.scale : 0,
387
29
            field.type.__isset.len ? field.type.len : -1);
388
31
}
389
390
ColumnDefinition build_schema_column_metadata_from_external_field(
391
75
        const schema::external::TField& field, DataTypePtr type) {
392
75
    type = restore_current_primitive_type(field, std::move(type));
393
75
    return ColumnDefinition {
394
75
            .identifier = field.__isset.id ? Field::create_field<TYPE_INT>(field.id) : Field {},
395
75
            .name = field.__isset.name ? field.name : "",
396
75
            .name_mapping =
397
75
                    field.__isset.name_mapping ? field.name_mapping : std::vector<std::string> {},
398
75
            .has_name_mapping = field.__isset.name_mapping_is_authoritative &&
399
75
                                field.name_mapping_is_authoritative,
400
75
            .type = std::move(type),
401
75
            .children = {},
402
75
            .default_expr = nullptr,
403
75
            .initial_default_value = field.__isset.initial_default_value
404
75
                                             ? std::make_optional(field.initial_default_value)
405
75
                                             : std::nullopt,
406
75
            .initial_default_value_is_base64 = field.__isset.initial_default_value_is_base64 &&
407
75
                                               field.initial_default_value_is_base64,
408
75
            .is_optional = field.__isset.is_optional ? std::make_optional(field.is_optional)
409
75
                                                     : std::nullopt,
410
75
            .timestamp_is_adjusted_to_utc =
411
75
                    field.__isset.timestamp_is_adjusted_to_utc
412
75
                            ? std::make_optional(field.timestamp_is_adjusted_to_utc)
413
75
                            : std::nullopt,
414
75
            .is_partition_key = false,
415
75
    };
416
75
}
417
418
// NOLINTNEXTLINE(readability-function-size): keep recursive Iceberg type reconstruction together.
419
ColumnDefinition build_schema_column_from_external_field(const schema::external::TField& field,
420
                                                         DataTypePtr type,
421
49
                                                         bool prefer_current_name) {
422
49
    auto column = build_schema_column_metadata_from_external_field(field, std::move(type));
423
49
    if (column.type == nullptr || !field.__isset.nestedField) {
424
33
        return column;
425
33
    }
426
427
16
    const auto nested_type = remove_nullable(column.type);
428
16
    switch (nested_type->get_primitive_type()) {
429
10
    case TYPE_STRUCT: {
430
10
        if (!field.nestedField.__isset.struct_field ||
431
10
            !field.nestedField.struct_field.__isset.fields) {
432
0
            return column;
433
0
        }
434
10
        const auto& struct_type = assert_cast<const DataTypeStruct&>(*nested_type);
435
18
        for (const auto& child_ptr : field.nestedField.struct_field.fields) {
436
18
            const auto* child_field = get_field_ptr(child_ptr);
437
18
            if (child_field == nullptr || !child_field->__isset.name) {
438
0
                continue;
439
0
            }
440
18
            auto child_type = find_struct_child_type_by_external_field(struct_type, *child_field,
441
18
                                                                       prefer_current_name);
442
18
            if (child_type == nullptr) {
443
2
                continue;
444
2
            }
445
16
            column.children.push_back(build_schema_column_from_external_field(
446
16
                    *child_field, child_type, prefer_current_name));
447
16
        }
448
10
        break;
449
10
    }
450
3
    case TYPE_ARRAY: {
451
3
        if (!field.nestedField.__isset.array_field ||
452
3
            !field.nestedField.array_field.__isset.item_field) {
453
0
            return column;
454
0
        }
455
3
        const auto* item_field = get_field_ptr(field.nestedField.array_field.item_field);
456
3
        if (item_field == nullptr) {
457
0
            return column;
458
0
        }
459
3
        const auto& array_type = assert_cast<const DataTypeArray&>(*nested_type);
460
3
        auto child = build_schema_column_from_external_field(
461
3
                *item_field, array_type.get_nested_type(), prefer_current_name);
462
3
        child.name = "element";
463
3
        if (child.has_identifier_name()) {
464
0
            child.identifier = Field::create_field<TYPE_STRING>(child.name);
465
0
        }
466
3
        column.children.push_back(std::move(child));
467
3
        break;
468
3
    }
469
3
    case TYPE_MAP: {
470
3
        if (!field.nestedField.__isset.map_field ||
471
3
            !field.nestedField.map_field.__isset.key_field ||
472
3
            !field.nestedField.map_field.__isset.value_field) {
473
0
            return column;
474
0
        }
475
3
        const auto& map_type = assert_cast<const DataTypeMap&>(*nested_type);
476
3
        const auto* key_field = get_field_ptr(field.nestedField.map_field.key_field);
477
3
        if (key_field != nullptr) {
478
3
            auto child = build_schema_column_from_external_field(
479
3
                    *key_field, map_type.get_key_type(), prefer_current_name);
480
3
            child.name = "key";
481
3
            if (child.has_identifier_name()) {
482
0
                child.identifier = Field::create_field<TYPE_STRING>(child.name);
483
0
            }
484
3
            column.children.push_back(std::move(child));
485
3
        }
486
3
        const auto* value_field = get_field_ptr(field.nestedField.map_field.value_field);
487
3
        if (value_field != nullptr) {
488
3
            auto child = build_schema_column_from_external_field(
489
3
                    *value_field, map_type.get_value_type(), prefer_current_name);
490
3
            child.name = "value";
491
3
            if (child.has_identifier_name()) {
492
0
                child.identifier = Field::create_field<TYPE_STRING>(child.name);
493
0
            }
494
3
            column.children.push_back(std::move(child));
495
3
        }
496
3
        break;
497
3
    }
498
0
    default:
499
0
        break;
500
16
    }
501
16
    return column;
502
16
}
503
504
const schema::external::TField* find_external_root_field(const TFileScanRangeParams* params,
505
107
                                                         const ColumnDefinition& column) {
506
107
    if (params == nullptr || !params->__isset.history_schema_info ||
507
107
        params->history_schema_info.empty()) {
508
46
        return nullptr;
509
46
    }
510
61
    const auto* schema = &params->history_schema_info.front();
511
61
    if (params->__isset.current_schema_id) {
512
62
        for (const auto& candidate_schema : params->history_schema_info) {
513
62
            if (candidate_schema.__isset.schema_id &&
514
62
                candidate_schema.schema_id == params->current_schema_id) {
515
61
                schema = &candidate_schema;
516
61
                break;
517
61
            }
518
62
        }
519
61
    }
520
61
    if (!schema->__isset.root_field || !schema->root_field.__isset.fields) {
521
0
        return nullptr;
522
0
    }
523
61
    if (!supports_iceberg_scan_semantics_v1(params)) {
524
        // Old BEs used one ordered current-name/alias pass. Preserve that result for old-FE plans
525
        // until the explicit scan-semantics marker makes exact-name precedence cluster-wide.
526
7
        for (const auto& field_ptr : schema->root_field.fields) {
527
7
            const auto* field = get_field_ptr(field_ptr);
528
7
            if (field != nullptr && external_field_matches_name(*field, column.name)) {
529
7
                return field;
530
7
            }
531
7
        }
532
0
        return nullptr;
533
7
    }
534
    // A reused name identifies the newly added field, not an older sibling that retained that
535
    // spelling as an alias. Exhaust exact current names before consulting historical aliases.
536
62
    for (const auto& field_ptr : schema->root_field.fields) {
537
62
        const auto* field = get_field_ptr(field_ptr);
538
62
        if (field != nullptr && field->__isset.name &&
539
62
            to_lower(field->name) == to_lower(column.name)) {
540
53
            return field;
541
53
        }
542
62
    }
543
1
    for (const auto& field_ptr : schema->root_field.fields) {
544
1
        const auto* field = get_field_ptr(field_ptr);
545
1
        if (field != nullptr && field->__isset.name_mapping &&
546
1
            std::ranges::any_of(field->name_mapping, [&](const std::string& alias) {
547
0
                return to_lower(alias) == to_lower(column.name);
548
0
            })) {
549
0
            return field;
550
0
        }
551
1
    }
552
1
    return nullptr;
553
1
}
554
555
2
std::string expr_context_debug_string(const VExprContextSPtr& context) {
556
2
    if (context == nullptr) {
557
0
        return "null";
558
0
    }
559
2
    const auto root = context->root();
560
2
    if (root == nullptr) {
561
0
        return "VExprContext{root=null}";
562
0
    }
563
2
    std::ostringstream out;
564
2
    out << "VExprContext{root_name=" << root->expr_name() << ", root_debug=" << root->debug_string()
565
2
        << "}";
566
2
    return out.str();
567
2
}
568
569
1
std::string table_filter_debug_string(const TableFilter& filter) {
570
1
    std::ostringstream out;
571
1
    out << "TableFilter{conjunct=" << expr_context_debug_string(filter.conjunct)
572
1
        << ", global_indices="
573
1
        << join_table_reader_debug_strings(
574
1
                   filter.global_indices,
575
1
                   [](GlobalIndex global_index) { return std::to_string(global_index.value()); })
576
1
        << "}";
577
1
    return out.str();
578
1
}
579
580
4
bool contains_runtime_filter(const VExprContextSPtrs& conjuncts) {
581
4
    return std::ranges::any_of(conjuncts, [](const auto& conjunct) {
582
4
        return conjunct != nullptr && conjunct->root() != nullptr &&
583
4
               conjunct->root()->is_rf_wrapper();
584
4
    });
585
4
}
586
587
131
void collect_global_indices(const VExprSPtr& expr, std::set<GlobalIndex>* global_indices) {
588
131
    if (expr == nullptr) {
589
0
        return;
590
0
    }
591
131
    if (expr->is_rf_wrapper()) {
592
        // RuntimeFilterExpr wraps a real predicate expression but its own thrift node can still
593
        // look like SLOT_REF. Collect indices from the wrapped predicate; do not cast the wrapper
594
        // itself to VSlotRef.
595
10
        collect_global_indices(expr->get_impl(), global_indices);
596
10
        return;
597
10
    }
598
121
    if (expr->is_slot_ref()) {
599
40
        const auto* slot_ref = assert_cast<const VSlotRef*>(expr.get());
600
40
        DORIS_CHECK(slot_ref->column_id() >= 0);
601
40
        global_indices->insert(GlobalIndex(cast_set<size_t>(slot_ref->column_id())));
602
40
    }
603
121
    for (const auto& child : expr->children()) {
604
80
        collect_global_indices(child, global_indices);
605
80
    }
606
121
}
607
608
Status build_table_filters_from_conjunct(const VExprContextSPtr& conjunct, RuntimeState* state,
609
39
                                         std::vector<TableFilter>* table_filters) {
610
39
    if (conjunct == nullptr) {
611
0
        return Status::OK();
612
0
    }
613
39
    std::set<GlobalIndex> global_indices;
614
39
    collect_global_indices(conjunct->root(), &global_indices);
615
39
    if (!global_indices.empty()) {
616
37
        TableFilter table_filter;
617
37
        VExprSPtr filter_root;
618
37
        RETURN_IF_ERROR(clone_table_expr_tree(conjunct->root(), &filter_root));
619
37
        table_filter.conjunct = VExprContext::create_shared(std::move(filter_root));
620
38
        for (const auto global_index : global_indices) {
621
38
            table_filter.global_indices.push_back(global_index);
622
38
        }
623
37
        table_filters->push_back(std::move(table_filter));
624
37
    }
625
39
    return Status::OK();
626
39
}
627
628
Status parse_deletion_vector(const char* buf, size_t buffer_size, DeleteFileDesc::Format format,
629
5
                             DeletionVector* deletion_vector) {
630
5
    DORIS_CHECK(buf != nullptr);
631
5
    DORIS_CHECK(deletion_vector != nullptr);
632
5
    DORIS_CHECK(format == DeleteFileDesc::Format::PAIMON ||
633
5
                format == DeleteFileDesc::Format::ICEBERG);
634
635
5
    if (format == DeleteFileDesc::Format::PAIMON) {
636
1
        RETURN_IF_ERROR(decode_paimon_deletion_vector_buffer(buf, buffer_size, deletion_vector));
637
1
        return Status::OK();
638
1
    }
639
640
4
    return decode_iceberg_deletion_vector_buffer(buf, buffer_size, deletion_vector);
641
5
}
642
643
} // namespace
644
645
std::shared_ptr<io::FileSystemProperties> create_system_properties(
646
229
        const TFileScanRangeParams* scan_params) {
647
229
    auto system_properties = std::make_shared<io::FileSystemProperties>();
648
229
    if (scan_params == nullptr || !scan_params->__isset.file_type) {
649
145
        system_properties->system_type = TFileType::FILE_LOCAL;
650
145
        return system_properties;
651
145
    }
652
84
    system_properties->system_type = scan_params->file_type;
653
84
    system_properties->properties = scan_params->properties;
654
84
    system_properties->hdfs_params = scan_params->hdfs_params;
655
84
    if (scan_params->__isset.broker_addresses) {
656
0
        system_properties->broker_addresses.assign(scan_params->broker_addresses.begin(),
657
0
                                                   scan_params->broker_addresses.end());
658
0
    }
659
84
    return system_properties;
660
229
}
661
662
12
std::string TableReader::debug_string() const {
663
12
    std::ostringstream out;
664
12
    out << "TableReader{format=" << file_format_to_string(_format)
665
12
        << ", push_down_agg_type=" << push_down_agg_to_string(_push_down_agg_type)
666
12
        << ", aggregate_pushdown_tried=" << _aggregate_pushdown_tried
667
12
        << ", has_current_reader=" << (_data_reader.reader != nullptr)
668
12
        << ", has_current_task=" << (_current_task != nullptr)
669
12
        << ", current_file=" << current_file_debug_string(_current_task)
670
12
        << ", has_delete_rows=" << (_delete_rows != nullptr)
671
12
        << ", delete_row_count=" << (_delete_rows == nullptr ? 0 : _delete_rows->size())
672
12
        << ", has_deletion_vector=" << (_deletion_vector != nullptr)
673
12
        << ", deletion_vector_cardinality="
674
12
        << (_deletion_vector == nullptr ? 0 : _deletion_vector->cardinality())
675
12
        << ", has_system_properties=" << (_system_properties != nullptr) << ", system_type="
676
12
        << (_system_properties == nullptr ? static_cast<int>(TFileType::FILE_LOCAL)
677
12
                                          : static_cast<int>(_system_properties->system_type))
678
12
        << ", has_scan_params=" << (_scan_params != nullptr)
679
12
        << ", has_io_ctx=" << (_io_ctx != nullptr)
680
12
        << ", has_runtime_state=" << (_runtime_state != nullptr)
681
12
        << ", has_scanner_profile=" << (_scanner_profile != nullptr)
682
12
        << ", mapper_options=" << _mapper_options.debug_string() << ", projected_columns="
683
12
        << join_table_reader_debug_strings(
684
12
                   _projected_columns,
685
12
                   [](const ColumnDefinition& column) { return column.debug_string(); })
686
12
        << ", partition_values=" << partition_values_debug_string(_partition_values)
687
12
        << ", table_filters="
688
12
        << join_table_reader_debug_strings(
689
12
                   _table_filters,
690
12
                   [](const TableFilter& filter) { return table_filter_debug_string(filter); })
691
12
        << ", conjunct_count=" << _conjuncts.size() << ", conjuncts="
692
12
        << join_table_reader_debug_strings(_conjuncts,
693
12
                                           [](const VExprContextSPtr& conjunct) {
694
1
                                               return expr_context_debug_string(conjunct);
695
1
                                           })
696
12
        << ", file_schema="
697
12
        << join_table_reader_debug_strings(
698
12
                   _data_reader.file_schema,
699
12
                   [](const ColumnDefinition& field) { return field.debug_string(); })
700
12
        << ", file_block_layout="
701
12
        << join_table_reader_debug_strings(
702
12
                   _data_reader.file_block_layout,
703
12
                   [](const FileBlockColumn& column) {
704
2
                       std::ostringstream column_out;
705
2
                       column_out << "FileBlockColumn{file_column_id=" << column.file_column_id
706
2
                                  << ", name=" << column.name << ", type="
707
2
                                  << (column.type == nullptr ? "null" : column.type->get_name())
708
2
                                  << "}";
709
2
                       return column_out.str();
710
2
                   })
711
12
        << ", block_template_columns=" << _data_reader.block_template.columns()
712
12
        << ", column_mapper="
713
12
        << (_data_reader.column_mapper == nullptr ? "null"
714
12
                                                  : _data_reader.column_mapper->debug_string())
715
12
        << "}";
716
12
    return out.str();
717
12
}
718
719
Status TableReader::annotate_projected_column(const TFileScanSlotInfo& slot_info,
720
                                              ProjectedColumnBuildContext* context,
721
31
                                              ColumnDefinition* column) const {
722
31
    (void)slot_info;
723
31
    DORIS_CHECK(context != nullptr);
724
31
    DORIS_CHECK(column != nullptr);
725
31
    context->schema_column.reset();
726
31
    const auto* schema_field = find_external_root_field(context->scan_params, *column);
727
31
    if (schema_field == nullptr) {
728
7
        return Status::OK();
729
7
    }
730
24
    const bool use_current_semantics = supports_iceberg_scan_semantics_v1(context->scan_params);
731
24
    context->schema_column = build_schema_column_from_external_field(*schema_field, column->type,
732
24
                                                                     use_current_semantics);
733
24
    if (!use_current_semantics) {
734
        // IDs and encoded defaults predate the result-changing semantics. Strip only the new
735
        // default channel so an old-FE plan keeps the same generic root/nested values on every BE.
736
7
        clear_initial_default_metadata(&*context->schema_column);
737
7
    }
738
24
    column->identifier = context->schema_column->identifier;
739
24
    column->name_mapping = context->schema_column->name_mapping;
740
24
    column->has_name_mapping = context->schema_column->has_name_mapping;
741
    // Projected roots already carry a generic FE default expression, but Iceberg binary defaults
742
    // need the raw Base64 marker so missing-file materialization can decode rather than copy text.
743
24
    column->initial_default_value = context->schema_column->initial_default_value;
744
24
    column->initial_default_value_is_base64 =
745
24
            context->schema_column->initial_default_value_is_base64;
746
24
    return Status::OK();
747
31
}
748
749
std::optional<ColumnDefinition> TableReader::_find_table_column_by_field_id(
750
1
        int32_t field_id, DataTypePtr type, bool include_historical_schemas) const {
751
1
    if (_scan_params == nullptr || !_scan_params->__isset.history_schema_info ||
752
1
        _scan_params->history_schema_info.empty()) {
753
1
        return std::nullopt;
754
1
    }
755
0
    const auto find_field = [field_id](const schema::external::TSchema& schema) {
756
0
        return schema.__isset.root_field ? find_external_field_by_id(&schema.root_field, field_id)
757
0
                                         : nullptr;
758
0
    };
759
760
0
    const auto* current_schema = &_scan_params->history_schema_info.front();
761
0
    if (_scan_params->__isset.current_schema_id) {
762
0
        for (const auto& candidate_schema : _scan_params->history_schema_info) {
763
0
            if (candidate_schema.__isset.schema_id &&
764
0
                candidate_schema.schema_id == _scan_params->current_schema_id) {
765
0
                current_schema = &candidate_schema;
766
0
                break;
767
0
            }
768
0
        }
769
0
    }
770
0
    if (const auto* field = find_field(*current_schema); field != nullptr) {
771
0
        return build_schema_column_from_external_field(
772
0
                *field, std::move(type), supports_iceberg_scan_semantics_v1(_scan_params));
773
0
    }
774
0
    if (!include_historical_schemas) {
775
0
        return std::nullopt;
776
0
    }
777
778
0
    const schema::external::TSchema* latest_schema = nullptr;
779
0
    const schema::external::TField* latest_field = nullptr;
780
0
    for (const auto& candidate_schema : _scan_params->history_schema_info) {
781
0
        if (&candidate_schema == current_schema) {
782
0
            continue;
783
0
        }
784
0
        const auto* candidate_field = find_field(candidate_schema);
785
0
        if (candidate_field == nullptr) {
786
0
            continue;
787
0
        }
788
0
        if (latest_schema == nullptr || (candidate_schema.__isset.schema_id &&
789
0
                                         (!latest_schema->__isset.schema_id ||
790
0
                                          candidate_schema.schema_id > latest_schema->schema_id))) {
791
0
            latest_schema = &candidate_schema;
792
0
            latest_field = candidate_field;
793
0
        }
794
0
    }
795
0
    if (latest_field == nullptr) {
796
0
        return std::nullopt;
797
0
    }
798
0
    return build_schema_column_from_external_field(
799
0
            *latest_field, std::move(type), supports_iceberg_scan_semantics_v1(_scan_params));
800
0
}
801
802
std::optional<std::vector<ColumnDefinition>> TableReader::_find_table_column_path_by_field_id(
803
19
        int32_t field_id, DataTypePtr leaf_type, bool include_historical_schemas) const {
804
19
    if (_scan_params == nullptr || !_scan_params->__isset.history_schema_info ||
805
19
        _scan_params->history_schema_info.empty()) {
806
1
        return std::nullopt;
807
1
    }
808
18
    const auto build_path = [&](const schema::external::TSchema& schema)
809
19
            -> std::optional<std::vector<ColumnDefinition>> {
810
19
        auto external_path = find_external_struct_field_path_by_id(schema, field_id);
811
19
        if (!external_path.has_value()) {
812
1
            return std::nullopt;
813
1
        }
814
815
18
        std::vector<DataTypePtr> path_types(external_path->size());
816
18
        path_types.back() = leaf_type;
817
26
        for (size_t index = external_path->size(); index > 1; --index) {
818
8
            const auto* parent = (*external_path)[index - 2];
819
8
            const auto* child = (*external_path)[index - 1];
820
8
            DORIS_CHECK(parent != nullptr);
821
8
            DORIS_CHECK(child != nullptr);
822
8
            DORIS_CHECK(child->__isset.name);
823
8
            if (!parent->__isset.nestedField || !parent->nestedField.__isset.struct_field) {
824
0
                return std::nullopt;
825
0
            }
826
8
            DataTypePtr path_type = std::make_shared<DataTypeStruct>(
827
8
                    DataTypes {path_types[index - 1]}, Strings {child->name});
828
8
            if (parent->__isset.is_optional && parent->is_optional) {
829
8
                path_type = make_nullable(path_type);
830
8
            }
831
8
            path_types[index - 2] = std::move(path_type);
832
8
        }
833
834
18
        std::vector<ColumnDefinition> result;
835
18
        result.reserve(external_path->size());
836
44
        for (size_t index = 0; index < external_path->size(); ++index) {
837
26
            result.push_back(build_schema_column_metadata_from_external_field(
838
26
                    *(*external_path)[index], path_types[index]));
839
26
        }
840
        // Keep metadata hierarchy aligned with the synthetic exact-ID ancestor types.
841
26
        for (size_t index = result.size(); index > 1; --index) {
842
8
            result[index - 2].children.push_back(result[index - 1]);
843
8
        }
844
18
        return result;
845
18
    };
846
847
18
    const auto* current_schema = &_scan_params->history_schema_info.front();
848
18
    if (_scan_params->__isset.current_schema_id) {
849
18
        for (const auto& candidate_schema : _scan_params->history_schema_info) {
850
18
            if (candidate_schema.__isset.schema_id &&
851
18
                candidate_schema.schema_id == _scan_params->current_schema_id) {
852
18
                current_schema = &candidate_schema;
853
18
                break;
854
18
            }
855
18
        }
856
18
    }
857
18
    if (auto path = build_path(*current_schema); path.has_value()) {
858
17
        return path;
859
17
    }
860
1
    if (!include_historical_schemas) {
861
0
        return std::nullopt;
862
0
    }
863
864
1
    const schema::external::TSchema* latest_schema = nullptr;
865
1
    std::optional<std::vector<ColumnDefinition>> latest_path;
866
2
    for (const auto& candidate_schema : _scan_params->history_schema_info) {
867
2
        if (&candidate_schema == current_schema) {
868
1
            continue;
869
1
        }
870
1
        auto candidate_path = build_path(candidate_schema);
871
1
        if (!candidate_path.has_value()) {
872
0
            continue;
873
0
        }
874
1
        if (latest_schema == nullptr || (candidate_schema.__isset.schema_id &&
875
0
                                         (!latest_schema->__isset.schema_id ||
876
1
                                          candidate_schema.schema_id > latest_schema->schema_id))) {
877
1
            latest_schema = &candidate_schema;
878
1
            latest_path = std::move(candidate_path);
879
1
        }
880
1
    }
881
1
    return latest_path;
882
1
}
883
884
std::optional<std::vector<ColumnDefinition>>
885
TableReader::_find_table_column_identity_path_by_field_id(int32_t field_id,
886
41
                                                          bool include_historical_schemas) const {
887
41
    if (_scan_params == nullptr || !_scan_params->__isset.history_schema_info ||
888
41
        _scan_params->history_schema_info.empty()) {
889
9
        return std::nullopt;
890
9
    }
891
32
    const auto find_path = [field_id](const schema::external::TSchema& schema)
892
37
            -> std::optional<std::vector<ColumnDefinition>> {
893
37
        if (!schema.__isset.root_field || !schema.root_field.__isset.fields) {
894
0
            return std::nullopt;
895
0
        }
896
37
        std::vector<ColumnDefinition> roots;
897
37
        roots.reserve(schema.root_field.fields.size());
898
69
        for (const auto& field_ptr : schema.root_field.fields) {
899
69
            const auto* field = get_field_ptr(field_ptr);
900
69
            if (field != nullptr) {
901
69
                roots.push_back(build_schema_identity_from_external_field(*field));
902
69
            }
903
69
        }
904
37
        std::vector<ColumnDefinition> path;
905
37
        if (find_column_identity_path_by_id(roots, field_id, &path)) {
906
32
            return path;
907
32
        }
908
5
        return std::nullopt;
909
37
    };
910
911
32
    const auto* current_schema = &_scan_params->history_schema_info.front();
912
32
    if (_scan_params->__isset.current_schema_id) {
913
32
        for (const auto& candidate_schema : _scan_params->history_schema_info) {
914
32
            if (candidate_schema.__isset.schema_id &&
915
32
                candidate_schema.schema_id == _scan_params->current_schema_id) {
916
32
                current_schema = &candidate_schema;
917
32
                break;
918
32
            }
919
32
        }
920
32
    }
921
32
    if (auto path = find_path(*current_schema); path.has_value()) {
922
27
        return path;
923
27
    }
924
5
    if (!include_historical_schemas) {
925
0
        return std::nullopt;
926
0
    }
927
928
5
    const schema::external::TSchema* latest_schema = nullptr;
929
5
    std::optional<std::vector<ColumnDefinition>> latest_path;
930
10
    for (const auto& candidate_schema : _scan_params->history_schema_info) {
931
10
        if (&candidate_schema == current_schema) {
932
5
            continue;
933
5
        }
934
5
        auto candidate_path = find_path(candidate_schema);
935
5
        if (!candidate_path.has_value()) {
936
0
            continue;
937
0
        }
938
5
        if (latest_schema == nullptr || (candidate_schema.__isset.schema_id &&
939
0
                                         (!latest_schema->__isset.schema_id ||
940
5
                                          candidate_schema.schema_id > latest_schema->schema_id))) {
941
5
            latest_schema = &candidate_schema;
942
5
            latest_path = std::move(candidate_path);
943
5
        }
944
5
    }
945
5
    return latest_path;
946
5
}
947
948
229
Status TableReader::init(TableReadOptions&& options) {
949
229
    _scanner_profile = options.scanner_profile;
950
229
    if (_scanner_profile != nullptr) {
951
94
        const auto hierarchy = file_scan_profile::ensure_hierarchy(_scanner_profile);
952
94
        static const char* table_profile = file_scan_profile::TABLE_READER;
953
94
        static const char* file_reader_profile = file_scan_profile::FILE_READER;
954
94
        _profile.total_timer = hierarchy.table_reader;
955
94
        _profile.file_reader_total_timer = hierarchy.file_reader;
956
94
        _profile.init_timer =
957
94
                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "InitTime", table_profile, 1);
958
94
        _profile.num_delete_files = ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, "NumDeleteFiles",
959
94
                                                                 TUnit::UNIT, table_profile, 1);
960
94
        _profile.num_delete_rows = ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, "NumDeleteRows",
961
94
                                                                TUnit::UNIT, table_profile, 1);
962
94
        _profile.parse_delete_file_time = ADD_CHILD_TIMER_WITH_LEVEL(
963
94
                _scanner_profile, "ParseDeleteFileTime", table_profile, 1);
964
94
        _profile.decoded_dv_cache_hit_count =
965
94
                ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, "DeletionVectorDecodedCacheHitCount",
966
94
                                             TUnit::UNIT, table_profile, 1);
967
94
        _profile.decoded_dv_cache_miss_count = ADD_CHILD_COUNTER_WITH_LEVEL(
968
94
                _scanner_profile, "DeletionVectorDecodedCacheMissCount", TUnit::UNIT, table_profile,
969
94
                1);
970
94
        _profile.dv_file_cache_hit_count = ADD_CHILD_COUNTER_WITH_LEVEL(
971
94
                _scanner_profile, "DeletionVectorFileCacheHitCount", TUnit::UNIT, table_profile, 1);
972
94
        _profile.dv_file_cache_miss_count =
973
94
                ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, "DeletionVectorFileCacheMissCount",
974
94
                                             TUnit::UNIT, table_profile, 1);
975
94
        _profile.dv_file_cache_peer_read_count = ADD_CHILD_COUNTER_WITH_LEVEL(
976
94
                _scanner_profile, "DeletionVectorFileCachePeerReadCount", TUnit::UNIT,
977
94
                table_profile, 1);
978
94
        _profile.exec_timer =
979
94
                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "GetBlockTime", table_profile, 1);
980
94
        _profile.prepare_split_timer =
981
94
                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "PrepareSplitTime", table_profile, 1);
982
94
        _profile.finalize_timer =
983
94
                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "FinalizeBlockTime", table_profile, 1);
984
94
        _profile.create_reader_timer =
985
94
                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "CreateReaderTime", table_profile, 1);
986
94
        _profile.pushdown_agg_timer =
987
94
                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "PushDownAggTime", table_profile, 1);
988
94
        _profile.open_reader_timer =
989
94
                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "OpenReaderTime", table_profile, 1);
990
94
        _profile.refresh_conjuncts_timer = ADD_CHILD_TIMER_WITH_LEVEL(
991
94
                _scanner_profile, "RefreshConjunctsTime", table_profile, 1);
992
94
        _profile.runtime_filter_partition_prune_timer = ADD_CHILD_TIMER_WITH_LEVEL(
993
94
                _scanner_profile, "FileScannerRuntimeFilterPartitionPruningTime", table_profile, 1);
994
94
        _profile.runtime_filter_partition_pruned_range_counter = ADD_CHILD_COUNTER_WITH_LEVEL(
995
94
                _scanner_profile, "RuntimeFilterPartitionPrunedRangeNum", TUnit::UNIT,
996
94
                table_profile, 1);
997
94
        _profile.close_timer =
998
94
                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "CloseTime", table_profile, 1);
999
        // Lifecycle timer names remain globally unique because RuntimeProfile's visual hierarchy
1000
        // does not namespace counters that share the same display parent.
1001
94
        _profile.file_reader_init_timer = ADD_CHILD_TIMER_WITH_LEVEL(
1002
94
                _scanner_profile, "FileReaderInitTime", file_reader_profile, 1);
1003
94
        _profile.file_reader_schema_timer = ADD_CHILD_TIMER_WITH_LEVEL(
1004
94
                _scanner_profile, "FileReaderGetSchemaTime", file_reader_profile, 1);
1005
94
        _profile.file_reader_mapper_timer = ADD_CHILD_TIMER_WITH_LEVEL(
1006
94
                _scanner_profile, "FileReaderCreateColumnMapperTime", file_reader_profile, 1);
1007
94
        _profile.file_reader_open_timer = ADD_CHILD_TIMER_WITH_LEVEL(
1008
94
                _scanner_profile, "FileReaderOpenTime", file_reader_profile, 1);
1009
94
        _profile.file_reader_refresh_timer = ADD_CHILD_TIMER_WITH_LEVEL(
1010
94
                _scanner_profile, "FileReaderRefreshScanRequestTime", file_reader_profile, 1);
1011
94
        _profile.file_reader_get_block_timer = ADD_CHILD_TIMER_WITH_LEVEL(
1012
94
                _scanner_profile, "FileReaderGetBlockTime", file_reader_profile, 1);
1013
94
        _profile.file_reader_aggregate_timer = ADD_CHILD_TIMER_WITH_LEVEL(
1014
94
                _scanner_profile, "FileReaderAggregatePushDownTime", file_reader_profile, 1);
1015
94
        _profile.file_reader_close_timer = ADD_CHILD_TIMER_WITH_LEVEL(
1016
94
                _scanner_profile, "FileReaderCloseTime", file_reader_profile, 1);
1017
94
    }
1018
    // Establish lifecycle timers before consuming options or constructing filesystem properties;
1019
    // placing these scopes at the tail records only scope teardown and hides expensive init work.
1020
229
    SCOPED_TIMER(_profile.total_timer);
1021
229
    SCOPED_TIMER(_profile.init_timer);
1022
229
    _scan_params = options.scan_params;
1023
229
    _format = options.format;
1024
229
    _io_ctx = options.io_ctx;
1025
229
    _runtime_state = options.runtime_state;
1026
229
    _file_slot_descs = options.file_slot_descs;
1027
229
    _push_down_agg_type = options.push_down_agg_type;
1028
229
    _push_down_count_columns = options.push_down_count_columns;
1029
229
    _initial_condition_cache_digest = options.condition_cache_digest;
1030
229
    _condition_cache_digest = _initial_condition_cache_digest;
1031
229
    _projected_columns = std::move(options.projected_columns);
1032
229
    if (supports_iceberg_scan_semantics_v1(_scan_params)) {
1033
76
        for (auto& projected_column : _projected_columns) {
1034
76
            const auto* schema_field = find_external_root_field(_scan_params, projected_column);
1035
76
            if (schema_field != nullptr) {
1036
36
                attach_full_schema_identity(
1037
36
                        &projected_column,
1038
36
                        build_schema_identity_from_external_field(*schema_field));
1039
36
            }
1040
76
        }
1041
68
    }
1042
229
    _system_properties = create_system_properties(_scan_params);
1043
229
    _mapper_options.mode = TableColumnMappingMode::BY_NAME;
1044
229
    _conjuncts = std::move(options.conjuncts);
1045
229
    return Status::OK();
1046
229
}
1047
1048
154
Status TableReader::_build_table_filters_from_conjuncts() {
1049
154
    _table_filters.clear();
1050
154
    _constant_pruning_safe_filter_count = 0;
1051
154
    bool in_safe_prefix = true;
1052
154
    for (const auto& conjunct : _conjuncts) {
1053
39
        DORIS_CHECK(conjunct != nullptr);
1054
39
        DORIS_CHECK(conjunct->root() != nullptr);
1055
        // `_table_filters` omits expressions without slot references, but such an expression still
1056
        // occupies a position in the row-level conjunct order. Record how many localized filters
1057
        // precede the first unsafe original conjunct so constant pruning cannot jump over a
1058
        // slotless non-deterministic/error-preserving barrier. Unsafe predicates remain solely on
1059
        // Scanner's original row-level path because localizing a clone would execute their state
1060
        // twice with independent state.
1061
39
        if (in_safe_prefix && !_is_safe_to_pre_execute(conjunct)) {
1062
6
            in_safe_prefix = false;
1063
6
        }
1064
39
        RETURN_IF_ERROR(
1065
39
                build_table_filters_from_conjunct(conjunct, _runtime_state, &_table_filters));
1066
39
        if (in_safe_prefix) {
1067
31
            _constant_pruning_safe_filter_count = _table_filters.size();
1068
31
        }
1069
39
    }
1070
154
    return Status::OK();
1071
154
}
1072
1073
namespace {
1074
1075
4
bool same_scan_projection(const LocalColumnIndex& lhs, const LocalColumnIndex& rhs) {
1076
4
    if (lhs.index != rhs.index || lhs.project_all_children != rhs.project_all_children ||
1077
4
        lhs.children.size() != rhs.children.size()) {
1078
0
        return false;
1079
0
    }
1080
4
    for (size_t index = 0; index < lhs.children.size(); ++index) {
1081
0
        if (!same_scan_projection(lhs.children[index], rhs.children[index])) {
1082
0
            return false;
1083
0
        }
1084
0
    }
1085
4
    return true;
1086
4
}
1087
1088
const LocalColumnIndex* find_scan_projection(const FileScanRequest& request,
1089
8
                                             LocalColumnId column_id) {
1090
13
    const auto find_by_id = [column_id](const std::vector<LocalColumnIndex>& projections) {
1091
13
        return std::ranges::find_if(projections, [column_id](const LocalColumnIndex& projection) {
1092
10
            return projection.column_id() == column_id;
1093
10
        });
1094
13
    };
1095
8
    auto it = find_by_id(request.predicate_columns);
1096
8
    if (it != request.predicate_columns.end()) {
1097
3
        return &*it;
1098
3
    }
1099
5
    it = find_by_id(request.non_predicate_columns);
1100
5
    return it == request.non_predicate_columns.end() ? nullptr : &*it;
1101
8
}
1102
1103
3
bool same_physical_scan_layout(const FileScanRequest& lhs, const FileScanRequest& rhs) {
1104
3
    if (lhs.local_positions != rhs.local_positions) {
1105
0
        return false;
1106
0
    }
1107
4
    for (const auto& [column_id, _] : lhs.local_positions) {
1108
4
        const auto* lhs_projection = find_scan_projection(lhs, column_id);
1109
4
        const auto* rhs_projection = find_scan_projection(rhs, column_id);
1110
4
        if (lhs_projection == nullptr || rhs_projection == nullptr ||
1111
4
            !same_scan_projection(*lhs_projection, *rhs_projection)) {
1112
0
            return false;
1113
0
        }
1114
4
    }
1115
3
    return true;
1116
3
}
1117
1118
} // namespace
1119
1120
8
Status TableReader::refresh_conjuncts(VExprContextSPtrs conjuncts) {
1121
8
    SCOPED_TIMER(_profile.total_timer);
1122
8
    SCOPED_TIMER(_profile.refresh_conjuncts_timer);
1123
8
    _conjuncts = std::move(conjuncts);
1124
8
    if (_data_reader.reader == nullptr) {
1125
        // The split is prepared but its physical reader has not opened yet. open_reader() will use
1126
        // this newest snapshot directly, so no pending request is needed.
1127
5
        return Status::OK();
1128
5
    }
1129
3
    if (!_data_reader.reader->supports_scan_request_refresh()) {
1130
0
        return Status::OK();
1131
0
    }
1132
1133
3
    RETURN_IF_ERROR(_build_table_filters_from_conjuncts());
1134
    // create_scan_request() rebuilds mapping projections in place. Build late predicates with an
1135
    // isolated mapper so the active row group cannot observe an unprepared or incompatible mapper
1136
    // before its physical request reaches the reader's safe activation boundary.
1137
3
    auto refreshed_mapper = _data_reader.reader->create_column_mapper(_mapper_options);
1138
3
    DORIS_CHECK(refreshed_mapper != nullptr);
1139
3
    RETURN_IF_ERROR(refreshed_mapper->create_mapping(_projected_columns, _partition_values,
1140
3
                                                     _data_reader.file_schema));
1141
3
    auto refreshed_request = std::make_shared<FileScanRequest>();
1142
3
    RETURN_IF_ERROR(refreshed_mapper->create_scan_request(
1143
3
            _table_filters, _projected_columns, refreshed_request.get(), _runtime_state,
1144
3
            _file_scan_request == nullptr ? nullptr : &_file_scan_request->local_positions));
1145
    // A refresh does not prove that every future runtime filter has arrived. Keep carrier values
1146
    // available whenever the split started with pending filters.
1147
3
    if (_push_down_agg_type == TPushAggOp::type::COUNT && _push_down_count_columns.has_value() &&
1148
3
        _push_down_count_columns->empty() && _all_runtime_filters_applied_for_split) {
1149
0
        for (const auto& column : refreshed_request->non_predicate_columns) {
1150
0
            refreshed_request->count_star_placeholder_columns.push_back(column.column_id());
1151
0
        }
1152
0
    }
1153
3
    RETURN_IF_ERROR(customize_file_scan_request(refreshed_request.get()));
1154
3
    if (_file_scan_request == nullptr ||
1155
3
        !same_physical_scan_layout(*refreshed_request, *_file_scan_request)) {
1156
        // A reader cannot reinterpret columns already materialized with another block layout.
1157
        // Keep scanner-level filtering as the correctness fallback for hidden slots or nested
1158
        // projections instead of switching an incompatible physical shape mid-file.
1159
0
        return Status::OK();
1160
0
    }
1161
3
    RETURN_IF_ERROR(_open_local_filter_exprs(*refreshed_request));
1162
1163
3
    if (_condition_cache_ctx != nullptr && !_condition_cache_ctx->is_hit) {
1164
        // Rows before and after a late RF were evaluated by different predicate snapshots. Such a
1165
        // partial MISS bitmap must never be published under either snapshot's cache key.
1166
0
        _condition_cache = nullptr;
1167
0
        _condition_cache_ctx = nullptr;
1168
0
        _data_reader.reader->set_condition_cache_context(nullptr);
1169
0
    }
1170
3
    {
1171
3
        SCOPED_TIMER(_profile.file_reader_total_timer);
1172
3
        SCOPED_TIMER(_profile.file_reader_refresh_timer);
1173
3
        RETURN_IF_ERROR(_data_reader.reader->queue_scan_request(refreshed_request));
1174
3
    }
1175
3
    _file_scan_request = std::move(refreshed_request);
1176
3
    return Status::OK();
1177
3
}
1178
1179
152
Status TableReader::_open_local_filter_exprs(const FileScanRequest& file_request) {
1180
152
    RowDescriptor row_desc;
1181
152
    for (const auto& conjunct : file_request.conjuncts) {
1182
28
        RETURN_IF_ERROR(conjunct->prepare(_runtime_state, row_desc));
1183
28
        RETURN_IF_ERROR(conjunct->open(_runtime_state));
1184
28
    }
1185
152
    for (const auto& delete_conjunct : file_request.delete_conjuncts) {
1186
56
        RETURN_IF_ERROR(delete_conjunct->prepare(_runtime_state, row_desc));
1187
56
        RETURN_IF_ERROR(delete_conjunct->open(_runtime_state));
1188
56
    }
1189
152
    return Status::OK();
1190
152
}
1191
1192
149
bool TableReader::_should_enable_condition_cache(const FileScanRequest& file_request) const {
1193
149
    if (_condition_cache_digest == 0 || _push_down_agg_type == TPushAggOp::type::COUNT ||
1194
149
        _current_file_description == std::nullopt || _data_reader.reader == nullptr) {
1195
142
        return false;
1196
142
    }
1197
    // Condition cache is populated by file readers after evaluating file-local row-level
1198
    // conjuncts. Metadata pruning can skip row groups/pages, but it does not produce a per-row
1199
    // survivor bitmap that can safely populate the cache.
1200
7
    if (file_request.conjuncts.empty()) {
1201
1
        return false;
1202
1
    }
1203
    // Delete files/deletion vectors are table-format state. They may change independently of the
1204
    // data file path/mtime/size used by the external cache key, so caching their result can become
1205
    // stale. Keep delete filtering enabled, but do not read or write condition cache.
1206
6
    if (_delete_rows != nullptr || _deletion_vector != nullptr ||
1207
6
        !file_request.delete_conjuncts.empty()) {
1208
1
        return false;
1209
1
    }
1210
    // Only scanner-driven splits provide a digest rebuilt from the exact RF snapshot. Keep the
1211
    // conservative behavior for standalone TableReader callers: their initial digest may describe
1212
    // only static predicate P and must not store P AND RF under that key.
1213
5
    return _condition_cache_digest_covers_current_split ||
1214
5
           !contains_runtime_filter(file_request.conjuncts);
1215
6
}
1216
1217
149
Status TableReader::_init_reader_condition_cache(const FileScanRequest& file_request) {
1218
149
    _condition_cache = nullptr;
1219
149
    _condition_cache_ctx = nullptr;
1220
149
    if (!_should_enable_condition_cache(file_request)) {
1221
145
        return Status::OK();
1222
145
    }
1223
1224
4
    auto* cache = segment_v2::ConditionCache::instance();
1225
4
    if (cache == nullptr) {
1226
0
        return Status::OK();
1227
0
    }
1228
4
    const auto& file = *_current_file_description;
1229
4
    _condition_cache_key = segment_v2::ConditionCache::ExternalCacheKey(
1230
4
            file.path, file.mtime, file.file_size, _condition_cache_digest, file.range_start_offset,
1231
4
            file.range_size,
1232
4
            segment_v2::ConditionCache::ExternalCacheKey::BASE_GRANULE_AWARE_VERSION);
1233
1234
4
    segment_v2::ConditionCacheHandle handle;
1235
4
    const bool condition_cache_hit = cache->lookup(_condition_cache_key, &handle);
1236
4
    if (condition_cache_hit) {
1237
0
        _condition_cache = handle.get_filter_result();
1238
0
        ++_condition_cache_hit_count;
1239
4
    } else {
1240
4
        const int64_t total_rows = _data_reader.reader->get_total_rows();
1241
4
        if (total_rows <= 0) {
1242
0
            return Status::OK();
1243
0
        }
1244
        // Add one guard granule for split ranges that start in the middle of a granule. A guard
1245
        // false bit beyond the real range never overlaps real rows, but avoids boundary overflow
1246
        // when a reader marks the last partial granule.
1247
4
        const size_t num_granules = (total_rows + ConditionCacheContext::GRANULE_SIZE - 1) /
1248
4
                                    ConditionCacheContext::GRANULE_SIZE;
1249
4
        _condition_cache = std::make_shared<std::vector<bool>>(num_granules + 1, false);
1250
4
    }
1251
1252
4
    if (_condition_cache != nullptr) {
1253
4
        _condition_cache_ctx = std::make_shared<ConditionCacheContext>();
1254
4
        _condition_cache_ctx->is_hit = condition_cache_hit;
1255
4
        _condition_cache_ctx->filter_result = _condition_cache;
1256
4
        _condition_cache_ctx->num_granules = _condition_cache->size();
1257
4
        if (condition_cache_hit) {
1258
0
            _condition_cache_ctx->base_granule = handle.get_base_granule();
1259
0
        }
1260
4
        _data_reader.reader->set_condition_cache_context(_condition_cache_ctx);
1261
4
    }
1262
4
    return Status::OK();
1263
4
}
1264
1265
151
void TableReader::_finalize_reader_condition_cache() {
1266
151
    if (_condition_cache_ctx == nullptr || _condition_cache_ctx->is_hit) {
1267
147
        _condition_cache = nullptr;
1268
147
        _condition_cache_ctx = nullptr;
1269
147
        return;
1270
147
    }
1271
    // LIMIT or scanner cancellation may close a reader before all selected row ranges are visited.
1272
    // Unvisited granules remain false in a MISS bitmap, so inserting a partial bitmap would make a
1273
    // later HIT skip valid rows. Only publish cache entries after the physical reader reaches EOF.
1274
4
    if (!_current_reader_reached_eof) {
1275
2
        _condition_cache = nullptr;
1276
2
        _condition_cache_ctx = nullptr;
1277
2
        return;
1278
2
    }
1279
2
    DORIS_CHECK(_condition_cache_ctx->num_granules <= _condition_cache->size());
1280
2
    _condition_cache->resize(_condition_cache_ctx->num_granules);
1281
2
    segment_v2::ConditionCache::instance()->insert(
1282
2
            _condition_cache_key, std::move(_condition_cache), _condition_cache_ctx->base_granule);
1283
2
    _condition_cache = nullptr;
1284
2
    _condition_cache_ctx = nullptr;
1285
2
}
1286
1287
212
Status TableReader::create_next_reader(bool* eos) {
1288
212
    SCOPED_TIMER(_profile.create_reader_timer);
1289
212
    DCHECK(_data_reader.reader == nullptr);
1290
212
    if (_current_task == nullptr) {
1291
60
        *eos = true;
1292
60
        return Status::OK();
1293
60
    }
1294
1295
152
    RETURN_IF_ERROR(create_file_reader(&_data_reader.reader));
1296
152
    DORIS_CHECK(_data_reader.reader != nullptr);
1297
152
    if (_batch_size > 0) {
1298
0
        _data_reader.reader->set_batch_size(_batch_size);
1299
0
    }
1300
152
    Status st;
1301
152
    {
1302
152
        SCOPED_TIMER(_profile.file_reader_total_timer);
1303
152
        SCOPED_TIMER(_profile.file_reader_init_timer);
1304
152
        st = _data_reader.reader->init(_runtime_state);
1305
152
    }
1306
152
    if (!st.ok()) {
1307
1
        if (_io_ctx != nullptr && _io_ctx->should_stop && st.is<ErrorCode::END_OF_FILE>()) {
1308
0
            *eos = true;
1309
0
            _data_reader.reader.reset();
1310
0
            return Status::OK();
1311
0
        }
1312
1
        return st;
1313
1
    }
1314
151
    st = open_reader();
1315
151
    if (!st.ok()) {
1316
1
        if (_io_ctx != nullptr && _io_ctx->should_stop && st.is<ErrorCode::END_OF_FILE>()) {
1317
0
            *eos = true;
1318
0
            _data_reader.reader.reset();
1319
0
            return Status::OK();
1320
0
        }
1321
1
        return st;
1322
1
    }
1323
150
    if (_data_reader.reader == nullptr) {
1324
1
        *eos = _current_task == nullptr;
1325
1
        return Status::OK();
1326
1
    }
1327
149
    *eos = false;
1328
149
    return Status::OK();
1329
150
}
1330
1331
122
Status TableReader::create_file_reader(std::unique_ptr<FileReader>* reader) {
1332
122
    DORIS_CHECK(reader != nullptr);
1333
122
    const bool enable_mapping_timestamp_tz = _scan_params != nullptr &&
1334
122
                                             _scan_params->__isset.enable_mapping_timestamp_tz &&
1335
122
                                             _scan_params->enable_mapping_timestamp_tz;
1336
122
    const bool enable_mapping_varbinary = _scan_params != nullptr &&
1337
122
                                          _scan_params->__isset.enable_mapping_varbinary &&
1338
122
                                          _scan_params->enable_mapping_varbinary;
1339
122
    const std::string hive_parquet_time_zone =
1340
122
            _scan_params != nullptr && _scan_params->__isset.hive_parquet_time_zone
1341
122
                    ? _scan_params->hive_parquet_time_zone
1342
122
                    : "";
1343
122
    if (_format == FileFormat::PARQUET) {
1344
        // V2 must honor the scan contract directly; otherwise Hive STRING columns backed by an
1345
        // unannotated BYTE_ARRAY are silently exposed as VARBINARY and predicate bytes no longer
1346
        // match the table type.
1347
111
        *reader = std::make_unique<format::parquet::ParquetReader>(
1348
111
                _system_properties, _current_task->data_file, _io_ctx, _scanner_profile,
1349
111
                _global_rowid_context, enable_mapping_timestamp_tz, enable_mapping_varbinary,
1350
111
                hive_parquet_time_zone);
1351
111
        return Status::OK();
1352
111
    }
1353
11
    if (_format == FileFormat::ORC) {
1354
11
        *reader = std::make_unique<format::orc::OrcReader>(
1355
11
                _system_properties, _current_task->data_file, _io_ctx, _scanner_profile,
1356
11
                _global_rowid_context, enable_mapping_timestamp_tz);
1357
11
        return Status::OK();
1358
11
    }
1359
0
    if (_format == FileFormat::CSV) {
1360
0
        if (_file_slot_descs == nullptr) {
1361
0
            return Status::InvalidArgument("CSV reader requires file slot descriptors");
1362
0
        }
1363
        // CSV has no embedded schema. TableReader owns table-level mapping, while CsvReader needs
1364
        // only the physical file slots plus scan text parameters to build a file-local schema.
1365
        // Non-file columns such as partitions/defaults/virtual row ids are intentionally excluded
1366
        // from `_file_slot_descs` and are materialized during finalize_chunk().
1367
0
        *reader = std::make_unique<format::csv::CsvReader>(
1368
0
                _system_properties, _current_task->data_file, _io_ctx, _scanner_profile,
1369
0
                _scan_params, *_file_slot_descs, _current_range_compress_type,
1370
0
                _current_range_load_id);
1371
0
        return Status::OK();
1372
0
    }
1373
0
    if (_format == FileFormat::TEXT) {
1374
0
        if (_file_slot_descs == nullptr) {
1375
0
            return Status::InvalidArgument("Text reader requires file slot descriptors");
1376
0
        }
1377
        // Text files have no embedded schema. As with CSV, TableReader handles table-level mapping
1378
        // and only passes physical file slots to the v2 TextReader.
1379
0
        *reader = std::make_unique<format::text::TextReader>(
1380
0
                _system_properties, _current_task->data_file, _io_ctx, _scanner_profile,
1381
0
                _scan_params, *_file_slot_descs, _current_range_compress_type,
1382
0
                _current_range_load_id);
1383
0
        return Status::OK();
1384
0
    }
1385
0
    if (_format == FileFormat::JSON) {
1386
0
        if (_file_slot_descs == nullptr) {
1387
0
            return Status::InvalidArgument("JSON reader requires file slot descriptors");
1388
0
        }
1389
0
        *reader = std::make_unique<format::json::JsonReader>(
1390
0
                _system_properties, _current_task->data_file, _io_ctx, _scanner_profile,
1391
0
                _scan_params, _current_file_range_desc, *_file_slot_descs,
1392
0
                _current_range_compress_type, _current_range_load_id);
1393
0
        return Status::OK();
1394
0
    }
1395
0
    if (_format == FileFormat::NATIVE) {
1396
0
        *reader = std::make_unique<format::native::NativeReader>(
1397
0
                _system_properties, _current_task->data_file, _io_ctx, _scanner_profile);
1398
0
        return Status::OK();
1399
0
    }
1400
0
    return Status::NotSupported("TableReader does not support file format {}",
1401
0
                                file_format_to_string(_format));
1402
0
}
1403
1404
185
std::unique_ptr<io::FileDescription> create_file_description(const TFileRangeDesc& range) {
1405
185
    auto file_description = std::make_unique<io::FileDescription>();
1406
185
    file_description->path = range.path;
1407
185
    file_description->file_size = range.__isset.file_size ? range.file_size : -1;
1408
185
    file_description->mtime = range.__isset.modification_time ? range.modification_time : 0;
1409
185
    file_description->range_start_offset = range.__isset.start_offset ? range.start_offset : 0;
1410
185
    file_description->range_size = range.__isset.size ? range.size : -1;
1411
185
    if (range.__isset.fs_name) {
1412
5
        file_description->fs_name = range.fs_name;
1413
5
    }
1414
185
    if (range.__isset.file_cache_admission) {
1415
0
        file_description->file_cache_admission = range.file_cache_admission;
1416
0
    }
1417
185
    return file_description;
1418
185
}
1419
1420
186
Status TableReader::prepare_split(const SplitReadOptions& options) {
1421
186
    SCOPED_TIMER(_profile.total_timer);
1422
186
    SCOPED_TIMER(_profile.prepare_split_timer);
1423
186
    _current_split_pruned = false;
1424
186
    _all_runtime_filters_applied_for_split = options.all_runtime_filters_applied;
1425
186
    _condition_cache_digest_covers_current_split = options.condition_cache_digest.has_value();
1426
186
    if (options.condition_cache_digest.has_value()) {
1427
        // The split snapshot may include RFs that arrived after TableReader::init(). Use the digest
1428
        // computed from that exact snapshot. Example: an initial P digest must not be used to store
1429
        // the bitmap for P AND late RF{7, 9}; the scanner supplies digest(P AND RF{7, 9}) here.
1430
1
        _condition_cache_digest = *options.condition_cache_digest;
1431
185
    } else {
1432
        // An explicit scanner digest is split-scoped. Restore the init-time digest when a later
1433
        // standalone split omits it instead of leaking the previous split's RF payload into its key.
1434
185
        _condition_cache_digest = _initial_condition_cache_digest;
1435
185
    }
1436
186
    if (options.conjuncts.has_value()) {
1437
2
        _conjuncts = *options.conjuncts;
1438
2
    }
1439
    // Update to current split format to handle ORC/PARQUET files in one table.
1440
186
    _format = options.current_split_format;
1441
186
    _partition_values = std::move(options.partition_values);
1442
186
    _current_task.reset();
1443
186
    _current_file_description.reset();
1444
186
    _current_file_range_desc = options.current_range;
1445
186
    _current_range_compress_type = options.current_range.__isset.compress_type
1446
186
                                           ? options.current_range.compress_type
1447
186
                                           : TFileCompressType::UNKNOWN;
1448
186
    _current_range_load_id = options.current_range.__isset.load_id
1449
186
                                     ? std::make_optional(options.current_range.load_id)
1450
186
                                     : std::nullopt;
1451
186
    _global_rowid_context = options.global_rowid_context;
1452
186
    _delete_rows = nullptr;
1453
186
    _deletion_vector = nullptr;
1454
186
    _aggregate_pushdown_tried = false;
1455
186
    _remaining_table_level_count = -1;
1456
186
    _remaining_file_level_count = -1;
1457
186
    _current_split_uses_metadata_count = false;
1458
186
    _current_reader_reached_eof = false;
1459
186
    RETURN_IF_ERROR(_evaluate_partition_prune_conjuncts(options.partition_prune_conjuncts,
1460
186
                                                        &_current_split_pruned));
1461
186
    if (_current_split_pruned) {
1462
1
        COUNTER_UPDATE(_profile.runtime_filter_partition_pruned_range_counter, 1);
1463
1
        return Status::OK();
1464
1
    }
1465
185
    _current_task = std::make_unique<ScanTask>();
1466
185
    _current_task->data_file = create_file_description(options.current_range);
1467
185
    _current_file_description = *_current_task->data_file;
1468
    // A table-level row count is only equivalent to scanning the split when no row predicate is
1469
    // active and no predicate can arrive later. The metadata path can return several batches for
1470
    // one split; after its first synthetic batch there is no way to recover the real rows if a
1471
    // runtime filter arrives before the next scheduler turn.
1472
    // Table-level metadata only contains the number of rows; it cannot evaluate an expression or
1473
    // the NULL state of a COUNT argument. Require the new FE's explicit empty argument list, which
1474
    // means COUNT(*)/COUNT(1). A non-empty list means COUNT(col), while nullopt comes from an old FE
1475
    // whose COUNT semantics are unknown during a BE-first rolling upgrade.
1476
185
    if (_push_down_agg_type == TPushAggOp::type::COUNT && _push_down_count_columns.has_value() &&
1477
185
        _push_down_count_columns->empty() && options.all_runtime_filters_applied &&
1478
185
        _conjuncts.empty() && options.current_range.__isset.table_format_params &&
1479
185
        options.current_range.table_format_params.__isset.table_level_row_count) {
1480
3
        DORIS_CHECK(options.current_range.table_format_params.table_level_row_count >= -1);
1481
3
        _remaining_table_level_count =
1482
3
                options.current_range.table_format_params.table_level_row_count;
1483
3
        _current_split_uses_metadata_count = _is_table_level_count_active();
1484
3
    }
1485
185
    if (_is_table_level_count_active()) {
1486
2
        return Status::OK();
1487
2
    }
1488
183
    return _parse_delete_predicates(options);
1489
185
}
1490
1491
Status TableReader::_evaluate_partition_prune_conjuncts(const VExprContextSPtrs& conjuncts,
1492
186
                                                        bool* can_filter_all) {
1493
186
    DORIS_CHECK(can_filter_all != nullptr);
1494
186
    SCOPED_TIMER(_profile.runtime_filter_partition_prune_timer);
1495
186
    *can_filter_all = false;
1496
186
    if (conjuncts.empty() || _partition_values.empty()) {
1497
183
        return Status::OK();
1498
183
    }
1499
1500
3
    VExprContextSPtrs partition_conjuncts;
1501
3
    for (const auto& conjunct : conjuncts) {
1502
3
        DORIS_CHECK(conjunct != nullptr);
1503
3
        DORIS_CHECK(conjunct->root() != nullptr);
1504
        // Keep only the safe prefix of the original conjunct order. If an unsafe conjunct is
1505
        // skipped, a later predicate could prune the split before the unsafe one reaches its
1506
        // normal row-level evaluation point.
1507
3
        if (!_is_safe_to_pre_execute(conjunct)) {
1508
1
            break;
1509
1
        }
1510
2
        std::set<GlobalIndex> global_indices;
1511
2
        collect_global_indices(conjunct->root(), &global_indices);
1512
2
        if (global_indices.empty()) {
1513
0
            continue;
1514
0
        }
1515
2
        const bool partition_only = std::ranges::all_of(global_indices, [&](GlobalIndex index) {
1516
2
            if (index.value() >= _projected_columns.size()) {
1517
0
                return false;
1518
0
            }
1519
2
            const auto& column = _projected_columns[index.value()];
1520
2
            return column.is_partition_key &&
1521
2
                   find_partition_value(column, _partition_values) != nullptr;
1522
2
        });
1523
2
        if (partition_only) {
1524
2
            partition_conjuncts.push_back(conjunct);
1525
2
        }
1526
2
    }
1527
3
    if (partition_conjuncts.empty()) {
1528
1
        return Status::OK();
1529
1
    }
1530
1531
2
    Block block;
1532
2
    RETURN_IF_ERROR(_build_partition_prune_block(&block));
1533
2
    RowDescriptor row_desc;
1534
2
    for (const auto& conjunct : partition_conjuncts) {
1535
2
        RETURN_IF_ERROR(conjunct->prepare(_runtime_state, row_desc));
1536
2
        RETURN_IF_ERROR(conjunct->open(_runtime_state));
1537
2
    }
1538
2
    IColumn::Filter result_filter(block.rows(), 1);
1539
2
    return VExprContext::execute_conjuncts(partition_conjuncts, nullptr, &block, &result_filter,
1540
2
                                           can_filter_all);
1541
2
}
1542
1543
68
bool TableReader::_is_safe_to_pre_execute(const VExprContextSPtr& conjunct) {
1544
68
    DORIS_CHECK(conjunct != nullptr);
1545
68
    DORIS_CHECK(conjunct->root() != nullptr);
1546
68
    const auto root = conjunct->root();
1547
68
    const auto impl = root->get_impl();
1548
68
    const auto predicate = impl != nullptr ? impl : root;
1549
    // Split pruning evaluates a predicate once before any file rows are read. Reordering
1550
    // non-deterministic or error-preserving expressions can change their row-level semantics,
1551
    // even when every referenced slot is a partition column or maps to a constant entry.
1552
68
    return predicate->is_safe_to_execute_on_selected_rows();
1553
68
}
1554
1555
2
Status TableReader::_build_partition_prune_block(Block* block) const {
1556
2
    DORIS_CHECK(block != nullptr);
1557
2
    DORIS_CHECK(!_projected_columns.empty());
1558
2
    block->clear();
1559
2
    for (const auto& column : _projected_columns) {
1560
2
        DORIS_CHECK(column.type != nullptr);
1561
2
        ColumnPtr value_column = column.type->create_column_const_with_default_value(1);
1562
2
        if (column.is_partition_key) {
1563
2
            const auto* partition_value = find_partition_value(column, _partition_values);
1564
2
            if (partition_value != nullptr) {
1565
2
                value_column = column.type->create_column_const(1, *partition_value);
1566
2
            }
1567
2
        }
1568
2
        block->insert({std::move(value_column), column.type, column.name});
1569
2
    }
1570
2
    return Status::OK();
1571
2
}
1572
1573
183
Status TableReader::_parse_delete_predicates(const SplitReadOptions& options) {
1574
183
    DeleteFileDesc desc {.fs_name = options.current_range.fs_name};
1575
183
    bool has_delete_file = false;
1576
183
    RETURN_IF_ERROR(_parse_deletion_vector_file(options.current_range.table_format_params, &desc,
1577
183
                                                &has_delete_file));
1578
183
    if (has_delete_file) {
1579
7
        DORIS_CHECK(options.cache != nullptr);
1580
7
        Status create_status = Status::OK();
1581
1582
7
        bool decoded_cache_hit = false;
1583
7
        _deletion_vector = options.cache->get<DeletionVector>(
1584
7
                desc.key,
1585
7
                [&]() -> DeletionVector* {
1586
7
                    auto deletion_vector = std::make_unique<DeletionVector>();
1587
1588
7
                    DeletionVectorReader dv_reader(_runtime_state, _scanner_profile, *_scan_params,
1589
7
                                                   desc, _io_ctx.get());
1590
7
                    create_status = dv_reader.open();
1591
7
                    if (!create_status.ok()) [[unlikely]] {
1592
0
                        return nullptr;
1593
0
                    }
1594
1595
7
                    size_t bytes_read = desc.size;
1596
7
                    std::vector<char> buffer(bytes_read);
1597
7
                    DBUG_EXECUTE_IF("TableReader.parse_deletion_vector.io_error", {
1598
7
                        create_status =
1599
7
                                Status::IOError("injected format v2 deletion vector read failure");
1600
7
                        return nullptr;
1601
7
                    });
1602
6
                    DBUG_EXECUTE_IF("TableReader.parse_deletion_vector.should_stop", {
1603
6
                        create_status = Status::EndOfFile("stop read.");
1604
6
                        return nullptr;
1605
6
                    });
1606
5
                    create_status =
1607
5
                            dv_reader.read_at(desc.start_offset, {buffer.data(), bytes_read});
1608
5
                    const auto& file_cache_stats = dv_reader.file_cache_statistics();
1609
5
                    COUNTER_UPDATE(_profile.dv_file_cache_hit_count,
1610
5
                                   file_cache_stats.num_local_io_total);
1611
5
                    COUNTER_UPDATE(_profile.dv_file_cache_miss_count,
1612
5
                                   file_cache_stats.num_remote_io_total);
1613
5
                    COUNTER_UPDATE(_profile.dv_file_cache_peer_read_count,
1614
5
                                   file_cache_stats.num_peer_io_total);
1615
5
                    if (!create_status.ok()) [[unlikely]] {
1616
0
                        return nullptr;
1617
0
                    }
1618
1619
5
                    const char* buf = buffer.data();
1620
5
                    SCOPED_TIMER(_profile.parse_delete_file_time);
1621
5
                    create_status = parse_deletion_vector(buf, bytes_read, desc.format,
1622
5
                                                          deletion_vector.get());
1623
5
                    if (!create_status.ok()) [[unlikely]] {
1624
1
                        return nullptr;
1625
1
                    }
1626
4
                    COUNTER_UPDATE(_profile.num_delete_rows, deletion_vector->cardinality());
1627
4
                    return deletion_vector.release();
1628
5
                },
1629
7
                &decoded_cache_hit);
1630
7
        RETURN_IF_ERROR(create_status);
1631
4
        COUNTER_UPDATE(decoded_cache_hit ? _profile.decoded_dv_cache_hit_count
1632
4
                                         : _profile.decoded_dv_cache_miss_count,
1633
4
                       1);
1634
4
    }
1635
1636
180
    return Status::OK();
1637
183
}
1638
} // namespace doris::format