Coverage Report

Created: 2026-07-29 00:19

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
109
const schema::external::TField* get_field_ptr(const schema::external::TFieldPtr& field_ptr) {
144
109
    if (!field_ptr.__isset.field_ptr || field_ptr.field_ptr == nullptr) {
145
0
        return nullptr;
146
0
    }
147
109
    return field_ptr.field_ptr.get();
148
109
}
149
150
22
ColumnDefinition build_schema_identity_from_external_field(const schema::external::TField& field) {
151
22
    ColumnDefinition identity;
152
22
    if (field.__isset.id) {
153
22
        identity.identifier = Field::create_field<TYPE_INT>(field.id);
154
22
    }
155
22
    identity.name = field.__isset.name ? field.name : "";
156
22
    identity.name_mapping =
157
22
            field.__isset.name_mapping ? field.name_mapping : std::vector<std::string> {};
158
22
    identity.has_name_mapping =
159
22
            field.__isset.name_mapping_is_authoritative && field.name_mapping_is_authoritative;
160
22
    if (!field.__isset.nestedField) {
161
20
        return identity;
162
20
    }
163
2
    if (field.nestedField.__isset.struct_field && field.nestedField.struct_field.__isset.fields) {
164
4
        for (const auto& child_ptr : field.nestedField.struct_field.fields) {
165
4
            if (const auto* child = get_field_ptr(child_ptr); child != nullptr) {
166
4
                identity.children.push_back(build_schema_identity_from_external_field(*child));
167
4
            }
168
4
        }
169
2
    } else if (field.nestedField.__isset.array_field &&
170
0
               field.nestedField.array_field.__isset.item_field) {
171
0
        if (const auto* child = get_field_ptr(field.nestedField.array_field.item_field);
172
0
            child != nullptr) {
173
0
            identity.children.push_back(build_schema_identity_from_external_field(*child));
174
0
            identity.children.back().name = "element";
175
0
        }
176
0
    } else if (field.nestedField.__isset.map_field) {
177
0
        if (field.nestedField.map_field.__isset.key_field) {
178
0
            if (const auto* child = get_field_ptr(field.nestedField.map_field.key_field);
179
0
                child != nullptr) {
180
0
                identity.children.push_back(build_schema_identity_from_external_field(*child));
181
0
                identity.children.back().name = "key";
182
0
            }
183
0
        }
184
0
        if (field.nestedField.map_field.__isset.value_field) {
185
0
            if (const auto* child = get_field_ptr(field.nestedField.map_field.value_field);
186
0
                child != nullptr) {
187
0
                identity.children.push_back(build_schema_identity_from_external_field(*child));
188
0
                identity.children.back().name = "value";
189
0
            }
190
0
        }
191
0
    }
192
2
    return identity;
193
22
}
194
195
const ColumnDefinition* find_identity_child(const ColumnDefinition& projected_child,
196
2
                                            const ColumnDefinition& identity_parent) {
197
2
    const auto child_it = std::ranges::find_if(
198
3
            identity_parent.children, [&](const ColumnDefinition& identity_child) {
199
3
                if (projected_child.has_identifier_field_id() &&
200
3
                    identity_child.has_identifier_field_id()) {
201
3
                    return projected_child.get_identifier_field_id() ==
202
3
                           identity_child.get_identifier_field_id();
203
3
                }
204
0
                if (to_lower(projected_child.name) == to_lower(identity_child.name)) {
205
0
                    return true;
206
0
                }
207
0
                return std::ranges::any_of(
208
0
                        identity_child.name_mapping, [&](const std::string& alias) {
209
0
                            return to_lower(projected_child.name) == to_lower(alias);
210
0
                        });
211
0
            });
212
2
    return child_it == identity_parent.children.end() ? nullptr : &*child_it;
213
2
}
214
215
20
void attach_full_schema_identity(ColumnDefinition* projected, const ColumnDefinition& identity) {
216
20
    DORIS_CHECK(projected != nullptr);
217
    // Access-path children control materialization, but wrapper discovery needs sibling IDs that
218
    // were pruned from that projection. Keep the complete identity tree on a separate channel.
219
20
    projected->identity_children = identity.children;
220
20
    for (auto& projected_child : projected->children) {
221
2
        if (const auto* identity_child = find_identity_child(projected_child, identity);
222
2
            identity_child != nullptr) {
223
2
            attach_full_schema_identity(&projected_child, *identity_child);
224
2
        }
225
2
    }
226
20
}
227
228
13
void clear_initial_default_metadata(ColumnDefinition* column) {
229
13
    DORIS_CHECK(column != nullptr);
230
13
    column->initial_default_value.reset();
231
13
    column->initial_default_value_is_base64 = false;
232
13
    for (auto& child : column->children) {
233
6
        clear_initial_default_metadata(&child);
234
6
    }
235
13
}
236
237
15
bool external_field_matches_name(const schema::external::TField& field, const std::string& name) {
238
15
    if (field.__isset.name && to_lower(field.name) == to_lower(name)) {
239
5
        return true;
240
5
    }
241
10
    return field.__isset.name_mapping &&
242
10
           std::ranges::any_of(field.name_mapping, [&](const std::string& alias) {
243
8
               return to_lower(alias) == to_lower(name);
244
8
           });
245
15
}
246
247
DataTypePtr find_struct_child_type_by_external_field(const DataTypeStruct& struct_type,
248
                                                     const schema::external::TField& field,
249
18
                                                     bool prefer_current_name) {
250
18
    if (prefer_current_name && field.__isset.name) {
251
22
        for (size_t field_idx = 0; field_idx < struct_type.get_elements().size(); ++field_idx) {
252
18
            if (to_lower(field.name) == to_lower(struct_type.get_element_name(field_idx))) {
253
11
                return struct_type.get_element(field_idx);
254
11
            }
255
18
        }
256
15
    }
257
10
    for (size_t field_idx = 0; field_idx < struct_type.get_elements().size(); ++field_idx) {
258
8
        const auto& element_name = struct_type.get_element_name(field_idx);
259
8
        if (external_field_matches_name(field, element_name)) {
260
5
            return struct_type.get_element(field_idx);
261
5
        }
262
8
    }
263
2
    return nullptr;
264
7
}
265
266
DataTypePtr restore_current_primitive_type(const schema::external::TField& field,
267
62
                                           DataTypePtr fallback_type) {
268
62
    if (!field.__isset.type) {
269
39
        return fallback_type;
270
39
    }
271
23
    DORIS_CHECK(fallback_type != nullptr);
272
23
    const auto primitive_type = thrift_to_type(field.type.type);
273
23
    if (is_complex_type(primitive_type)) {
274
2
        return fallback_type;
275
2
    }
276
    // The delete file can expose an older physical type, but initial defaults belong to the
277
    // current table field. Restore that type from FE before parsing the default and let the table
278
    // reader apply the normal promotion cast to the delete-key type.
279
21
    return DataTypeFactory::instance().create_data_type(
280
21
            primitive_type, fallback_type->is_nullable(),
281
21
            field.type.__isset.precision ? field.type.precision : 0,
282
21
            field.type.__isset.scale ? field.type.scale : 0,
283
21
            field.type.__isset.len ? field.type.len : -1);
284
23
}
285
286
// NOLINTNEXTLINE(readability-function-size): keep recursive Iceberg type reconstruction together.
287
ColumnDefinition build_schema_column_from_external_field(const schema::external::TField& field,
288
                                                         DataTypePtr type,
289
62
                                                         bool prefer_current_name) {
290
62
    type = restore_current_primitive_type(field, std::move(type));
291
62
    ColumnDefinition column {
292
62
            .identifier = field.__isset.id ? Field::create_field<TYPE_INT>(field.id) : Field {},
293
62
            .name = field.__isset.name ? field.name : "",
294
62
            .name_mapping =
295
62
                    field.__isset.name_mapping ? field.name_mapping : std::vector<std::string> {},
296
62
            .has_name_mapping = field.__isset.name_mapping_is_authoritative &&
297
62
                                field.name_mapping_is_authoritative,
298
62
            .type = std::move(type),
299
62
            .children = {},
300
62
            .default_expr = nullptr,
301
62
            .initial_default_value = field.__isset.initial_default_value
302
62
                                             ? std::make_optional(field.initial_default_value)
303
62
                                             : std::nullopt,
304
62
            .initial_default_value_is_base64 = field.__isset.initial_default_value_is_base64 &&
305
62
                                               field.initial_default_value_is_base64,
306
62
            .is_optional = field.__isset.is_optional ? std::make_optional(field.is_optional)
307
62
                                                     : std::nullopt,
308
62
            .is_partition_key = false,
309
62
    };
310
62
    if (column.type == nullptr || !field.__isset.nestedField) {
311
46
        return column;
312
46
    }
313
314
16
    const auto nested_type = remove_nullable(column.type);
315
16
    switch (nested_type->get_primitive_type()) {
316
10
    case TYPE_STRUCT: {
317
10
        if (!field.nestedField.__isset.struct_field ||
318
10
            !field.nestedField.struct_field.__isset.fields) {
319
0
            return column;
320
0
        }
321
10
        const auto& struct_type = assert_cast<const DataTypeStruct&>(*nested_type);
322
18
        for (const auto& child_ptr : field.nestedField.struct_field.fields) {
323
18
            const auto* child_field = get_field_ptr(child_ptr);
324
18
            if (child_field == nullptr || !child_field->__isset.name) {
325
0
                continue;
326
0
            }
327
18
            auto child_type = find_struct_child_type_by_external_field(struct_type, *child_field,
328
18
                                                                       prefer_current_name);
329
18
            if (child_type == nullptr) {
330
2
                continue;
331
2
            }
332
16
            column.children.push_back(build_schema_column_from_external_field(
333
16
                    *child_field, child_type, prefer_current_name));
334
16
        }
335
10
        break;
336
10
    }
337
3
    case TYPE_ARRAY: {
338
3
        if (!field.nestedField.__isset.array_field ||
339
3
            !field.nestedField.array_field.__isset.item_field) {
340
0
            return column;
341
0
        }
342
3
        const auto* item_field = get_field_ptr(field.nestedField.array_field.item_field);
343
3
        if (item_field == nullptr) {
344
0
            return column;
345
0
        }
346
3
        const auto& array_type = assert_cast<const DataTypeArray&>(*nested_type);
347
3
        auto child = build_schema_column_from_external_field(
348
3
                *item_field, array_type.get_nested_type(), prefer_current_name);
349
3
        child.name = "element";
350
3
        if (child.has_identifier_name()) {
351
0
            child.identifier = Field::create_field<TYPE_STRING>(child.name);
352
0
        }
353
3
        column.children.push_back(std::move(child));
354
3
        break;
355
3
    }
356
3
    case TYPE_MAP: {
357
3
        if (!field.nestedField.__isset.map_field ||
358
3
            !field.nestedField.map_field.__isset.key_field ||
359
3
            !field.nestedField.map_field.__isset.value_field) {
360
0
            return column;
361
0
        }
362
3
        const auto& map_type = assert_cast<const DataTypeMap&>(*nested_type);
363
3
        const auto* key_field = get_field_ptr(field.nestedField.map_field.key_field);
364
3
        if (key_field != nullptr) {
365
3
            auto child = build_schema_column_from_external_field(
366
3
                    *key_field, map_type.get_key_type(), prefer_current_name);
367
3
            child.name = "key";
368
3
            if (child.has_identifier_name()) {
369
0
                child.identifier = Field::create_field<TYPE_STRING>(child.name);
370
0
            }
371
3
            column.children.push_back(std::move(child));
372
3
        }
373
3
        const auto* value_field = get_field_ptr(field.nestedField.map_field.value_field);
374
3
        if (value_field != nullptr) {
375
3
            auto child = build_schema_column_from_external_field(
376
3
                    *value_field, map_type.get_value_type(), prefer_current_name);
377
3
            child.name = "value";
378
3
            if (child.has_identifier_name()) {
379
0
                child.identifier = Field::create_field<TYPE_STRING>(child.name);
380
0
            }
381
3
            column.children.push_back(std::move(child));
382
3
        }
383
3
        break;
384
3
    }
385
0
    default:
386
0
        break;
387
16
    }
388
16
    return column;
389
16
}
390
391
const schema::external::TField* find_external_root_field(const TFileScanRangeParams* params,
392
89
                                                         const ColumnDefinition& column) {
393
89
    if (params == nullptr || !params->__isset.history_schema_info ||
394
89
        params->history_schema_info.empty()) {
395
46
        return nullptr;
396
46
    }
397
43
    const auto* schema = &params->history_schema_info.front();
398
43
    if (params->__isset.current_schema_id) {
399
44
        for (const auto& candidate_schema : params->history_schema_info) {
400
44
            if (candidate_schema.__isset.schema_id &&
401
44
                candidate_schema.schema_id == params->current_schema_id) {
402
43
                schema = &candidate_schema;
403
43
                break;
404
43
            }
405
44
        }
406
43
    }
407
43
    if (!schema->__isset.root_field || !schema->root_field.__isset.fields) {
408
0
        return nullptr;
409
0
    }
410
43
    if (!supports_iceberg_scan_semantics_v1(params)) {
411
        // Old BEs used one ordered current-name/alias pass. Preserve that result for old-FE plans
412
        // until the explicit scan-semantics marker makes exact-name precedence cluster-wide.
413
7
        for (const auto& field_ptr : schema->root_field.fields) {
414
7
            const auto* field = get_field_ptr(field_ptr);
415
7
            if (field != nullptr && external_field_matches_name(*field, column.name)) {
416
7
                return field;
417
7
            }
418
7
        }
419
0
        return nullptr;
420
7
    }
421
    // A reused name identifies the newly added field, not an older sibling that retained that
422
    // spelling as an alias. Exhaust exact current names before consulting historical aliases.
423
44
    for (const auto& field_ptr : schema->root_field.fields) {
424
44
        const auto* field = get_field_ptr(field_ptr);
425
44
        if (field != nullptr && field->__isset.name &&
426
44
            to_lower(field->name) == to_lower(column.name)) {
427
35
            return field;
428
35
        }
429
44
    }
430
1
    for (const auto& field_ptr : schema->root_field.fields) {
431
1
        const auto* field = get_field_ptr(field_ptr);
432
1
        if (field != nullptr && field->__isset.name_mapping &&
433
1
            std::ranges::any_of(field->name_mapping, [&](const std::string& alias) {
434
0
                return to_lower(alias) == to_lower(column.name);
435
0
            })) {
436
0
            return field;
437
0
        }
438
1
    }
439
1
    return nullptr;
440
1
}
441
442
2
std::string expr_context_debug_string(const VExprContextSPtr& context) {
443
2
    if (context == nullptr) {
444
0
        return "null";
445
0
    }
446
2
    const auto root = context->root();
447
2
    if (root == nullptr) {
448
0
        return "VExprContext{root=null}";
449
0
    }
450
2
    std::ostringstream out;
451
2
    out << "VExprContext{root_name=" << root->expr_name() << ", root_debug=" << root->debug_string()
452
2
        << "}";
453
2
    return out.str();
454
2
}
455
456
1
std::string table_filter_debug_string(const TableFilter& filter) {
457
1
    std::ostringstream out;
458
1
    out << "TableFilter{conjunct=" << expr_context_debug_string(filter.conjunct)
459
1
        << ", global_indices="
460
1
        << join_table_reader_debug_strings(
461
1
                   filter.global_indices,
462
1
                   [](GlobalIndex global_index) { return std::to_string(global_index.value()); })
463
1
        << "}";
464
1
    return out.str();
465
1
}
466
467
4
bool contains_runtime_filter(const VExprContextSPtrs& conjuncts) {
468
4
    return std::ranges::any_of(conjuncts, [](const auto& conjunct) {
469
4
        return conjunct != nullptr && conjunct->root() != nullptr &&
470
4
               conjunct->root()->is_rf_wrapper();
471
4
    });
472
4
}
473
474
114
void collect_global_indices(const VExprSPtr& expr, std::set<GlobalIndex>* global_indices) {
475
114
    if (expr == nullptr) {
476
0
        return;
477
0
    }
478
114
    if (expr->is_rf_wrapper()) {
479
        // RuntimeFilterExpr wraps a real predicate expression but its own thrift node can still
480
        // look like SLOT_REF. Collect indices from the wrapped predicate; do not cast the wrapper
481
        // itself to VSlotRef.
482
7
        collect_global_indices(expr->get_impl(), global_indices);
483
7
        return;
484
7
    }
485
107
    if (expr->is_slot_ref()) {
486
36
        const auto* slot_ref = assert_cast<const VSlotRef*>(expr.get());
487
36
        DORIS_CHECK(slot_ref->column_id() >= 0);
488
36
        global_indices->insert(GlobalIndex(cast_set<size_t>(slot_ref->column_id())));
489
36
    }
490
107
    for (const auto& child : expr->children()) {
491
70
        collect_global_indices(child, global_indices);
492
70
    }
493
107
}
494
495
Status build_table_filters_from_conjunct(const VExprContextSPtr& conjunct, RuntimeState* state,
496
35
                                         std::vector<TableFilter>* table_filters) {
497
35
    if (conjunct == nullptr) {
498
0
        return Status::OK();
499
0
    }
500
35
    std::set<GlobalIndex> global_indices;
501
35
    collect_global_indices(conjunct->root(), &global_indices);
502
35
    if (!global_indices.empty()) {
503
33
        TableFilter table_filter;
504
33
        VExprSPtr filter_root;
505
33
        RETURN_IF_ERROR(clone_table_expr_tree(conjunct->root(), &filter_root));
506
33
        table_filter.conjunct = VExprContext::create_shared(std::move(filter_root));
507
34
        for (const auto global_index : global_indices) {
508
34
            table_filter.global_indices.push_back(global_index);
509
34
        }
510
33
        table_filters->push_back(std::move(table_filter));
511
33
    }
512
35
    return Status::OK();
513
35
}
514
515
Status parse_deletion_vector(const char* buf, size_t buffer_size, DeleteFileDesc::Format format,
516
5
                             DeletionVector* deletion_vector) {
517
5
    DORIS_CHECK(buf != nullptr);
518
5
    DORIS_CHECK(deletion_vector != nullptr);
519
5
    DORIS_CHECK(format == DeleteFileDesc::Format::PAIMON ||
520
5
                format == DeleteFileDesc::Format::ICEBERG);
521
522
5
    if (format == DeleteFileDesc::Format::PAIMON) {
523
1
        RETURN_IF_ERROR(decode_paimon_deletion_vector_buffer(buf, buffer_size, deletion_vector));
524
1
        return Status::OK();
525
1
    }
526
527
4
    return decode_iceberg_deletion_vector_buffer(buf, buffer_size, deletion_vector);
528
5
}
529
530
} // namespace
531
532
std::shared_ptr<io::FileSystemProperties> create_system_properties(
533
185
        const TFileScanRangeParams* scan_params) {
534
185
    auto system_properties = std::make_shared<io::FileSystemProperties>();
535
185
    if (scan_params == nullptr || !scan_params->__isset.file_type) {
536
121
        system_properties->system_type = TFileType::FILE_LOCAL;
537
121
        return system_properties;
538
121
    }
539
64
    system_properties->system_type = scan_params->file_type;
540
64
    system_properties->properties = scan_params->properties;
541
64
    system_properties->hdfs_params = scan_params->hdfs_params;
542
64
    if (scan_params->__isset.broker_addresses) {
543
0
        system_properties->broker_addresses.assign(scan_params->broker_addresses.begin(),
544
0
                                                   scan_params->broker_addresses.end());
545
0
    }
546
64
    return system_properties;
547
185
}
548
549
12
std::string TableReader::debug_string() const {
550
12
    std::ostringstream out;
551
12
    out << "TableReader{format=" << file_format_to_string(_format)
552
12
        << ", push_down_agg_type=" << push_down_agg_to_string(_push_down_agg_type)
553
12
        << ", aggregate_pushdown_tried=" << _aggregate_pushdown_tried
554
12
        << ", has_current_reader=" << (_data_reader.reader != nullptr)
555
12
        << ", has_current_task=" << (_current_task != nullptr)
556
12
        << ", current_file=" << current_file_debug_string(_current_task)
557
12
        << ", has_delete_rows=" << (_delete_rows != nullptr)
558
12
        << ", delete_row_count=" << (_delete_rows == nullptr ? 0 : _delete_rows->size())
559
12
        << ", has_deletion_vector=" << (_deletion_vector != nullptr)
560
12
        << ", deletion_vector_cardinality="
561
12
        << (_deletion_vector == nullptr ? 0 : _deletion_vector->cardinality())
562
12
        << ", has_system_properties=" << (_system_properties != nullptr) << ", system_type="
563
12
        << (_system_properties == nullptr ? static_cast<int>(TFileType::FILE_LOCAL)
564
12
                                          : static_cast<int>(_system_properties->system_type))
565
12
        << ", has_scan_params=" << (_scan_params != nullptr)
566
12
        << ", has_io_ctx=" << (_io_ctx != nullptr)
567
12
        << ", has_runtime_state=" << (_runtime_state != nullptr)
568
12
        << ", has_scanner_profile=" << (_scanner_profile != nullptr)
569
12
        << ", mapper_options=" << _mapper_options.debug_string() << ", projected_columns="
570
12
        << join_table_reader_debug_strings(
571
12
                   _projected_columns,
572
12
                   [](const ColumnDefinition& column) { return column.debug_string(); })
573
12
        << ", partition_values=" << partition_values_debug_string(_partition_values)
574
12
        << ", table_filters="
575
12
        << join_table_reader_debug_strings(
576
12
                   _table_filters,
577
12
                   [](const TableFilter& filter) { return table_filter_debug_string(filter); })
578
12
        << ", conjunct_count=" << _conjuncts.size() << ", conjuncts="
579
12
        << join_table_reader_debug_strings(_conjuncts,
580
12
                                           [](const VExprContextSPtr& conjunct) {
581
1
                                               return expr_context_debug_string(conjunct);
582
1
                                           })
583
12
        << ", file_schema="
584
12
        << join_table_reader_debug_strings(
585
12
                   _data_reader.file_schema,
586
12
                   [](const ColumnDefinition& field) { return field.debug_string(); })
587
12
        << ", file_block_layout="
588
12
        << join_table_reader_debug_strings(
589
12
                   _data_reader.file_block_layout,
590
12
                   [](const FileBlockColumn& column) {
591
2
                       std::ostringstream column_out;
592
2
                       column_out << "FileBlockColumn{file_column_id=" << column.file_column_id
593
2
                                  << ", name=" << column.name << ", type="
594
2
                                  << (column.type == nullptr ? "null" : column.type->get_name())
595
2
                                  << "}";
596
2
                       return column_out.str();
597
2
                   })
598
12
        << ", block_template_columns=" << _data_reader.block_template.columns()
599
12
        << ", column_mapper="
600
12
        << (_data_reader.column_mapper == nullptr ? "null"
601
12
                                                  : _data_reader.column_mapper->debug_string())
602
12
        << "}";
603
12
    return out.str();
604
12
}
605
606
Status TableReader::annotate_projected_column(const TFileScanSlotInfo& slot_info,
607
                                              ProjectedColumnBuildContext* context,
608
31
                                              ColumnDefinition* column) const {
609
31
    (void)slot_info;
610
31
    DORIS_CHECK(context != nullptr);
611
31
    DORIS_CHECK(column != nullptr);
612
31
    context->schema_column.reset();
613
31
    const auto* schema_field = find_external_root_field(context->scan_params, *column);
614
31
    if (schema_field == nullptr) {
615
7
        return Status::OK();
616
7
    }
617
24
    const bool use_current_semantics = supports_iceberg_scan_semantics_v1(context->scan_params);
618
24
    context->schema_column = build_schema_column_from_external_field(*schema_field, column->type,
619
24
                                                                     use_current_semantics);
620
24
    if (!use_current_semantics) {
621
        // IDs and encoded defaults predate the result-changing semantics. Strip only the new
622
        // default channel so an old-FE plan keeps the same generic root/nested values on every BE.
623
7
        clear_initial_default_metadata(&*context->schema_column);
624
7
    }
625
24
    column->identifier = context->schema_column->identifier;
626
24
    column->name_mapping = context->schema_column->name_mapping;
627
24
    column->has_name_mapping = context->schema_column->has_name_mapping;
628
    // Projected roots already carry a generic FE default expression, but Iceberg binary defaults
629
    // need the raw Base64 marker so missing-file materialization can decode rather than copy text.
630
24
    column->initial_default_value = context->schema_column->initial_default_value;
631
24
    column->initial_default_value_is_base64 =
632
24
            context->schema_column->initial_default_value_is_base64;
633
24
    return Status::OK();
634
31
}
635
636
std::optional<ColumnDefinition> TableReader::_find_table_column_by_field_id(
637
15
        int32_t field_id, DataTypePtr type, bool include_historical_schemas) const {
638
15
    if (_scan_params == nullptr || !_scan_params->__isset.history_schema_info ||
639
15
        _scan_params->history_schema_info.empty()) {
640
2
        return std::nullopt;
641
2
    }
642
14
    const auto find_field = [field_id](const schema::external::TSchema& schema) {
643
14
        if (!schema.__isset.root_field || !schema.root_field.__isset.fields) {
644
0
            return static_cast<const schema::external::TField*>(nullptr);
645
0
        }
646
26
        for (const auto& field_ptr : schema.root_field.fields) {
647
26
            const auto* field = get_field_ptr(field_ptr);
648
26
            if (field != nullptr && field->__isset.id && field->id == field_id) {
649
13
                return field;
650
13
            }
651
26
        }
652
1
        return static_cast<const schema::external::TField*>(nullptr);
653
14
    };
654
655
13
    const auto* current_schema = &_scan_params->history_schema_info.front();
656
13
    if (_scan_params->__isset.current_schema_id) {
657
13
        for (const auto& candidate_schema : _scan_params->history_schema_info) {
658
13
            if (candidate_schema.__isset.schema_id &&
659
13
                candidate_schema.schema_id == _scan_params->current_schema_id) {
660
13
                current_schema = &candidate_schema;
661
13
                break;
662
13
            }
663
13
        }
664
13
    }
665
13
    if (const auto* field = find_field(*current_schema); field != nullptr) {
666
12
        return build_schema_column_from_external_field(
667
12
                *field, std::move(type), supports_iceberg_scan_semantics_v1(_scan_params));
668
12
    }
669
1
    if (!include_historical_schemas) {
670
0
        return std::nullopt;
671
0
    }
672
673
1
    const schema::external::TSchema* latest_schema = nullptr;
674
1
    const schema::external::TField* latest_field = nullptr;
675
2
    for (const auto& candidate_schema : _scan_params->history_schema_info) {
676
2
        if (&candidate_schema == current_schema) {
677
1
            continue;
678
1
        }
679
1
        const auto* candidate_field = find_field(candidate_schema);
680
1
        if (candidate_field == nullptr) {
681
0
            continue;
682
0
        }
683
1
        if (latest_schema == nullptr || (candidate_schema.__isset.schema_id &&
684
0
                                         (!latest_schema->__isset.schema_id ||
685
1
                                          candidate_schema.schema_id > latest_schema->schema_id))) {
686
1
            latest_schema = &candidate_schema;
687
1
            latest_field = candidate_field;
688
1
        }
689
1
    }
690
1
    if (latest_field == nullptr) {
691
0
        return std::nullopt;
692
0
    }
693
1
    return build_schema_column_from_external_field(
694
1
            *latest_field, std::move(type), supports_iceberg_scan_semantics_v1(_scan_params));
695
1
}
696
697
185
Status TableReader::init(TableReadOptions&& options) {
698
185
    _scanner_profile = options.scanner_profile;
699
185
    if (_scanner_profile != nullptr) {
700
75
        const auto hierarchy = file_scan_profile::ensure_hierarchy(_scanner_profile);
701
75
        static const char* table_profile = file_scan_profile::TABLE_READER;
702
75
        static const char* file_reader_profile = file_scan_profile::FILE_READER;
703
75
        _profile.total_timer = hierarchy.table_reader;
704
75
        _profile.file_reader_total_timer = hierarchy.file_reader;
705
75
        _profile.init_timer =
706
75
                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "InitTime", table_profile, 1);
707
75
        _profile.num_delete_files = ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, "NumDeleteFiles",
708
75
                                                                 TUnit::UNIT, table_profile, 1);
709
75
        _profile.num_delete_rows = ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, "NumDeleteRows",
710
75
                                                                TUnit::UNIT, table_profile, 1);
711
75
        _profile.parse_delete_file_time = ADD_CHILD_TIMER_WITH_LEVEL(
712
75
                _scanner_profile, "ParseDeleteFileTime", table_profile, 1);
713
75
        _profile.decoded_dv_cache_hit_count =
714
75
                ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, "DeletionVectorDecodedCacheHitCount",
715
75
                                             TUnit::UNIT, table_profile, 1);
716
75
        _profile.decoded_dv_cache_miss_count = ADD_CHILD_COUNTER_WITH_LEVEL(
717
75
                _scanner_profile, "DeletionVectorDecodedCacheMissCount", TUnit::UNIT, table_profile,
718
75
                1);
719
75
        _profile.dv_file_cache_hit_count = ADD_CHILD_COUNTER_WITH_LEVEL(
720
75
                _scanner_profile, "DeletionVectorFileCacheHitCount", TUnit::UNIT, table_profile, 1);
721
75
        _profile.dv_file_cache_miss_count =
722
75
                ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, "DeletionVectorFileCacheMissCount",
723
75
                                             TUnit::UNIT, table_profile, 1);
724
75
        _profile.dv_file_cache_peer_read_count = ADD_CHILD_COUNTER_WITH_LEVEL(
725
75
                _scanner_profile, "DeletionVectorFileCachePeerReadCount", TUnit::UNIT,
726
75
                table_profile, 1);
727
75
        _profile.exec_timer =
728
75
                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "GetBlockTime", table_profile, 1);
729
75
        _profile.prepare_split_timer =
730
75
                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "PrepareSplitTime", table_profile, 1);
731
75
        _profile.finalize_timer =
732
75
                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "FinalizeBlockTime", table_profile, 1);
733
75
        _profile.create_reader_timer =
734
75
                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "CreateReaderTime", table_profile, 1);
735
75
        _profile.pushdown_agg_timer =
736
75
                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "PushDownAggTime", table_profile, 1);
737
75
        _profile.open_reader_timer =
738
75
                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "OpenReaderTime", table_profile, 1);
739
75
        _profile.runtime_filter_partition_prune_timer = ADD_CHILD_TIMER_WITH_LEVEL(
740
75
                _scanner_profile, "FileScannerRuntimeFilterPartitionPruningTime", table_profile, 1);
741
75
        _profile.runtime_filter_partition_pruned_range_counter = ADD_CHILD_COUNTER_WITH_LEVEL(
742
75
                _scanner_profile, "RuntimeFilterPartitionPrunedRangeNum", TUnit::UNIT,
743
75
                table_profile, 1);
744
75
        _profile.close_timer =
745
75
                ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "CloseTime", table_profile, 1);
746
        // Lifecycle timer names remain globally unique because RuntimeProfile's visual hierarchy
747
        // does not namespace counters that share the same display parent.
748
75
        _profile.file_reader_init_timer = ADD_CHILD_TIMER_WITH_LEVEL(
749
75
                _scanner_profile, "FileReaderInitTime", file_reader_profile, 1);
750
75
        _profile.file_reader_schema_timer = ADD_CHILD_TIMER_WITH_LEVEL(
751
75
                _scanner_profile, "FileReaderGetSchemaTime", file_reader_profile, 1);
752
75
        _profile.file_reader_mapper_timer = ADD_CHILD_TIMER_WITH_LEVEL(
753
75
                _scanner_profile, "FileReaderCreateColumnMapperTime", file_reader_profile, 1);
754
75
        _profile.file_reader_open_timer = ADD_CHILD_TIMER_WITH_LEVEL(
755
75
                _scanner_profile, "FileReaderOpenTime", file_reader_profile, 1);
756
75
        _profile.file_reader_get_block_timer = ADD_CHILD_TIMER_WITH_LEVEL(
757
75
                _scanner_profile, "FileReaderGetBlockTime", file_reader_profile, 1);
758
75
        _profile.file_reader_aggregate_timer = ADD_CHILD_TIMER_WITH_LEVEL(
759
75
                _scanner_profile, "FileReaderAggregatePushDownTime", file_reader_profile, 1);
760
75
        _profile.file_reader_close_timer = ADD_CHILD_TIMER_WITH_LEVEL(
761
75
                _scanner_profile, "FileReaderCloseTime", file_reader_profile, 1);
762
75
    }
763
    // Establish lifecycle timers before consuming options or constructing filesystem properties;
764
    // placing these scopes at the tail records only scope teardown and hides expensive init work.
765
185
    SCOPED_TIMER(_profile.total_timer);
766
185
    SCOPED_TIMER(_profile.init_timer);
767
185
    _scan_params = options.scan_params;
768
185
    _format = options.format;
769
185
    _io_ctx = options.io_ctx;
770
185
    _runtime_state = options.runtime_state;
771
185
    _file_slot_descs = options.file_slot_descs;
772
185
    _push_down_agg_type = options.push_down_agg_type;
773
185
    _push_down_count_columns = options.push_down_count_columns;
774
185
    _initial_condition_cache_digest = options.condition_cache_digest;
775
185
    _condition_cache_digest = _initial_condition_cache_digest;
776
185
    _projected_columns = std::move(options.projected_columns);
777
185
    if (supports_iceberg_scan_semantics_v1(_scan_params)) {
778
58
        for (auto& projected_column : _projected_columns) {
779
58
            const auto* schema_field = find_external_root_field(_scan_params, projected_column);
780
58
            if (schema_field != nullptr) {
781
18
                attach_full_schema_identity(
782
18
                        &projected_column,
783
18
                        build_schema_identity_from_external_field(*schema_field));
784
18
            }
785
58
        }
786
50
    }
787
185
    _system_properties = create_system_properties(_scan_params);
788
185
    _mapper_options.mode = TableColumnMappingMode::BY_NAME;
789
185
    _conjuncts = std::move(options.conjuncts);
790
185
    return Status::OK();
791
185
}
792
793
129
Status TableReader::_build_table_filters_from_conjuncts() {
794
129
    _table_filters.clear();
795
129
    _constant_pruning_safe_filter_count = 0;
796
129
    bool in_safe_prefix = true;
797
129
    for (const auto& conjunct : _conjuncts) {
798
35
        DORIS_CHECK(conjunct != nullptr);
799
35
        DORIS_CHECK(conjunct->root() != nullptr);
800
        // `_table_filters` omits expressions without slot references, but such an expression still
801
        // occupies a position in the row-level conjunct order. Record how many localized filters
802
        // precede the first unsafe original conjunct so constant pruning cannot jump over a
803
        // slotless non-deterministic/error-preserving barrier. Unsafe predicates remain solely on
804
        // Scanner's original row-level path because localizing a clone would execute their state
805
        // twice with independent state.
806
35
        if (in_safe_prefix && !_is_safe_to_pre_execute(conjunct)) {
807
5
            in_safe_prefix = false;
808
5
        }
809
35
        RETURN_IF_ERROR(
810
35
                build_table_filters_from_conjunct(conjunct, _runtime_state, &_table_filters));
811
35
        if (in_safe_prefix) {
812
28
            _constant_pruning_safe_filter_count = _table_filters.size();
813
28
        }
814
35
    }
815
129
    return Status::OK();
816
129
}
817
818
127
Status TableReader::_open_local_filter_exprs(const FileScanRequest& file_request) {
819
127
    RowDescriptor row_desc;
820
127
    for (const auto& conjunct : file_request.conjuncts) {
821
24
        RETURN_IF_ERROR(conjunct->prepare(_runtime_state, row_desc));
822
24
        RETURN_IF_ERROR(conjunct->open(_runtime_state));
823
24
    }
824
127
    for (const auto& delete_conjunct : file_request.delete_conjuncts) {
825
38
        RETURN_IF_ERROR(delete_conjunct->prepare(_runtime_state, row_desc));
826
38
        RETURN_IF_ERROR(delete_conjunct->open(_runtime_state));
827
38
    }
828
127
    return Status::OK();
829
127
}
830
831
127
bool TableReader::_should_enable_condition_cache(const FileScanRequest& file_request) const {
832
127
    if (_condition_cache_digest == 0 || _push_down_agg_type == TPushAggOp::type::COUNT ||
833
127
        _current_file_description == std::nullopt || _data_reader.reader == nullptr) {
834
120
        return false;
835
120
    }
836
    // Condition cache is populated by file readers after evaluating file-local row-level
837
    // conjuncts. Metadata pruning can skip row groups/pages, but it does not produce a per-row
838
    // survivor bitmap that can safely populate the cache.
839
7
    if (file_request.conjuncts.empty()) {
840
1
        return false;
841
1
    }
842
    // Delete files/deletion vectors are table-format state. They may change independently of the
843
    // data file path/mtime/size used by the external cache key, so caching their result can become
844
    // stale. Keep delete filtering enabled, but do not read or write condition cache.
845
6
    if (_delete_rows != nullptr || _deletion_vector != nullptr ||
846
6
        !file_request.delete_conjuncts.empty()) {
847
1
        return false;
848
1
    }
849
    // Only scanner-driven splits provide a digest rebuilt from the exact RF snapshot. Keep the
850
    // conservative behavior for standalone TableReader callers: their initial digest may describe
851
    // only static predicate P and must not store P AND RF under that key.
852
5
    return _condition_cache_digest_covers_current_split ||
853
5
           !contains_runtime_filter(file_request.conjuncts);
854
6
}
855
856
127
Status TableReader::_init_reader_condition_cache(const FileScanRequest& file_request) {
857
127
    _condition_cache = nullptr;
858
127
    _condition_cache_ctx = nullptr;
859
127
    if (!_should_enable_condition_cache(file_request)) {
860
123
        return Status::OK();
861
123
    }
862
863
4
    auto* cache = segment_v2::ConditionCache::instance();
864
4
    if (cache == nullptr) {
865
0
        return Status::OK();
866
0
    }
867
4
    const auto& file = *_current_file_description;
868
4
    _condition_cache_key = segment_v2::ConditionCache::ExternalCacheKey(
869
4
            file.path, file.mtime, file.file_size, _condition_cache_digest, file.range_start_offset,
870
4
            file.range_size,
871
4
            segment_v2::ConditionCache::ExternalCacheKey::BASE_GRANULE_AWARE_VERSION);
872
873
4
    segment_v2::ConditionCacheHandle handle;
874
4
    const bool condition_cache_hit = cache->lookup(_condition_cache_key, &handle);
875
4
    if (condition_cache_hit) {
876
0
        _condition_cache = handle.get_filter_result();
877
0
        ++_condition_cache_hit_count;
878
4
    } else {
879
4
        const int64_t total_rows = _data_reader.reader->get_total_rows();
880
4
        if (total_rows <= 0) {
881
0
            return Status::OK();
882
0
        }
883
        // Add one guard granule for split ranges that start in the middle of a granule. A guard
884
        // false bit beyond the real range never overlaps real rows, but avoids boundary overflow
885
        // when a reader marks the last partial granule.
886
4
        const size_t num_granules = (total_rows + ConditionCacheContext::GRANULE_SIZE - 1) /
887
4
                                    ConditionCacheContext::GRANULE_SIZE;
888
4
        _condition_cache = std::make_shared<std::vector<bool>>(num_granules + 1, false);
889
4
    }
890
891
4
    if (_condition_cache != nullptr) {
892
4
        _condition_cache_ctx = std::make_shared<ConditionCacheContext>();
893
4
        _condition_cache_ctx->is_hit = condition_cache_hit;
894
4
        _condition_cache_ctx->filter_result = _condition_cache;
895
4
        _condition_cache_ctx->num_granules = _condition_cache->size();
896
4
        if (condition_cache_hit) {
897
0
            _condition_cache_ctx->base_granule = handle.get_base_granule();
898
0
        }
899
4
        _data_reader.reader->set_condition_cache_context(_condition_cache_ctx);
900
4
    }
901
4
    return Status::OK();
902
4
}
903
904
129
void TableReader::_finalize_reader_condition_cache() {
905
129
    if (_condition_cache_ctx == nullptr || _condition_cache_ctx->is_hit) {
906
125
        _condition_cache = nullptr;
907
125
        _condition_cache_ctx = nullptr;
908
125
        return;
909
125
    }
910
    // LIMIT or scanner cancellation may close a reader before all selected row ranges are visited.
911
    // Unvisited granules remain false in a MISS bitmap, so inserting a partial bitmap would make a
912
    // later HIT skip valid rows. Only publish cache entries after the physical reader reaches EOF.
913
4
    if (!_current_reader_reached_eof) {
914
2
        _condition_cache = nullptr;
915
2
        _condition_cache_ctx = nullptr;
916
2
        return;
917
2
    }
918
2
    DORIS_CHECK(_condition_cache_ctx->num_granules <= _condition_cache->size());
919
2
    _condition_cache->resize(_condition_cache_ctx->num_granules);
920
2
    segment_v2::ConditionCache::instance()->insert(
921
2
            _condition_cache_key, std::move(_condition_cache), _condition_cache_ctx->base_granule);
922
2
    _condition_cache = nullptr;
923
2
    _condition_cache_ctx = nullptr;
924
2
}
925
926
171
Status TableReader::create_next_reader(bool* eos) {
927
171
    SCOPED_TIMER(_profile.create_reader_timer);
928
171
    DCHECK(_data_reader.reader == nullptr);
929
171
    if (_current_task == nullptr) {
930
41
        *eos = true;
931
41
        return Status::OK();
932
41
    }
933
934
130
    RETURN_IF_ERROR(create_file_reader(&_data_reader.reader));
935
130
    DORIS_CHECK(_data_reader.reader != nullptr);
936
130
    if (_batch_size > 0) {
937
0
        _data_reader.reader->set_batch_size(_batch_size);
938
0
    }
939
130
    Status st;
940
130
    {
941
130
        SCOPED_TIMER(_profile.file_reader_total_timer);
942
130
        SCOPED_TIMER(_profile.file_reader_init_timer);
943
130
        st = _data_reader.reader->init(_runtime_state);
944
130
    }
945
130
    if (!st.ok()) {
946
1
        if (_io_ctx != nullptr && _io_ctx->should_stop && st.is<ErrorCode::END_OF_FILE>()) {
947
0
            *eos = true;
948
0
            _data_reader.reader.reset();
949
0
            return Status::OK();
950
0
        }
951
1
        return st;
952
1
    }
953
129
    st = open_reader();
954
129
    if (!st.ok()) {
955
1
        if (_io_ctx != nullptr && _io_ctx->should_stop && st.is<ErrorCode::END_OF_FILE>()) {
956
0
            *eos = true;
957
0
            _data_reader.reader.reset();
958
0
            return Status::OK();
959
0
        }
960
1
        return st;
961
1
    }
962
128
    if (_data_reader.reader == nullptr) {
963
1
        *eos = _current_task == nullptr;
964
1
        return Status::OK();
965
1
    }
966
127
    *eos = false;
967
127
    return Status::OK();
968
128
}
969
970
103
Status TableReader::create_file_reader(std::unique_ptr<FileReader>* reader) {
971
103
    DORIS_CHECK(reader != nullptr);
972
103
    const bool enable_mapping_timestamp_tz = _scan_params != nullptr &&
973
103
                                             _scan_params->__isset.enable_mapping_timestamp_tz &&
974
103
                                             _scan_params->enable_mapping_timestamp_tz;
975
103
    const bool enable_mapping_varbinary = _scan_params != nullptr &&
976
103
                                          _scan_params->__isset.enable_mapping_varbinary &&
977
103
                                          _scan_params->enable_mapping_varbinary;
978
103
    if (_format == FileFormat::PARQUET) {
979
        // V2 must honor the scan contract directly; otherwise Hive STRING columns backed by an
980
        // unannotated BYTE_ARRAY are silently exposed as VARBINARY and predicate bytes no longer
981
        // match the table type.
982
100
        *reader = std::make_unique<format::parquet::ParquetReader>(
983
100
                _system_properties, _current_task->data_file, _io_ctx, _scanner_profile,
984
100
                _global_rowid_context, enable_mapping_timestamp_tz, enable_mapping_varbinary);
985
100
        return Status::OK();
986
100
    }
987
3
    if (_format == FileFormat::ORC) {
988
3
        *reader = std::make_unique<format::orc::OrcReader>(
989
3
                _system_properties, _current_task->data_file, _io_ctx, _scanner_profile,
990
3
                _global_rowid_context, enable_mapping_timestamp_tz);
991
3
        return Status::OK();
992
3
    }
993
0
    if (_format == FileFormat::CSV) {
994
0
        if (_file_slot_descs == nullptr) {
995
0
            return Status::InvalidArgument("CSV reader requires file slot descriptors");
996
0
        }
997
        // CSV has no embedded schema. TableReader owns table-level mapping, while CsvReader needs
998
        // only the physical file slots plus scan text parameters to build a file-local schema.
999
        // Non-file columns such as partitions/defaults/virtual row ids are intentionally excluded
1000
        // from `_file_slot_descs` and are materialized during finalize_chunk().
1001
0
        *reader = std::make_unique<format::csv::CsvReader>(
1002
0
                _system_properties, _current_task->data_file, _io_ctx, _scanner_profile,
1003
0
                _scan_params, *_file_slot_descs, _current_range_compress_type,
1004
0
                _current_range_load_id);
1005
0
        return Status::OK();
1006
0
    }
1007
0
    if (_format == FileFormat::TEXT) {
1008
0
        if (_file_slot_descs == nullptr) {
1009
0
            return Status::InvalidArgument("Text reader requires file slot descriptors");
1010
0
        }
1011
        // Text files have no embedded schema. As with CSV, TableReader handles table-level mapping
1012
        // and only passes physical file slots to the v2 TextReader.
1013
0
        *reader = std::make_unique<format::text::TextReader>(
1014
0
                _system_properties, _current_task->data_file, _io_ctx, _scanner_profile,
1015
0
                _scan_params, *_file_slot_descs, _current_range_compress_type,
1016
0
                _current_range_load_id);
1017
0
        return Status::OK();
1018
0
    }
1019
0
    if (_format == FileFormat::JSON) {
1020
0
        if (_file_slot_descs == nullptr) {
1021
0
            return Status::InvalidArgument("JSON reader requires file slot descriptors");
1022
0
        }
1023
0
        *reader = std::make_unique<format::json::JsonReader>(
1024
0
                _system_properties, _current_task->data_file, _io_ctx, _scanner_profile,
1025
0
                _scan_params, _current_file_range_desc, *_file_slot_descs,
1026
0
                _current_range_compress_type, _current_range_load_id);
1027
0
        return Status::OK();
1028
0
    }
1029
0
    if (_format == FileFormat::NATIVE) {
1030
0
        *reader = std::make_unique<format::native::NativeReader>(
1031
0
                _system_properties, _current_task->data_file, _io_ctx, _scanner_profile);
1032
0
        return Status::OK();
1033
0
    }
1034
0
    return Status::NotSupported("TableReader does not support file format {}",
1035
0
                                file_format_to_string(_format));
1036
0
}
1037
1038
161
std::unique_ptr<io::FileDescription> create_file_description(const TFileRangeDesc& range) {
1039
161
    auto file_description = std::make_unique<io::FileDescription>();
1040
161
    file_description->path = range.path;
1041
161
    file_description->file_size = range.__isset.file_size ? range.file_size : -1;
1042
161
    file_description->mtime = range.__isset.modification_time ? range.modification_time : 0;
1043
161
    file_description->range_start_offset = range.__isset.start_offset ? range.start_offset : 0;
1044
161
    file_description->range_size = range.__isset.size ? range.size : -1;
1045
161
    if (range.__isset.fs_name) {
1046
5
        file_description->fs_name = range.fs_name;
1047
5
    }
1048
161
    if (range.__isset.file_cache_admission) {
1049
0
        file_description->file_cache_admission = range.file_cache_admission;
1050
0
    }
1051
161
    return file_description;
1052
161
}
1053
1054
162
Status TableReader::prepare_split(const SplitReadOptions& options) {
1055
162
    SCOPED_TIMER(_profile.total_timer);
1056
162
    SCOPED_TIMER(_profile.prepare_split_timer);
1057
162
    _current_split_pruned = false;
1058
162
    _all_runtime_filters_applied_for_split = options.all_runtime_filters_applied;
1059
162
    _condition_cache_digest_covers_current_split = options.condition_cache_digest.has_value();
1060
162
    if (options.condition_cache_digest.has_value()) {
1061
        // The split snapshot may include RFs that arrived after TableReader::init(). Use the digest
1062
        // computed from that exact snapshot. Example: an initial P digest must not be used to store
1063
        // the bitmap for P AND late RF{7, 9}; the scanner supplies digest(P AND RF{7, 9}) here.
1064
1
        _condition_cache_digest = *options.condition_cache_digest;
1065
161
    } else {
1066
        // An explicit scanner digest is split-scoped. Restore the init-time digest when a later
1067
        // standalone split omits it instead of leaking the previous split's RF payload into its key.
1068
161
        _condition_cache_digest = _initial_condition_cache_digest;
1069
161
    }
1070
162
    if (options.conjuncts.has_value()) {
1071
2
        _conjuncts = *options.conjuncts;
1072
2
    }
1073
    // Update to current split format to handle ORC/PARQUET files in one table.
1074
162
    _format = options.current_split_format;
1075
162
    _partition_values = std::move(options.partition_values);
1076
162
    _current_task.reset();
1077
162
    _current_file_description.reset();
1078
162
    _current_file_range_desc = options.current_range;
1079
162
    _current_range_compress_type = options.current_range.__isset.compress_type
1080
162
                                           ? options.current_range.compress_type
1081
162
                                           : TFileCompressType::UNKNOWN;
1082
162
    _current_range_load_id = options.current_range.__isset.load_id
1083
162
                                     ? std::make_optional(options.current_range.load_id)
1084
162
                                     : std::nullopt;
1085
162
    _global_rowid_context = options.global_rowid_context;
1086
162
    _delete_rows = nullptr;
1087
162
    _deletion_vector = nullptr;
1088
162
    _aggregate_pushdown_tried = false;
1089
162
    _remaining_table_level_count = -1;
1090
162
    _remaining_file_level_count = -1;
1091
162
    _current_split_uses_metadata_count = false;
1092
162
    _current_reader_reached_eof = false;
1093
162
    RETURN_IF_ERROR(_evaluate_partition_prune_conjuncts(options.partition_prune_conjuncts,
1094
162
                                                        &_current_split_pruned));
1095
162
    if (_current_split_pruned) {
1096
1
        COUNTER_UPDATE(_profile.runtime_filter_partition_pruned_range_counter, 1);
1097
1
        return Status::OK();
1098
1
    }
1099
161
    _current_task = std::make_unique<ScanTask>();
1100
161
    _current_task->data_file = create_file_description(options.current_range);
1101
161
    _current_file_description = *_current_task->data_file;
1102
    // A table-level row count is only equivalent to scanning the split when no row predicate is
1103
    // active and no predicate can arrive later. The metadata path can return several batches for
1104
    // one split; after its first synthetic batch there is no way to recover the real rows if a
1105
    // runtime filter arrives before the next scheduler turn.
1106
    // Table-level metadata only contains the number of rows; it cannot evaluate an expression or
1107
    // the NULL state of a COUNT argument. Require the new FE's explicit empty argument list, which
1108
    // means COUNT(*)/COUNT(1). A non-empty list means COUNT(col), while nullopt comes from an old FE
1109
    // whose COUNT semantics are unknown during a BE-first rolling upgrade.
1110
161
    if (_push_down_agg_type == TPushAggOp::type::COUNT && _push_down_count_columns.has_value() &&
1111
161
        _push_down_count_columns->empty() && options.all_runtime_filters_applied &&
1112
161
        _conjuncts.empty() && options.current_range.__isset.table_format_params &&
1113
161
        options.current_range.table_format_params.__isset.table_level_row_count) {
1114
3
        DORIS_CHECK(options.current_range.table_format_params.table_level_row_count >= -1);
1115
3
        _remaining_table_level_count =
1116
3
                options.current_range.table_format_params.table_level_row_count;
1117
3
        _current_split_uses_metadata_count = _is_table_level_count_active();
1118
3
    }
1119
161
    if (_is_table_level_count_active()) {
1120
2
        return Status::OK();
1121
2
    }
1122
159
    return _parse_delete_predicates(options);
1123
161
}
1124
1125
Status TableReader::_evaluate_partition_prune_conjuncts(const VExprContextSPtrs& conjuncts,
1126
162
                                                        bool* can_filter_all) {
1127
162
    DORIS_CHECK(can_filter_all != nullptr);
1128
162
    SCOPED_TIMER(_profile.runtime_filter_partition_prune_timer);
1129
162
    *can_filter_all = false;
1130
162
    if (conjuncts.empty() || _partition_values.empty()) {
1131
159
        return Status::OK();
1132
159
    }
1133
1134
3
    VExprContextSPtrs partition_conjuncts;
1135
3
    for (const auto& conjunct : conjuncts) {
1136
3
        DORIS_CHECK(conjunct != nullptr);
1137
3
        DORIS_CHECK(conjunct->root() != nullptr);
1138
        // Keep only the safe prefix of the original conjunct order. If an unsafe conjunct is
1139
        // skipped, a later predicate could prune the split before the unsafe one reaches its
1140
        // normal row-level evaluation point.
1141
3
        if (!_is_safe_to_pre_execute(conjunct)) {
1142
1
            break;
1143
1
        }
1144
2
        std::set<GlobalIndex> global_indices;
1145
2
        collect_global_indices(conjunct->root(), &global_indices);
1146
2
        if (global_indices.empty()) {
1147
0
            continue;
1148
0
        }
1149
2
        const bool partition_only = std::ranges::all_of(global_indices, [&](GlobalIndex index) {
1150
2
            if (index.value() >= _projected_columns.size()) {
1151
0
                return false;
1152
0
            }
1153
2
            const auto& column = _projected_columns[index.value()];
1154
2
            return column.is_partition_key &&
1155
2
                   find_partition_value(column, _partition_values) != nullptr;
1156
2
        });
1157
2
        if (partition_only) {
1158
2
            partition_conjuncts.push_back(conjunct);
1159
2
        }
1160
2
    }
1161
3
    if (partition_conjuncts.empty()) {
1162
1
        return Status::OK();
1163
1
    }
1164
1165
2
    Block block;
1166
2
    RETURN_IF_ERROR(_build_partition_prune_block(&block));
1167
2
    RowDescriptor row_desc;
1168
2
    for (const auto& conjunct : partition_conjuncts) {
1169
2
        RETURN_IF_ERROR(conjunct->prepare(_runtime_state, row_desc));
1170
2
        RETURN_IF_ERROR(conjunct->open(_runtime_state));
1171
2
    }
1172
2
    IColumn::Filter result_filter(block.rows(), 1);
1173
2
    return VExprContext::execute_conjuncts(partition_conjuncts, nullptr, &block, &result_filter,
1174
2
                                           can_filter_all);
1175
2
}
1176
1177
64
bool TableReader::_is_safe_to_pre_execute(const VExprContextSPtr& conjunct) {
1178
64
    DORIS_CHECK(conjunct != nullptr);
1179
64
    DORIS_CHECK(conjunct->root() != nullptr);
1180
64
    const auto root = conjunct->root();
1181
64
    const auto impl = root->get_impl();
1182
64
    const auto predicate = impl != nullptr ? impl : root;
1183
    // Split pruning evaluates a predicate once before any file rows are read. Reordering
1184
    // non-deterministic or error-preserving expressions can change their row-level semantics,
1185
    // even when every referenced slot is a partition column or maps to a constant entry.
1186
64
    return predicate->is_safe_to_execute_on_selected_rows();
1187
64
}
1188
1189
2
Status TableReader::_build_partition_prune_block(Block* block) const {
1190
2
    DORIS_CHECK(block != nullptr);
1191
2
    DORIS_CHECK(!_projected_columns.empty());
1192
2
    block->clear();
1193
2
    for (const auto& column : _projected_columns) {
1194
2
        DORIS_CHECK(column.type != nullptr);
1195
2
        ColumnPtr value_column = column.type->create_column_const_with_default_value(1);
1196
2
        if (column.is_partition_key) {
1197
2
            const auto* partition_value = find_partition_value(column, _partition_values);
1198
2
            if (partition_value != nullptr) {
1199
2
                value_column = column.type->create_column_const(1, *partition_value);
1200
2
            }
1201
2
        }
1202
2
        block->insert({std::move(value_column), column.type, column.name});
1203
2
    }
1204
2
    return Status::OK();
1205
2
}
1206
1207
159
Status TableReader::_parse_delete_predicates(const SplitReadOptions& options) {
1208
159
    DeleteFileDesc desc {.fs_name = options.current_range.fs_name};
1209
159
    bool has_delete_file = false;
1210
159
    RETURN_IF_ERROR(_parse_deletion_vector_file(options.current_range.table_format_params, &desc,
1211
159
                                                &has_delete_file));
1212
159
    if (has_delete_file) {
1213
7
        DORIS_CHECK(options.cache != nullptr);
1214
7
        Status create_status = Status::OK();
1215
1216
7
        bool decoded_cache_hit = false;
1217
7
        _deletion_vector = options.cache->get<DeletionVector>(
1218
7
                desc.key,
1219
7
                [&]() -> DeletionVector* {
1220
7
                    auto deletion_vector = std::make_unique<DeletionVector>();
1221
1222
7
                    DeletionVectorReader dv_reader(_runtime_state, _scanner_profile, *_scan_params,
1223
7
                                                   desc, _io_ctx.get());
1224
7
                    create_status = dv_reader.open();
1225
7
                    if (!create_status.ok()) [[unlikely]] {
1226
0
                        return nullptr;
1227
0
                    }
1228
1229
7
                    size_t bytes_read = desc.size;
1230
7
                    std::vector<char> buffer(bytes_read);
1231
7
                    DBUG_EXECUTE_IF("TableReader.parse_deletion_vector.io_error", {
1232
7
                        create_status =
1233
7
                                Status::IOError("injected format v2 deletion vector read failure");
1234
7
                        return nullptr;
1235
7
                    });
1236
6
                    DBUG_EXECUTE_IF("TableReader.parse_deletion_vector.should_stop", {
1237
6
                        create_status = Status::EndOfFile("stop read.");
1238
6
                        return nullptr;
1239
6
                    });
1240
5
                    create_status =
1241
5
                            dv_reader.read_at(desc.start_offset, {buffer.data(), bytes_read});
1242
5
                    const auto& file_cache_stats = dv_reader.file_cache_statistics();
1243
5
                    COUNTER_UPDATE(_profile.dv_file_cache_hit_count,
1244
5
                                   file_cache_stats.num_local_io_total);
1245
5
                    COUNTER_UPDATE(_profile.dv_file_cache_miss_count,
1246
5
                                   file_cache_stats.num_remote_io_total);
1247
5
                    COUNTER_UPDATE(_profile.dv_file_cache_peer_read_count,
1248
5
                                   file_cache_stats.num_peer_io_total);
1249
5
                    if (!create_status.ok()) [[unlikely]] {
1250
0
                        return nullptr;
1251
0
                    }
1252
1253
5
                    const char* buf = buffer.data();
1254
5
                    SCOPED_TIMER(_profile.parse_delete_file_time);
1255
5
                    create_status = parse_deletion_vector(buf, bytes_read, desc.format,
1256
5
                                                          deletion_vector.get());
1257
5
                    if (!create_status.ok()) [[unlikely]] {
1258
1
                        return nullptr;
1259
1
                    }
1260
4
                    COUNTER_UPDATE(_profile.num_delete_rows, deletion_vector->cardinality());
1261
4
                    return deletion_vector.release();
1262
5
                },
1263
7
                &decoded_cache_hit);
1264
7
        RETURN_IF_ERROR(create_status);
1265
4
        COUNTER_UPDATE(decoded_cache_hit ? _profile.decoded_dv_cache_hit_count
1266
4
                                         : _profile.decoded_dv_cache_miss_count,
1267
4
                       1);
1268
4
    }
1269
1270
156
    return Status::OK();
1271
159
}
1272
} // namespace doris::format