Coverage Report

Created: 2026-07-14 01:40

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format_v2/table_reader.h
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
#pragma once
19
20
#include <bvar/status.h>
21
22
#include <algorithm>
23
#include <exception>
24
#include <map>
25
#include <memory>
26
#include <optional>
27
#include <string>
28
#include <string_view>
29
#include <utility>
30
#include <vector>
31
32
#include "common/cast_set.h"
33
#include "common/exception.h"
34
#include "common/logging.h"
35
#include "common/status.h"
36
#include "core/assert_cast.h"
37
#include "core/block/block.h"
38
#include "core/column/column_array.h"
39
#include "core/column/column_const.h"
40
#include "core/column/column_map.h"
41
#include "core/column/column_nullable.h"
42
#include "core/column/column_struct.h"
43
#include "core/column/column_vector.h"
44
#include "core/data_type/data_type.h"
45
#include "core/data_type/data_type_array.h"
46
#include "core/data_type/data_type_map.h"
47
#include "core/data_type/data_type_nullable.h"
48
#include "core/data_type/data_type_number.h"
49
#include "core/data_type/data_type_string.h"
50
#include "core/data_type/data_type_struct.h"
51
#include "core/field.h"
52
#include "exec/common/stringop_substring.h"
53
#include "exprs/vexpr.h"
54
#include "exprs/vexpr_context.h"
55
#include "exprs/vexpr_fwd.h"
56
#include "exprs/vslot_ref.h"
57
#include "format/table/deletion_vector.h"
58
#include "format_v2/column_data.h"
59
#include "format_v2/column_mapper.h"
60
#include "format_v2/expr/cast.h"
61
#include "format_v2/expr/delete_predicate.h"
62
#include "format_v2/file_reader.h"
63
#include "format_v2/parquet/reader/column_reader.h"
64
#include "format_v2/schema_projection.h"
65
#include "gen_cpp/PlanNodes_types.h"
66
#include "io/io_common.h"
67
#include "runtime/descriptors.h"
68
#include "storage/segment/condition_cache.h"
69
70
namespace doris {
71
class Block;
72
struct DeleteFileDesc;
73
class RuntimeState;
74
} // namespace doris
75
76
namespace doris::format {
77
78
using DeleteRows = std::vector<int64_t>;
79
80
// Row-level predicates on table/global schema. They are rewritten to file-local expressions when
81
// possible, and remain the source of row-level filtering after localization.
82
struct TableFilter {
83
    VExprContextSPtr conjunct;
84
    std::vector<GlobalIndex> global_indices;
85
};
86
87
struct ScanTask {
88
30.5k
    virtual ~ScanTask() = default;
89
90
    std::unique_ptr<io::FileDescription> data_file;
91
};
92
93
struct ProjectedColumnBuildContext {
94
    const TFileScanRangeParams* scan_params = nullptr;
95
    const TFileRangeDesc* range = nullptr;
96
    RuntimeState* runtime_state = nullptr;
97
    std::optional<ColumnDefinition> schema_column = std::nullopt;
98
    size_t next_file_column_idx = 0;
99
};
100
101
struct ReadProfile {
102
    RuntimeProfile::Counter* num_delete_files = nullptr;
103
    RuntimeProfile::Counter* num_delete_rows = nullptr;
104
    RuntimeProfile::Counter* parse_delete_file_time = nullptr;
105
    RuntimeProfile::Counter* decoded_dv_cache_hit_count = nullptr;
106
    RuntimeProfile::Counter* decoded_dv_cache_miss_count = nullptr;
107
    RuntimeProfile::Counter* dv_file_cache_hit_count = nullptr;
108
    RuntimeProfile::Counter* dv_file_cache_miss_count = nullptr;
109
    RuntimeProfile::Counter* dv_file_cache_peer_read_count = nullptr;
110
    RuntimeProfile::Counter* exec_timer = nullptr;
111
    RuntimeProfile::Counter* prepare_split_timer = nullptr;
112
    RuntimeProfile::Counter* finalize_timer = nullptr;
113
    RuntimeProfile::Counter* create_reader_timer = nullptr;
114
    RuntimeProfile::Counter* pushdown_agg_timer = nullptr;
115
    RuntimeProfile::Counter* open_reader_timer = nullptr;
116
    RuntimeProfile::Counter* runtime_filter_partition_prune_timer = nullptr;
117
    RuntimeProfile::Counter* runtime_filter_partition_pruned_range_counter = nullptr;
118
};
119
120
struct TableReadOptions {
121
    // Columns need to be read from file and output by table reader. They are all in table/global
122
    // schema semantics.
123
    const std::vector<ColumnDefinition> projected_columns;
124
    // All complex conjuncts from scan operator
125
    const VExprContextSPtrs conjuncts;
126
    // File format of the underlying data files, needed for reader initialization and reader-level
127
    // filter pushdown.
128
    const FileFormat format;
129
    TFileScanRangeParams* scan_params;
130
    std::shared_ptr<io::IOContext> io_ctx;
131
    RuntimeState* runtime_state;
132
    RuntimeProfile* scanner_profile;
133
    // File formats without complete self-describing metadata, such as CSV, Text, and JSON, need
134
    // the FE-planned physical file slots to build their file-local schema and deserialize values.
135
    const std::vector<SlotDescriptor*>* file_slot_descs = nullptr;
136
    // Push-down aggregate type.
137
    const TPushAggOp::type push_down_agg_type = TPushAggOp::type::NONE;
138
    // Digest of stable pushed-down predicates. A zero digest disables condition cache.
139
    uint64_t condition_cache_digest = 0;
140
};
141
142
struct SplitReadOptions {
143
    // Split-level information for reader initialization, which may include file path, partition values, delete file info, etc. The content is table format specific and opaque to table reader base class; it's the responsibility of the concrete table reader implementation to parse necessary information for reader initialization and filter pushdown.
144
    std::map<std::string, Field> partition_values;
145
    // Latest scanner conjuncts rewritten to table/global column indices. Runtime filters may
146
    // arrive after TableReader::init(), so scanner-driven splits replace the initial snapshot.
147
    // nullopt preserves the initial snapshot for standalone TableReader callers.
148
    std::optional<VExprContextSPtrs> conjuncts;
149
    // Independent clones used for partition pruning because evaluation prepares and opens them
150
    // against a synthetic partition block before the file reader opens its row-level conjuncts.
151
    VExprContextSPtrs partition_prune_conjuncts;
152
    // Table-level COUNT may emit one metadata-derived batch and resume on a later scheduler turn.
153
    // It is safe only after every runtime filter assigned to the scanner has arrived; otherwise a
154
    // filter could arrive after synthetic rows have already been returned and those rows cannot be
155
    // retracted. Standalone TableReader callers have no scanner runtime-filter lifecycle.
156
    bool all_runtime_filters_applied = true;
157
    ShardedKVCache* cache = nullptr;
158
    TFileRangeDesc current_range;
159
    FileFormat current_split_format = FileFormat::PARQUET;
160
    std::optional<GlobalRowIdContext> global_rowid_context;
161
};
162
163
// Base class for table-level readers.
164
// This layer owns common table-level orchestration, such as split iteration, dynamic partition
165
// pruning, delete handling and conversion from file-local blocks to table-schema blocks. Concrete
166
// table-format readers only need to provide format-specific hooks for opening readers and parsing
167
// split metadata.
168
class TableReader {
169
public:
170
20.0k
    virtual ~TableReader() = default;
171
172
    // Initialize common runtime options for the table reader. Subclasses may call this from their
173
    // own init(options); table-format schema and split metadata are provided later per split.
174
    virtual Status init(TableReadOptions&& options);
175
176
    // FileScannerV2 adjusts this before each get_block() using an adaptive bytes-per-row estimate.
177
    // Store it here as well as forwarding to the current reader so newly opened split readers start
178
    // with the latest predicted batch size.
179
115k
    virtual void set_batch_size(size_t batch_size) {
180
115k
        _batch_size = std::max<size_t>(1, batch_size);
181
115k
        if (_data_reader.reader != nullptr) {
182
56.9k
            _data_reader.reader->set_batch_size(_batch_size);
183
56.9k
        }
184
115k
    }
185
186
#ifdef BE_TEST
187
    size_t TEST_batch_size() const { return _batch_size; }
188
#endif
189
190
    // Prepare for reading a new split/task.
191
    // 1. Pass a new split/task to reader, which will be used in subsequent open_reader() to initialize the underlying file reader.
192
    // 2. Parse delete predicates from split/task information, which will be used for later dynamic filtering and delete handling.
193
    virtual Status prepare_split(const SplitReadOptions& options);
194
195
43.6k
    virtual bool current_split_pruned() const { return _current_split_pruned; }
196
197
    // Discard the active split after the caller decides an error is ignorable, for example a
198
    // stale external-table file listing that returns NOT_FOUND. The next prepare_split() must start
199
    // with no concrete reader or split-local state left from the failed split.
200
1
    virtual Status abort_split() {
201
1
        if (_data_reader.reader != nullptr) {
202
1
            RETURN_IF_ERROR(close_current_reader());
203
1
        } else {
204
0
            _current_task.reset();
205
0
            _current_file_description.reset();
206
0
        }
207
1
        _delete_rows = nullptr;
208
1
        _remaining_table_level_count = -1;
209
1
        _current_split_pruned = false;
210
1
        return Status::OK();
211
1
    }
212
213
    // Public entry point for reading a table-schema block. The base class opens the current reader,
214
    // advances across EOF, and closes exhausted readers. Subclasses provide protected hooks for
215
    // table-format-specific behavior.
216
88.5k
    virtual Status get_block(Block* block, bool* eos) {
217
88.5k
        SCOPED_TIMER(_profile.exec_timer);
218
88.5k
        DORIS_CHECK(block->columns() == _projected_columns.size());
219
88.5k
        block->clear_column_data(_projected_columns.size());
220
221
122k
        while (true) {
222
122k
            if (*eos) {
223
0
                return Status::OK();
224
0
            }
225
122k
            if (_io_ctx != nullptr && _io_ctx->should_stop) {
226
4
                *eos = true;
227
4
                return Status::OK();
228
4
            }
229
122k
            if (!_data_reader.reader) {
230
60.0k
                if (_is_table_level_count_active()) {
231
152
                    RETURN_IF_ERROR(_read_table_level_count(block, eos));
232
152
                    return Status::OK();
233
152
                }
234
59.9k
                RETURN_IF_ERROR(create_next_reader(eos));
235
59.9k
                if (!_data_reader.reader) {
236
30.1k
                    DCHECK(*eos);
237
30.1k
                    return Status::OK();
238
30.1k
                }
239
59.9k
            }
240
241
            // Materialize a reduced row set for upper aggregate operators when aggregate
242
            // pushdown can be applied. This is not the final aggregate result: COUNT emits
243
            // `count` default rows for the upper COUNT(*), and MIN/MAX emits two rows containing
244
            // file-level min/max values for the upper MIN/MAX.
245
92.6k
            if (!_aggregate_pushdown_tried) {
246
29.8k
                SCOPED_TIMER(_profile.pushdown_agg_timer);
247
29.8k
                bool pushed_down = false;
248
29.8k
                const auto status = _try_materialize_aggregate_pushdown_rows(block, &pushed_down);
249
29.8k
                if (!status.ok()) {
250
1
                    if (_io_ctx != nullptr && _io_ctx->should_stop &&
251
1
                        status.is<ErrorCode::END_OF_FILE>()) {
252
1
                        *eos = true;
253
1
                        return Status::OK();
254
1
                    }
255
0
                    return status;
256
1
                }
257
29.8k
                if (pushed_down) {
258
638
                    return Status::OK();
259
638
                }
260
29.8k
            }
261
262
92.0k
            bool current_eof = false;
263
92.0k
            _data_reader.block_template.clear_column_data(
264
92.0k
                    cast_set<int64_t>(_data_reader.file_block_layout.size()));
265
92.0k
            size_t current_rows = 0;
266
92.0k
            RETURN_IF_ERROR(_data_reader.reader->get_block(&_data_reader.block_template,
267
92.0k
                                                           &current_rows, &current_eof));
268
92.0k
            const bool stopped_during_read = _io_ctx != nullptr && _io_ctx->should_stop;
269
92.0k
            if (current_rows == 0) {
270
34.4k
                if (current_eof) {
271
28.8k
                    _current_reader_reached_eof = !stopped_during_read;
272
28.8k
                    RETURN_IF_ERROR(close_current_reader());
273
28.8k
                }
274
34.4k
                continue;
275
34.4k
            }
276
92.0k
            DCHECK_EQ(_data_reader.block_template.columns(), _data_reader.file_block_layout.size())
277
0
                    << _data_reader.block_template.dump_structure();
278
57.5k
#ifndef NDEBUG
279
57.5k
            RETURN_IF_ERROR(_check_file_block_columns("after file reader get_block", current_rows));
280
57.5k
#endif
281
57.5k
            DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
282
57.5k
            RETURN_IF_ERROR(finalize_chunk(block, current_rows));
283
57.5k
#ifndef NDEBUG
284
57.5k
            RETURN_IF_ERROR(
285
57.5k
                    _check_table_block_columns("after finalize_chunk", block, current_rows));
286
57.5k
#endif
287
57.5k
            if (current_eof) {
288
14
                _current_reader_reached_eof = !stopped_during_read;
289
14
                RETURN_IF_ERROR(close_current_reader());
290
14
            }
291
57.5k
            return Status::OK();
292
57.5k
        }
293
88.5k
    }
294
295
    // Close the table reader and the currently active file reader. Subclasses that hold additional
296
    // table-format resources should override this and call TableReader::close() first.
297
20.0k
    virtual Status close() {
298
20.0k
        if (_data_reader.reader) {
299
342
            RETURN_IF_ERROR(close_current_reader());
300
342
        }
301
20.0k
        _current_task.reset();
302
20.0k
        _current_file_description.reset();
303
20.0k
        _remaining_table_level_count = -1;
304
20.0k
        return Status::OK();
305
20.0k
    }
306
307
39.8k
    int64_t condition_cache_hit_count() const { return _condition_cache_hit_count; }
308
309
    virtual std::string debug_string() const;
310
311
    virtual Status annotate_projected_column(const TFileScanSlotInfo& slot_info,
312
                                             ProjectedColumnBuildContext* context,
313
                                             ColumnDefinition* column) const;
314
315
9.97k
    virtual Status validate_projected_columns(const ProjectedColumnBuildContext& context) const {
316
9.97k
        (void)context;
317
9.97k
        return Status::OK();
318
9.97k
    }
319
320
protected:
321
    std::optional<ColumnDefinition> _find_current_table_column_by_field_id(int32_t field_id,
322
                                                                           DataTypePtr type) const;
323
324
    // Parse deletion vector information from table format specific file description.
325
    virtual Status _parse_deletion_vector_file(const TTableFormatFileDesc& t_desc,
326
17.6k
                                               DeleteFileDesc* desc, bool* has_delete_file) {
327
17.6k
        *has_delete_file = false;
328
17.6k
        return Status::OK();
329
17.6k
    }
330
331
    // Advance to the next reader. This closes the current reader first and then opens the next
332
    // concrete reader. Subclasses should not duplicate this loop.
333
    Status create_next_reader(bool* eos);
334
    virtual Status create_file_reader(std::unique_ptr<FileReader>* reader);
335
3.64k
    virtual TableColumnMappingMode mapping_mode() const { return TableColumnMappingMode::BY_NAME; }
336
16.3k
    virtual Status annotate_file_schema(std::vector<ColumnDefinition>* file_schema) {
337
16.3k
        DORIS_CHECK(file_schema != nullptr);
338
16.3k
        return Status::OK();
339
16.3k
    }
340
341
    // Open the concrete reader for the current split/task and build the file-local scan request.
342
30.3k
    virtual Status open_reader() {
343
30.3k
        SCOPED_TIMER(_profile.open_reader_timer);
344
        // 1. Get file schema and create column mapping.
345
30.3k
        std::vector<ColumnDefinition> file_schema;
346
30.3k
        RETURN_IF_ERROR(_data_reader.reader->get_schema(&file_schema));
347
        // For Paimon/Hudi, FE can provide field ids through `history_schema_info`. Annotate the
348
        // file schema before column mapping when the table format maps columns by field id.
349
30.3k
        RETURN_IF_ERROR(annotate_file_schema(&file_schema));
350
30.3k
        _data_reader.file_schema = file_schema;
351
30.3k
        _mapper_options.mode = mapping_mode();
352
353
30.3k
        _data_reader.column_mapper = _data_reader.reader->create_column_mapper(_mapper_options);
354
30.3k
        DORIS_CHECK(_data_reader.column_mapper != nullptr);
355
30.3k
        RETURN_IF_ERROR(_data_reader.column_mapper->create_mapping(_projected_columns,
356
30.3k
                                                                   _partition_values, file_schema));
357
30.3k
        DORIS_CHECK(_data_reader.column_mapper->mappings().size() == _projected_columns.size());
358
359
        // 2. Build table filters based on conjuncts and column predicates.
360
30.3k
        RETURN_IF_ERROR(_build_table_filters_from_conjuncts());
361
362
        // 3. Create file scan request based on column mapping and table filters, then open file
363
        // reader with the request. File scan request carries row-level expression filters and
364
        // file-level pruning hints. Only expression filters decide returned rows.
365
30.3k
        auto file_request = std::make_shared<FileScanRequest>();
366
30.3k
        RETURN_IF_ERROR(_data_reader.column_mapper->create_scan_request(
367
30.3k
                _table_filters, _projected_columns, file_request.get(), _runtime_state));
368
30.3k
        bool constant_filter_pruned_split = false;
369
30.3k
        RETURN_IF_ERROR(_evaluate_constant_filters(&constant_filter_pruned_split));
370
30.3k
        if (constant_filter_pruned_split) {
371
625
            RETURN_IF_ERROR(close_current_reader());
372
625
            return Status::OK();
373
625
        }
374
29.7k
        RETURN_IF_ERROR(customize_file_scan_request(file_request.get()));
375
29.7k
        RETURN_IF_ERROR(_open_local_filter_exprs(*file_request));
376
29.7k
        _data_reader.file_block_layout.clear();
377
29.7k
        _data_reader.block_template.clear();
378
29.7k
        _data_reader.file_block_layout.resize(file_request->local_positions.size());
379
380
        // 4. Build file block layout from file schema and column mapping. The layout describes
381
        // the block returned by file reader before table-column materialization.
382
194k
        for (const auto& [file_column_id, block_position] : file_request->local_positions) {
383
194k
            DORIS_CHECK(block_position.value() < _data_reader.file_block_layout.size());
384
194k
            const auto* field = _find_column_definition(_data_reader.file_schema, file_column_id);
385
194k
            DORIS_CHECK(field != nullptr);
386
387
194k
            ColumnDefinition projected_field;
388
194k
            {
389
194k
                auto it = std::find_if(
390
194k
                        file_request->non_predicate_columns.begin(),
391
194k
                        file_request->non_predicate_columns.end(),
392
3.99M
                        [&](const LocalColumnIndex& p) { return p.column_id() == file_column_id; });
393
194k
                if (it != file_request->non_predicate_columns.end()) {
394
176k
                    RETURN_IF_ERROR(project_column_definition(*field, *it, &projected_field));
395
176k
                }
396
194k
            }
397
194k
            {
398
194k
                auto it = std::find_if(
399
194k
                        file_request->predicate_columns.begin(),
400
194k
                        file_request->predicate_columns.end(),
401
194k
                        [&](const LocalColumnIndex& p) { return p.column_id() == file_column_id; });
402
194k
                if (it != file_request->predicate_columns.end()) {
403
17.5k
                    RETURN_IF_ERROR(project_column_definition(*field, *it, &projected_field));
404
17.5k
                }
405
194k
            }
406
194k
            _data_reader.file_block_layout[block_position.value()] = {
407
194k
                    .file_column_id = file_column_id,
408
194k
                    .name = projected_field.name,
409
194k
                    .type = projected_field.type,
410
194k
            };
411
194k
            DORIS_CHECK(_data_reader.file_block_layout[block_position.value()].type != nullptr);
412
194k
        }
413
414
        // 5. Prepare block template from file block layout. The block template stores the block
415
        // returned by file reader before table-column materialization.
416
29.7k
        _data_reader.block_template.reserve(_data_reader.file_block_layout.size());
417
194k
        for (const auto& column : _data_reader.file_block_layout) {
418
194k
            _data_reader.block_template.insert(
419
194k
                    {column.type->create_column(), column.type, column.name});
420
194k
        }
421
29.7k
        if (VLOG_DEBUG_IS_ON) {
422
0
            VLOG_DEBUG << "TableReader debug: " << debug_string();
423
0
        }
424
29.7k
        RETURN_IF_ERROR(_open_mapping_exprs());
425
29.7k
        RETURN_IF_ERROR(_data_reader.reader->open(file_request));
426
29.7k
        RETURN_IF_ERROR(_init_reader_condition_cache(*file_request));
427
29.7k
        return Status::OK();
428
29.7k
    }
429
430
    Status _build_table_filters_from_conjuncts();
431
    Status _evaluate_partition_prune_conjuncts(const VExprContextSPtrs& conjuncts,
432
                                               bool* can_filter_all);
433
    static bool _is_safe_to_pre_execute(const VExprContextSPtr& conjunct);
434
    Status _build_partition_prune_block(Block* block) const;
435
    Status _open_local_filter_exprs(const FileScanRequest& file_request);
436
    Status _init_reader_condition_cache(const FileScanRequest& file_request);
437
    void _finalize_reader_condition_cache();
438
    bool _should_enable_condition_cache(const FileScanRequest& file_request) const;
439
440
30.3k
    Status _evaluate_constant_filters(bool* can_filter_all) {
441
30.3k
        DORIS_CHECK(can_filter_all != nullptr);
442
30.3k
        DORIS_CHECK_LE(_constant_pruning_safe_filter_count, _table_filters.size());
443
30.3k
        *can_filter_all = false;
444
        // The bound was derived from the original `_conjuncts` order, which includes slotless
445
        // expressions omitted from `_table_filters`. Iterating only this prefix therefore cannot
446
        // skip an unsafe row-level predicate and pre-execute a later constant predicate.
447
49.7k
        for (size_t i = 0; i < _constant_pruning_safe_filter_count; ++i) {
448
19.9k
            const auto& table_filter = _table_filters[i];
449
19.9k
            if (table_filter.conjunct == nullptr) {
450
0
                continue;
451
0
            }
452
19.9k
            DORIS_CHECK(_is_safe_to_pre_execute(table_filter.conjunct));
453
            // RuntimeFilterExpr does not implement execute_column_impl(); it is evaluated by the
454
            // row-level filter path through execute_filter(). Constant split pruning uses
455
            // VExprContext::execute() on a one-row synthetic block, so runtime filters must not be
456
            // pre-executed here even when their referenced slot maps to a constant value.
457
19.9k
            if (table_filter.conjunct->root()->is_rf_wrapper() ||
458
19.9k
                !_table_filter_has_only_constant_entries(table_filter)) {
459
18.2k
                continue;
460
18.2k
            }
461
1.68k
            Block eval_block;
462
1.68k
            RETURN_IF_ERROR(_build_constant_filter_block(table_filter, &eval_block));
463
1.68k
            RowDescriptor row_desc;
464
1.68k
            RETURN_IF_ERROR(table_filter.conjunct->prepare(_runtime_state, row_desc));
465
1.68k
            RETURN_IF_ERROR(table_filter.conjunct->open(_runtime_state));
466
1.68k
            int result_column_id = -1;
467
1.68k
            RETURN_IF_ERROR(table_filter.conjunct->execute(&eval_block, &result_column_id));
468
1.68k
            DORIS_CHECK(result_column_id >= 0);
469
1.68k
            if (_filter_result_filters_all(eval_block.get_by_position(result_column_id).column)) {
470
625
                *can_filter_all = true;
471
625
                return Status::OK();
472
625
            }
473
1.68k
        }
474
29.7k
        return Status::OK();
475
30.3k
    }
476
477
17.4k
    bool _table_filter_has_only_constant_entries(const TableFilter& table_filter) const {
478
17.4k
        const auto& filter_entries = _data_reader.column_mapper->filter_entries();
479
17.5k
        for (const auto global_index : table_filter.global_indices) {
480
17.5k
            const auto entry_it = filter_entries.find(global_index);
481
17.5k
            if (entry_it == filter_entries.end() || !entry_it->second.is_constant()) {
482
15.7k
                return false;
483
15.7k
            }
484
17.5k
        }
485
1.71k
        return !table_filter.global_indices.empty();
486
17.4k
    }
487
488
1.70k
    Status _build_constant_filter_block(const TableFilter& table_filter, Block* eval_block) {
489
1.70k
        DORIS_CHECK(eval_block != nullptr);
490
1.70k
        eval_block->clear();
491
1.70k
        const auto& mappings = _data_reader.column_mapper->mappings();
492
1.70k
        const auto& filter_entries = _data_reader.column_mapper->filter_entries();
493
1.70k
        DORIS_CHECK(mappings.size() == _projected_columns.size());
494
7.21k
        for (size_t column_idx = 0; column_idx < mappings.size(); ++column_idx) {
495
5.51k
            const auto global_index = GlobalIndex(column_idx);
496
5.51k
            const auto& mapping = mappings[column_idx];
497
5.51k
            const auto entry_it = filter_entries.find(global_index);
498
5.51k
            const bool referenced_by_filter =
499
5.51k
                    std::find(table_filter.global_indices.begin(),
500
5.51k
                              table_filter.global_indices.end(),
501
5.51k
                              global_index) != table_filter.global_indices.end();
502
5.51k
            if (referenced_by_filter && entry_it != filter_entries.end() &&
503
5.51k
                entry_it->second.is_constant()) {
504
1.82k
                ColumnPtr constant_column;
505
1.82k
                RETURN_IF_ERROR(_materialize_constant_filter_column(
506
1.82k
                        entry_it->second.constant_index(), &constant_column));
507
1.82k
                eval_block->insert({std::move(constant_column), mapping.table_type,
508
1.82k
                                    mapping.table_column_name});
509
3.69k
            } else {
510
3.69k
                eval_block->insert({mapping.table_type->create_column_const_with_default_value(1),
511
3.69k
                                    mapping.table_type, mapping.table_column_name});
512
3.69k
            }
513
5.51k
        }
514
1.70k
        return Status::OK();
515
1.70k
    }
516
517
1.82k
    Status _materialize_constant_filter_column(ConstantIndex constant_index, ColumnPtr* column) {
518
1.82k
        DORIS_CHECK(column != nullptr);
519
1.82k
        const auto& constant_entry = _data_reader.column_mapper->constant_map().get(constant_index);
520
1.82k
        DORIS_CHECK(constant_entry.expr != nullptr);
521
1.82k
        DORIS_CHECK(constant_entry.type != nullptr);
522
1.82k
        RowDescriptor row_desc;
523
1.82k
        RETURN_IF_ERROR(constant_entry.expr->prepare(_runtime_state, row_desc));
524
1.82k
        RETURN_IF_ERROR(constant_entry.expr->open(_runtime_state));
525
1.82k
        Block eval_block;
526
1.82k
        eval_block.insert({constant_entry.type->create_column_const_with_default_value(1),
527
1.82k
                           constant_entry.type, "__table_reader_constant_filter"});
528
1.82k
        int result_column_id = -1;
529
1.82k
        RETURN_IF_ERROR(constant_entry.expr->execute(&eval_block, &result_column_id));
530
1.82k
        DORIS_CHECK(result_column_id >= 0);
531
1.82k
        *column = eval_block.get_by_position(result_column_id).column;
532
1.82k
        DORIS_CHECK((*column)->size() == 1);
533
1.82k
        return Status::OK();
534
1.82k
    }
535
536
1.70k
    static bool _filter_result_filters_all(const ColumnPtr& filter_column) {
537
1.70k
        DORIS_CHECK(filter_column.get() != nullptr);
538
1.70k
        DORIS_CHECK(filter_column->size() == 1);
539
1.70k
        return !filter_column->get_bool(0);
540
1.70k
    }
541
542
29.8k
    virtual Status customize_file_scan_request(FileScanRequest* file_request) {
543
29.8k
        return _append_delete_predicate(file_request);
544
29.8k
    }
545
546
103k
    bool _is_table_level_count_active() const { return _remaining_table_level_count >= 0; }
547
548
701
    Status _materialize_count_rows(size_t rows, Block* block) const {
549
701
        DORIS_CHECK(block != nullptr);
550
701
        DORIS_CHECK(block->columns() > 0 || rows == 0);
551
1.40k
        for (size_t column_idx = 0; column_idx < block->columns(); ++column_idx) {
552
701
            auto column = block->get_by_position(column_idx).type->create_column();
553
701
            column->resize(rows);
554
701
            block->replace_by_position(column_idx, std::move(column));
555
701
        }
556
701
        return Status::OK();
557
701
    }
558
559
152
    Status _read_table_level_count(Block* block, bool* eos) {
560
152
        DORIS_CHECK(block != nullptr);
561
152
        DORIS_CHECK(eos != nullptr);
562
152
        DORIS_CHECK(_push_down_agg_type == TPushAggOp::type::COUNT);
563
152
        DORIS_CHECK(_remaining_table_level_count >= 0);
564
152
        if (_remaining_table_level_count == 0) {
565
75
            _remaining_table_level_count = -1;
566
75
            _current_task.reset();
567
75
            *eos = true;
568
75
            return Status::OK();
569
75
        }
570
571
77
        const int64_t batch_size = _runtime_state == nullptr
572
77
                                           ? _remaining_table_level_count
573
77
                                           : static_cast<int64_t>(_runtime_state->batch_size());
574
77
        const auto rows = std::min(_remaining_table_level_count, batch_size);
575
77
        RETURN_IF_ERROR(_materialize_count_rows(cast_set<size_t>(rows), block));
576
77
        _remaining_table_level_count -= rows;
577
77
        *eos = false;
578
77
        return Status::OK();
579
77
    }
580
581
    void _append_file_scan_column(FileScanRequest* request, LocalColumnId column_id,
582
5.08k
                                  std::vector<LocalColumnIndex>* scan_columns) {
583
5.08k
        DORIS_CHECK(request != nullptr);
584
5.08k
        DORIS_CHECK(scan_columns != nullptr);
585
5.08k
        FileScanRequestBuilder builder(request);
586
5.08k
        Status status;
587
5.08k
        if (scan_columns == &request->predicate_columns) {
588
4.80k
            status = builder.add_predicate_column(column_id);
589
4.80k
        } else {
590
285
            DORIS_CHECK(scan_columns == &request->non_predicate_columns);
591
285
            status = builder.add_non_predicate_column(column_id);
592
285
        }
593
5.08k
        DORIS_CHECK(status.ok()) << status.to_string();
594
5.08k
        if (column_id == LocalColumnId(ROW_POSITION_COLUMN_ID) &&
595
5.08k
            _find_column_definition(_data_reader.file_schema, column_id) == nullptr) {
596
2.77k
            _data_reader.file_schema.push_back(row_position_column_definition());
597
2.77k
        }
598
5.08k
    }
599
600
    // Append DeletePredicate to file scan request if there are deletes. The predicate will be evaluated in file reader level and filter out deleted rows before returning data to table reader.
601
29.7k
    Status _append_delete_predicate(FileScanRequest* request) {
602
29.7k
        DORIS_CHECK(request != nullptr);
603
29.7k
        if ((_delete_rows == nullptr || _delete_rows->empty()) &&
604
29.7k
            (_deletion_vector == nullptr || _deletion_vector->isEmpty())) {
605
27.2k
            return Status::OK();
606
27.2k
        }
607
2.50k
        const auto row_position_column_id = LocalColumnId(ROW_POSITION_COLUMN_ID);
608
2.50k
        _append_file_scan_column(request, row_position_column_id, &request->predicate_columns);
609
610
2.50k
        const auto block_position = request->local_positions.at(row_position_column_id);
611
2.56k
        auto append_predicate = [&](auto& deleted_rows) {
612
2.56k
            auto delete_predicate = std::make_shared<DeletePredicate>(deleted_rows);
613
2.56k
            delete_predicate->add_child(VSlotRef::create_shared(
614
2.56k
                    cast_set<int>(block_position.value()), cast_set<int>(block_position.value()),
615
2.56k
                    -1, std::make_shared<DataTypeInt64>(), ROW_POSITION_COLUMN_NAME));
616
2.56k
            request->delete_conjuncts.push_back(
617
2.56k
                    VExprContext::create_shared(std::move(delete_predicate)));
618
2.56k
        };
_ZZN5doris6format11TableReader24_append_delete_predicateEPNS0_15FileScanRequestEENKUlRT_E_clISt6vectorIlSaIlEEEEDaS5_
Line
Count
Source
611
751
        auto append_predicate = [&](auto& deleted_rows) {
612
751
            auto delete_predicate = std::make_shared<DeletePredicate>(deleted_rows);
613
751
            delete_predicate->add_child(VSlotRef::create_shared(
614
751
                    cast_set<int>(block_position.value()), cast_set<int>(block_position.value()),
615
751
                    -1, std::make_shared<DataTypeInt64>(), ROW_POSITION_COLUMN_NAME));
616
751
            request->delete_conjuncts.push_back(
617
751
                    VExprContext::create_shared(std::move(delete_predicate)));
618
751
        };
_ZZN5doris6format11TableReader24_append_delete_predicateEPNS0_15FileScanRequestEENKUlRT_E_clIN7roaring12Roaring64MapEEEDaS5_
Line
Count
Source
611
1.80k
        auto append_predicate = [&](auto& deleted_rows) {
612
1.80k
            auto delete_predicate = std::make_shared<DeletePredicate>(deleted_rows);
613
1.80k
            delete_predicate->add_child(VSlotRef::create_shared(
614
1.80k
                    cast_set<int>(block_position.value()), cast_set<int>(block_position.value()),
615
1.80k
                    -1, std::make_shared<DataTypeInt64>(), ROW_POSITION_COLUMN_NAME));
616
1.80k
            request->delete_conjuncts.push_back(
617
1.80k
                    VExprContext::create_shared(std::move(delete_predicate)));
618
1.80k
        };
619
2.50k
        if (_delete_rows != nullptr && !_delete_rows->empty()) {
620
750
            append_predicate(*_delete_rows);
621
750
        }
622
2.50k
        if (_deletion_vector != nullptr && !_deletion_vector->isEmpty()) {
623
1.81k
            append_predicate(*_deletion_vector);
624
1.81k
        }
625
2.50k
        return Status::OK();
626
29.7k
    }
627
628
    // Close the current concrete reader. This hook is called by both create_next_reader() and
629
    // close(), so it should remain idempotent.
630
30.4k
    virtual Status close_current_reader() {
631
30.4k
        _finalize_reader_condition_cache();
632
30.4k
        RETURN_IF_ERROR(_data_reader.reader->close());
633
30.4k
        _data_reader.reader.reset();
634
30.4k
        if (_data_reader.column_mapper != nullptr) {
635
30.4k
            _data_reader.column_mapper->clear();
636
30.4k
            _data_reader.column_mapper.reset();
637
30.4k
        }
638
30.4k
        _table_filters.clear();
639
30.4k
        _constant_pruning_safe_filter_count = 0;
640
30.4k
        _data_reader.file_schema.clear();
641
30.4k
        _data_reader.file_block_layout.clear();
642
30.4k
        _data_reader.block_template.clear();
643
30.4k
        _current_task.reset();
644
30.4k
        _current_file_description.reset();
645
30.4k
        _current_reader_reached_eof = false;
646
30.4k
        return Status::OK();
647
30.4k
    }
648
649
0
    void _record_scan_rows(size_t rows) {
650
0
        if (_io_ctx != nullptr && _io_ctx->file_reader_stats != nullptr) {
651
0
            _io_ctx->file_reader_stats->read_rows += rows;
652
0
        }
653
0
    }
654
655
    // Finalize file-local block to table/global schema block.
656
57.6k
    Status finalize_chunk(Block* block, const size_t rows) {
657
57.6k
        SCOPED_TIMER(_profile.finalize_timer);
658
57.6k
        size_t idx = 0;
659
320k
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
660
320k
            ColumnPtr column;
661
320k
            RETURN_IF_ERROR(_materialize_mapping_column(mapping, &_data_reader.block_template, rows,
662
320k
                                                        &column));
663
320k
            block->replace_by_position(idx, IColumn::mutate(std::move(column)));
664
320k
            idx++;
665
320k
        }
666
57.6k
        RETURN_IF_ERROR(materialize_virtual_columns(block));
667
        // Enforce CHAR/VARCHAR length declared by the table schema after all file-to-table
668
        // materialization has finished.
669
57.6k
        RETURN_IF_ERROR(_truncate_char_or_varchar_columns(block));
670
57.6k
        return Status::OK();
671
57.6k
    }
672
673
    // Materialize virtual columns in the table block, such as Iceberg _row_id and
674
    // _last_updated_sequence_number. This runs after normal column materialization so finalize
675
    // expressions can reference those virtual columns.
676
45.4k
    virtual Status materialize_virtual_columns(Block* table_block) { return Status::OK(); }
677
678
#ifndef NDEBUG
679
57.6k
    Status _check_file_block_columns(std::string_view stage, size_t rows) {
680
57.6k
        DORIS_CHECK(_data_reader.block_template.columns() == _data_reader.file_block_layout.size());
681
372k
        for (size_t idx = 0; idx < _data_reader.block_template.columns(); ++idx) {
682
315k
            const auto& file_block_column = _data_reader.file_block_layout[idx];
683
315k
            const auto& column_with_type = _data_reader.block_template.get_by_position(idx);
684
315k
            const auto* column = column_with_type.column.get();
685
315k
            try {
686
315k
                if (column == nullptr) {
687
0
                    auto st = Status::InternalError(
688
0
                            "Invalid file block column {} at {}: file_column_id={}, name='{}', "
689
0
                            "type={}, column=null, expected_rows={}, reader={}",
690
0
                            idx, stage, file_block_column.file_column_id.value(),
691
0
                            file_block_column.name,
692
0
                            file_block_column.type == nullptr ? "null"
693
0
                                                              : file_block_column.type->get_name(),
694
0
                            rows, debug_string());
695
0
                    LOG(WARNING) << st;
696
0
                    return st;
697
0
                }
698
315k
                column->sanity_check();
699
315k
                auto st = column_with_type.check_type_and_column_match();
700
315k
                if (!st.ok()) {
701
0
                    auto contextual_status = Status::InternalError(
702
0
                            "Invalid file block column {} at {}: file_column_id={}, name='{}', "
703
0
                            "type={}, column={}, column_size={}, expected_rows={}, error={}, "
704
0
                            "reader={}",
705
0
                            idx, stage, file_block_column.file_column_id.value(),
706
0
                            file_block_column.name,
707
0
                            file_block_column.type == nullptr ? "null"
708
0
                                                              : file_block_column.type->get_name(),
709
0
                            column->get_name(), column->size(), rows, st.to_string(),
710
0
                            debug_string());
711
0
                    LOG(WARNING) << contextual_status;
712
0
                    return contextual_status;
713
0
                }
714
315k
            } catch (const Exception& e) {
715
0
                auto st = Status::InternalError(
716
0
                        "Invalid file block column {} at {}: file_column_id={}, name='{}', "
717
0
                        "type={}, column={}, column_size={}, expected_rows={}, error={}, "
718
0
                        "reader={}",
719
0
                        idx, stage, file_block_column.file_column_id.value(),
720
0
                        file_block_column.name,
721
0
                        file_block_column.type == nullptr ? "null"
722
0
                                                          : file_block_column.type->get_name(),
723
0
                        column == nullptr ? "null" : column->get_name(),
724
0
                        column == nullptr ? 0 : column->size(), rows, e.to_string(),
725
0
                        debug_string());
726
0
                LOG(WARNING) << st;
727
0
                return st;
728
0
            } catch (const std::exception& e) {
729
0
                auto st = Status::InternalError(
730
0
                        "Invalid file block column {} at {}: file_column_id={}, name='{}', "
731
0
                        "type={}, column={}, column_size={}, expected_rows={}, error={}, "
732
0
                        "reader={}",
733
0
                        idx, stage, file_block_column.file_column_id.value(),
734
0
                        file_block_column.name,
735
0
                        file_block_column.type == nullptr ? "null"
736
0
                                                          : file_block_column.type->get_name(),
737
0
                        column == nullptr ? "null" : column->get_name(),
738
0
                        column == nullptr ? 0 : column->size(), rows, e.what(), debug_string());
739
0
                LOG(WARNING) << st;
740
0
                return st;
741
0
            }
742
315k
        }
743
57.6k
        return Status::OK();
744
57.6k
    }
745
746
57.6k
    Status _check_table_block_columns(std::string_view stage, const Block* block, size_t rows) {
747
57.6k
        DORIS_CHECK(block != nullptr);
748
57.6k
        DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
749
378k
        for (size_t idx = 0; idx < block->columns(); ++idx) {
750
320k
            const auto& mapping = _data_reader.column_mapper->mappings()[idx];
751
320k
            const auto& column_with_type = block->get_by_position(idx);
752
320k
            const auto* column = column_with_type.column.get();
753
320k
            try {
754
320k
                if (column == nullptr) {
755
0
                    auto st = Status::InternalError(
756
0
                            "Invalid table block column {} at {}: table_column='{}', "
757
0
                            "global_index={}, type={}, column=null, expected_rows={}, mapping={}",
758
0
                            idx, stage, mapping.table_column_name, mapping.global_index.value(),
759
0
                            mapping.table_type == nullptr ? "null" : mapping.table_type->get_name(),
760
0
                            rows, mapping.debug_string());
761
0
                    LOG(WARNING) << st;
762
0
                    return st;
763
0
                }
764
320k
                column->sanity_check();
765
320k
                auto st = column_with_type.check_type_and_column_match();
766
320k
                if (!st.ok()) {
767
0
                    auto contextual_status = Status::InternalError(
768
0
                            "Invalid table block column {} at {}: table_column='{}', "
769
0
                            "global_index={}, type={}, column={}, column_size={}, "
770
0
                            "expected_rows={}, error={}, mapping={}",
771
0
                            idx, stage, mapping.table_column_name, mapping.global_index.value(),
772
0
                            mapping.table_type == nullptr ? "null" : mapping.table_type->get_name(),
773
0
                            column->get_name(), column->size(), rows, st.to_string(),
774
0
                            mapping.debug_string());
775
0
                    LOG(WARNING) << contextual_status;
776
0
                    return contextual_status;
777
0
                }
778
320k
            } catch (const Exception& e) {
779
0
                auto st = Status::InternalError(
780
0
                        "Invalid table block column {} at {}: table_column='{}', global_index={}, "
781
0
                        "type={}, column={}, column_size={}, expected_rows={}, error={}, "
782
0
                        "mapping={}",
783
0
                        idx, stage, mapping.table_column_name, mapping.global_index.value(),
784
0
                        mapping.table_type == nullptr ? "null" : mapping.table_type->get_name(),
785
0
                        column == nullptr ? "null" : column->get_name(),
786
0
                        column == nullptr ? 0 : column->size(), rows, e.to_string(),
787
0
                        mapping.debug_string());
788
0
                LOG(WARNING) << st;
789
0
                return st;
790
0
            } catch (const std::exception& e) {
791
0
                auto st = Status::InternalError(
792
0
                        "Invalid table block column {} at {}: table_column='{}', global_index={}, "
793
0
                        "type={}, column={}, column_size={}, expected_rows={}, error={}, "
794
0
                        "mapping={}",
795
0
                        idx, stage, mapping.table_column_name, mapping.global_index.value(),
796
0
                        mapping.table_type == nullptr ? "null" : mapping.table_type->get_name(),
797
0
                        column == nullptr ? "null" : column->get_name(),
798
0
                        column == nullptr ? 0 : column->size(), rows, e.what(),
799
0
                        mapping.debug_string());
800
0
                LOG(WARNING) << st;
801
0
                return st;
802
0
            }
803
320k
        }
804
57.6k
        return Status::OK();
805
57.6k
    }
806
#endif
807
808
57.6k
    Status _truncate_char_or_varchar_columns(Block* block) {
809
57.6k
        DORIS_CHECK(block != nullptr);
810
57.6k
        if (_runtime_state == nullptr ||
811
57.6k
            !_runtime_state->query_options().truncate_char_or_varchar_columns) {
812
57.6k
            return Status::OK();
813
57.6k
        }
814
11
        DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
815
29
        for (size_t idx = 0; idx < _data_reader.column_mapper->mappings().size(); ++idx) {
816
18
            const auto& mapping = _data_reader.column_mapper->mappings()[idx];
817
18
            if (!_should_truncate_char_or_varchar_column(mapping)) {
818
6
                continue;
819
6
            }
820
12
            const auto target_len =
821
12
                    assert_cast<const DataTypeString*>(remove_nullable(mapping.table_type).get())
822
12
                            ->len();
823
12
            _truncate_char_or_varchar_column(block, idx, target_len);
824
12
        }
825
11
        return Status::OK();
826
57.6k
    }
827
828
    // Return true when the table schema has a bounded CHAR/VARCHAR length that is stricter than
829
    // the file-side type. Examples:
830
    // - table VARCHAR(10), file VARCHAR(20): truncate to 10;
831
    // - table VARCHAR(10), file STRING: truncate to 10 because STRING has no declared bound;
832
    // - table STRING, any file type: no truncation because the target has no bound.
833
23
    static bool _should_truncate_char_or_varchar_column(const ColumnMapping& mapping) {
834
23
        if (mapping.table_type == nullptr) {
835
0
            return false;
836
0
        }
837
23
        const auto table_type = remove_nullable(mapping.table_type);
838
23
        const auto primitive_type = table_type->get_primitive_type();
839
23
        if (primitive_type != TYPE_VARCHAR && primitive_type != TYPE_CHAR) {
840
7
            return false;
841
7
        }
842
16
        const auto target_len = assert_cast<const DataTypeString*>(table_type.get())->len();
843
16
        if (target_len <= 0) {
844
0
            return false;
845
0
        }
846
16
        if (mapping.file_type == nullptr) {
847
0
            return true;
848
0
        }
849
16
        const auto file_type = remove_nullable(mapping.file_type);
850
16
        DORIS_CHECK(file_type != nullptr);
851
16
        int file_len = -1;
852
16
        if (file_type->get_primitive_type() == TYPE_VARCHAR ||
853
16
            file_type->get_primitive_type() == TYPE_CHAR ||
854
16
            file_type->get_primitive_type() == TYPE_STRING) {
855
15
            file_len = assert_cast<const DataTypeString*>(file_type.get())->len();
856
15
        }
857
858
16
        return file_len < 0 || target_len < file_len;
859
16
    }
860
861
    // Truncate a materialized CHAR/VARCHAR column in place by reusing the vectorized substring
862
    // implementation: substring(column, 1, len). Nullable columns are unwrapped before substring
863
    // execution and wrapped back with the original null map afterward, because substring operates
864
    // on the nested string payload only.
865
13
    static void _truncate_char_or_varchar_column(Block* block, size_t idx, int len) {
866
13
        DORIS_CHECK(block != nullptr);
867
13
        auto int_type = std::make_shared<DataTypeInt32>();
868
13
        const auto num_columns_without_result = cast_set<uint32_t>(block->columns());
869
13
        auto& target = block->get_by_position(idx);
870
13
        const bool is_nullable = target.type->is_nullable();
871
13
        ColumnPtr input_column = target.column;
872
13
        ColumnPtr null_map_column;
873
13
        if (is_nullable) {
874
13
            const auto* nullable_column = assert_cast<const ColumnNullable*>(target.column.get());
875
13
            input_column = nullable_column->get_nested_column_ptr();
876
13
            null_map_column = nullable_column->get_null_map_column_ptr();
877
13
        }
878
13
        block->replace_by_position(idx, std::move(input_column));
879
13
        block->insert({int_type->create_column_const(block->rows(), to_field<TYPE_INT>(1)),
880
13
                       int_type, "const 1"});
881
13
        block->insert({int_type->create_column_const(block->rows(), to_field<TYPE_INT>(len)),
882
13
                       int_type, "const len"});
883
13
        block->insert({nullptr, std::make_shared<DataTypeString>(), "result"});
884
885
13
        ColumnNumbers temp_arguments(3);
886
13
        temp_arguments[0] = cast_set<uint32_t>(idx);
887
13
        temp_arguments[1] = num_columns_without_result;
888
13
        temp_arguments[2] = num_columns_without_result + 1;
889
13
        const uint32_t result_column_id = num_columns_without_result + 2;
890
13
        SubstringUtil::substring_execute(*block, temp_arguments, result_column_id, block->rows());
891
892
13
        ColumnPtr result_column = block->get_by_position(result_column_id).column;
893
13
        if (is_nullable) {
894
13
            result_column = ColumnNullable::create(std::move(result_column), null_map_column);
895
13
        }
896
13
        block->replace_by_position(idx, std::move(result_column));
897
13
        block->erase_tail(num_columns_without_result);
898
13
    }
899
900
29.7k
    Status _try_materialize_aggregate_pushdown_rows(Block* block, bool* pushed_down) {
901
29.7k
        DORIS_CHECK(block != nullptr);
902
29.7k
        DORIS_CHECK(pushed_down != nullptr);
903
29.7k
        *pushed_down = false;
904
29.7k
        block->clear_column_data(_projected_columns.size());
905
29.7k
        _aggregate_pushdown_tried = true;
906
29.7k
        if (!_supports_aggregate_pushdown(_push_down_agg_type)) {
907
29.0k
            return Status::OK();
908
29.0k
        }
909
910
641
        FileAggregateRequest file_request;
911
641
        RETURN_IF_ERROR(_build_file_aggregate_request(_push_down_agg_type, &file_request));
912
641
        FileAggregateResult file_result;
913
641
        const auto status = _data_reader.reader->get_aggregate_result(file_request, &file_result);
914
641
        if (status.is<ErrorCode::NOT_IMPLEMENTED_ERROR>()) {
915
3
            return Status::OK();
916
3
        }
917
638
        RETURN_IF_ERROR(status);
918
637
        RETURN_IF_ERROR(
919
637
                _materialize_aggregate_pushdown_rows(_push_down_agg_type, file_result, block));
920
637
        *pushed_down = true;
921
637
        RETURN_IF_ERROR(close_current_reader());
922
637
        return Status::OK();
923
637
    }
924
925
30.4k
    virtual bool _supports_aggregate_pushdown(TPushAggOp::type agg_type) const {
926
        // Only COUNT and MIN/MAX can be push down.
927
30.4k
        if (agg_type != TPushAggOp::type::COUNT && agg_type != TPushAggOp::type::MINMAX) {
928
28.6k
            return false;
929
28.6k
        }
930
        // Aggregate pushdown returns reduced synthetic rows and may close the physical reader
931
        // before the next scheduler turn. If a runtime filter is still pending, those rows could
932
        // escape before the filter arrives and cannot later be reconstructed from real file rows.
933
        // This is the same irreversibility constraint as table-level metadata COUNT, and applies
934
        // to COUNT and MIN/MAX for Parquet/ORC as well as COUNT for text readers.
935
1.82k
        if (!_all_runtime_filters_applied_for_split) {
936
2
            return false;
937
2
        }
938
        // Scanner owns the original conjunct list and evaluates it after TableReader finalizes
939
        // rows. Even a slotless conjunct that cannot become a TableFilter must see every source
940
        // row before an aggregate reduces the stream to synthetic COUNT/MINMAX rows.
941
1.82k
        if (!_conjuncts.empty()) {
942
5
            return false;
943
5
        }
944
        // Only support aggregate pushdown when there is no delete or filter, so
945
        // the reduced rows consumed by the upper aggregate remain semantically equivalent to a
946
        // normal scan.
947
1.81k
        if ((_delete_rows != nullptr && !_delete_rows->empty()) ||
948
1.81k
            (_deletion_vector != nullptr && !_deletion_vector->isEmpty())) {
949
300
            return false;
950
300
        }
951
1.51k
        if (!_table_filters.empty()) {
952
0
            return false;
953
0
        }
954
1.51k
        if (agg_type == TPushAggOp::type::COUNT) {
955
1.32k
            return true;
956
1.32k
        }
957
        // For MIN/MAX, only support direct file-to-table column mappings. The two emitted rows
958
        // must be enough for the upper MIN/MAX aggregate without evaluating default expressions or
959
        // virtual columns.
960
197
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
961
168
            if (!mapping.file_local_id.has_value() ||
962
168
                mapping.virtual_column_type != TableVirtualColumnType::INVALID ||
963
168
                mapping.default_expr != nullptr || mapping.file_type == nullptr ||
964
168
                mapping.table_type == nullptr) {
965
5
                return false;
966
5
            }
967
163
            if (!_can_push_down_minmax_for_mapping(mapping)) {
968
112
                return false;
969
112
            }
970
163
        }
971
80
        return true;
972
197
    }
973
974
315k
    static ColumnPtr _detach_column(ColumnPtr column) {
975
315k
        DORIS_CHECK(column.get() != nullptr);
976
315k
        return IColumn::mutate(std::move(column));
977
315k
    }
978
979
47.2k
    static Status _align_column_nullability(ColumnPtr* column, const DataTypePtr& table_type) {
980
47.2k
        DORIS_CHECK(column != nullptr);
981
47.2k
        DORIS_CHECK(column->get() != nullptr);
982
47.2k
        DORIS_CHECK(table_type != nullptr);
983
        // Must return non-const column
984
47.2k
        *column = (*column)->convert_to_full_column_if_const();
985
47.2k
        if (table_type->is_nullable()) {
986
23.5k
            const auto& nested_type =
987
23.5k
                    assert_cast<const DataTypeNullable&>(*table_type).get_nested_type();
988
23.5k
            if (!(*column)->is_nullable()) {
989
2
                RETURN_IF_ERROR(_align_column_nullability(column, nested_type));
990
2
                *column = make_nullable(*column);
991
2
                return Status::OK();
992
2
            }
993
23.5k
            const auto& nullable_column = assert_cast<const ColumnNullable&>(**column);
994
23.5k
            ColumnPtr nested_column = nullable_column.get_nested_column_ptr();
995
23.5k
            RETURN_IF_ERROR(_align_column_nullability(&nested_column, nested_type));
996
23.5k
            *column = ColumnNullable::create(nested_column,
997
23.5k
                                             nullable_column.get_null_map_column_ptr());
998
23.5k
            return Status::OK();
999
23.5k
        }
1000
23.6k
        if ((*column)->is_nullable()) {
1001
0
            const auto& nullable_column = assert_cast<const ColumnNullable&>(**column);
1002
0
            if (nullable_column.has_null()) {
1003
0
                return Status::InternalError(
1004
0
                        "Default expression produced NULL for non-nullable table column");
1005
0
            }
1006
0
            ColumnPtr nested_column = nullable_column.get_nested_column_ptr();
1007
0
            RETURN_IF_ERROR(_align_column_nullability(&nested_column, table_type));
1008
0
            *column = nested_column;
1009
0
            return Status::OK();
1010
0
        }
1011
23.6k
        if (const auto* array_type = typeid_cast<const DataTypeArray*>(table_type.get())) {
1012
65
            const auto& array_column = assert_cast<const ColumnArray&>(**column);
1013
65
            ColumnPtr nested_column = array_column.get_data_ptr();
1014
65
            RETURN_IF_ERROR(
1015
65
                    _align_column_nullability(&nested_column, array_type->get_nested_type()));
1016
65
            *column = ColumnArray::create(nested_column, array_column.get_offsets_ptr());
1017
65
            return Status::OK();
1018
65
        }
1019
23.5k
        if (const auto* map_type = typeid_cast<const DataTypeMap*>(table_type.get())) {
1020
4
            const auto& map_column = assert_cast<const ColumnMap&>(**column);
1021
4
            ColumnPtr key_column = map_column.get_keys_ptr();
1022
4
            ColumnPtr value_column = map_column.get_values_ptr();
1023
4
            RETURN_IF_ERROR(_align_column_nullability(&key_column, map_type->get_key_type()));
1024
4
            RETURN_IF_ERROR(_align_column_nullability(&value_column, map_type->get_value_type()));
1025
4
            *column = ColumnMap::create(key_column, value_column, map_column.get_offsets_ptr());
1026
4
            return Status::OK();
1027
4
        }
1028
23.5k
        if (const auto* struct_type = typeid_cast<const DataTypeStruct*>(table_type.get())) {
1029
2.81k
            const auto& struct_column = assert_cast<const ColumnStruct&>(**column);
1030
2.81k
            Columns columns = struct_column.get_columns_copy();
1031
2.81k
            DORIS_CHECK(columns.size() == struct_type->get_elements().size());
1032
9.52k
            for (size_t i = 0; i < columns.size(); ++i) {
1033
6.70k
                RETURN_IF_ERROR(
1034
6.70k
                        _align_column_nullability(&columns[i], struct_type->get_element(i)));
1035
6.70k
            }
1036
2.81k
            *column = ColumnStruct::create(columns);
1037
2.81k
            return Status::OK();
1038
2.81k
        }
1039
20.7k
        return Status::OK();
1040
23.5k
    }
1041
1042
    static Status _execute_default_expr_without_root_type_check(
1043
            const VExprContextSPtr& default_expr, const Block* block,
1044
8.26k
            ColumnWithTypeAndName* result_data) {
1045
8.26k
        DORIS_CHECK(default_expr != nullptr);
1046
8.26k
        DORIS_CHECK(block != nullptr);
1047
8.26k
        DORIS_CHECK(result_data != nullptr);
1048
8.26k
        ColumnPtr result_column;
1049
8.26k
        Status st;
1050
8.26k
        RETURN_IF_CATCH_EXCEPTION({
1051
8.26k
            st = default_expr->root()->execute_column_impl(default_expr.get(), block, nullptr,
1052
8.26k
                                                           block->rows(), result_column);
1053
8.26k
        });
1054
8.26k
        RETURN_IF_ERROR(st);
1055
8.26k
        DORIS_CHECK(result_column.get() != nullptr);
1056
8.26k
        if (result_column->size() != block->rows()) {
1057
0
            return Status::InternalError(
1058
0
                    "Default expr {} return column size {} not equal to expected size {}",
1059
0
                    default_expr->expr_name(), result_column->size(), block->rows());
1060
0
        }
1061
8.26k
        result_data->column = result_column;
1062
8.26k
        result_data->type = default_expr->execute_type(block);
1063
8.26k
        result_data->name = default_expr->expr_name();
1064
8.26k
        return Status::OK();
1065
8.26k
    }
1066
1067
    Status _cast_column_to_type(ColumnPtr* column, const DataTypePtr& file_type,
1068
                                const DataTypePtr& table_type,
1069
4.38k
                                const std::string& column_name) const {
1070
4.38k
        DORIS_CHECK(column != nullptr);
1071
4.38k
        DORIS_CHECK(column->get() != nullptr);
1072
4.38k
        DORIS_CHECK(file_type != nullptr);
1073
4.38k
        DORIS_CHECK(table_type != nullptr);
1074
4.38k
        if (file_type->equals(*table_type)) {
1075
0
            return Status::OK();
1076
0
        }
1077
1078
4.38k
        DataTypePtr input_type = file_type;
1079
        // Cast wrappers unwrap nullable inputs according to the declared input type, so keep the
1080
        // root nullability of the declared type aligned with the actual column shape.
1081
4.38k
        if ((*column)->is_nullable() && !input_type->is_nullable()) {
1082
0
            input_type = make_nullable(input_type);
1083
4.38k
        } else if (!(*column)->is_nullable() && input_type->is_nullable()) {
1084
1
            input_type = remove_nullable(input_type);
1085
1
        }
1086
4.38k
        Block cast_block;
1087
4.38k
        cast_block.insert({*column, input_type, column_name});
1088
4.38k
        auto slot_ref = VSlotRef::create_shared(0, 0, -1, input_type, column_name);
1089
4.38k
        auto cast_expr = Cast::create_shared(table_type);
1090
4.38k
        cast_expr->add_child(std::move(slot_ref));
1091
4.38k
        auto cast_ctx = VExprContext::create_shared(std::move(cast_expr));
1092
4.38k
        RowDescriptor row_desc;
1093
4.38k
        RETURN_IF_ERROR(cast_ctx->prepare(_runtime_state, row_desc));
1094
4.38k
        RETURN_IF_ERROR(cast_ctx->open(_runtime_state));
1095
4.38k
        ColumnPtr cast_column;
1096
4.38k
        RETURN_IF_ERROR(cast_ctx->execute(&cast_block, cast_column));
1097
4.38k
        *column = std::move(cast_column);
1098
4.38k
        return Status::OK();
1099
4.38k
    }
1100
1101
    Status _materialize_present_child_mapping_column(const ColumnMapping& mapping,
1102
                                                     const ColumnPtr& file_column,
1103
8.58k
                                                     const size_t rows, ColumnPtr* column) {
1104
8.58k
        DORIS_CHECK(column != nullptr);
1105
8.58k
        DORIS_CHECK(mapping.file_type != nullptr);
1106
8.58k
        DORIS_CHECK(mapping.table_type != nullptr);
1107
8.58k
        *column = file_column;
1108
8.58k
        if (!mapping.is_trivial) {
1109
5.38k
            if (!mapping.child_mappings.empty()) {
1110
999
                RETURN_IF_ERROR(
1111
999
                        _materialize_complex_mapping_column(mapping, *column, rows, column));
1112
4.38k
            } else {
1113
4.38k
                RETURN_IF_ERROR(_cast_column_to_type(column, mapping.file_type, mapping.table_type,
1114
4.38k
                                                     mapping.file_column_name));
1115
4.38k
            }
1116
5.38k
        }
1117
8.58k
        RETURN_IF_ERROR(_align_column_nullability(column, mapping.table_type));
1118
8.58k
        return Status::OK();
1119
8.58k
    }
1120
1121
    Status _materialize_mapping_column(const ColumnMapping& mapping, Block* current_block,
1122
320k
                                       const size_t rows, ColumnPtr* column) {
1123
320k
        if (!mapping.is_trivial && mapping.file_local_id.has_value() &&
1124
320k
            !mapping.child_mappings.empty()) {
1125
4.82k
            DCHECK(mapping.projection != nullptr);
1126
4.82k
            int res_id;
1127
4.82k
            auto st = mapping.projection->execute(current_block, &res_id);
1128
4.82k
            if (!st.ok()) {
1129
0
                return Status::InternalError(
1130
0
                        "Failed to execute complex mapping projection for table column '{}' "
1131
0
                        "(global_index={}, file_local_id={}, rows={}): {}, mapping={}",
1132
0
                        mapping.table_column_name, mapping.global_index.value(),
1133
0
                        *mapping.file_local_id, rows, st.to_string(), mapping.debug_string());
1134
0
            }
1135
4.82k
            ColumnPtr result_column = current_block->get_by_position(res_id).column;
1136
4.82k
            RETURN_IF_ERROR(
1137
4.82k
                    _materialize_complex_mapping_column(mapping, result_column, rows, column));
1138
4.82k
            return Status::OK();
1139
4.82k
        }
1140
315k
        if (mapping.projection != nullptr) {
1141
307k
            int res_id;
1142
307k
            auto st = mapping.projection->execute(current_block, &res_id);
1143
307k
            if (!st.ok()) {
1144
0
                std::string file_local_id = "null";
1145
0
                if (mapping.file_local_id.has_value()) {
1146
0
                    file_local_id = std::to_string(*mapping.file_local_id);
1147
0
                }
1148
0
                return Status::InternalError(
1149
0
                        "Failed to execute mapping projection for table column '{}' "
1150
0
                        "(global_index={}, file_local_id={}, rows={}): {}, mapping={}",
1151
0
                        mapping.table_column_name, mapping.global_index.value(), file_local_id,
1152
0
                        rows, st.to_string(), mapping.debug_string());
1153
0
            }
1154
307k
            ColumnPtr result_column = current_block->get_by_position(res_id).column;
1155
307k
            *column = _detach_column(std::move(result_column));
1156
307k
            return Status::OK();
1157
307k
        }
1158
8.61k
        if (mapping.default_expr != nullptr) {
1159
8.25k
            if (current_block->rows() == rows) {
1160
7.25k
                ColumnWithTypeAndName result;
1161
7.25k
                RETURN_IF_ERROR(_execute_default_expr_without_root_type_check(
1162
7.25k
                        mapping.default_expr, current_block, &result));
1163
7.25k
                ColumnPtr result_column = result.column;
1164
7.25k
                RETURN_IF_ERROR(_align_column_nullability(&result_column, mapping.table_type));
1165
7.25k
                *column = _detach_column(std::move(result_column));
1166
7.25k
            } else {
1167
1.00k
                DORIS_CHECK(mapping.constant_index.has_value());
1168
1.00k
                Block eval_block;
1169
1.00k
                eval_block.insert({mapping.table_type->create_column_const_with_default_value(rows),
1170
1.00k
                                   mapping.table_type, "__table_reader_const_rows"});
1171
1.00k
                ColumnWithTypeAndName result;
1172
1.00k
                RETURN_IF_ERROR(_execute_default_expr_without_root_type_check(
1173
1.00k
                        mapping.default_expr, &eval_block, &result));
1174
1.00k
                ColumnPtr result_column = result.column;
1175
1.00k
                RETURN_IF_ERROR(_align_column_nullability(&result_column, mapping.table_type));
1176
1.00k
                *column = _detach_column(std::move(result_column));
1177
1.00k
            }
1178
8.25k
            return Status::OK();
1179
8.25k
        }
1180
356
        ColumnPtr result_column = mapping.table_type->create_column_const_with_default_value(rows);
1181
356
        *column = _detach_column(std::move(result_column));
1182
356
        return Status::OK();
1183
8.61k
    }
1184
1185
    Status _materialize_complex_mapping_column(const ColumnMapping& mapping,
1186
                                               const ColumnPtr& file_column, const size_t rows,
1187
5.81k
                                               ColumnPtr* column) {
1188
5.81k
        DORIS_CHECK(mapping.table_type != nullptr);
1189
5.81k
        DORIS_CHECK(file_column.get() != nullptr);
1190
5.81k
        const auto table_type = remove_nullable(mapping.table_type);
1191
5.81k
        switch (table_type->get_primitive_type()) {
1192
1.81k
        case TYPE_STRUCT:
1193
1.81k
            RETURN_IF_ERROR(_materialize_struct_mapping_column(mapping, file_column, rows, column));
1194
1.81k
            break;
1195
2.22k
        case TYPE_ARRAY:
1196
2.22k
            RETURN_IF_ERROR(_materialize_array_mapping_column(mapping, file_column, rows, column));
1197
2.22k
            break;
1198
2.22k
        case TYPE_MAP:
1199
1.78k
            RETURN_IF_ERROR(_materialize_map_mapping_column(mapping, file_column, rows, column));
1200
1.78k
            break;
1201
1.78k
        default:
1202
0
            *column = _detach_column(file_column);
1203
0
            break;
1204
5.81k
        }
1205
5.81k
        return Status::OK();
1206
5.81k
    }
1207
1208
    static std::vector<const ColumnMapping*> _present_child_mappings_in_file_order(
1209
1.81k
            const std::vector<ColumnMapping>& child_mappings) {
1210
1.81k
        std::vector<const ColumnMapping*> result;
1211
1.81k
        result.reserve(child_mappings.size());
1212
4.60k
        for (const auto& child_mapping : child_mappings) {
1213
4.60k
            if (child_mapping.file_local_id.has_value()) {
1214
2.79k
                result.push_back(&child_mapping);
1215
2.79k
            }
1216
4.60k
        }
1217
2.02k
        std::ranges::sort(result, [](const ColumnMapping* lhs, const ColumnMapping* rhs) {
1218
2.02k
            DORIS_CHECK(lhs->file_local_id.has_value());
1219
2.02k
            DORIS_CHECK(rhs->file_local_id.has_value());
1220
2.02k
            return *lhs->file_local_id < *rhs->file_local_id;
1221
2.02k
        });
1222
1.81k
        return result;
1223
1.81k
    }
1224
1225
    static size_t _file_child_ordinal_for_mapping(
1226
            const ColumnMapping& mapping, const ColumnMapping& child_mapping,
1227
2.79k
            const std::vector<const ColumnMapping*>& file_ordered_children) {
1228
2.79k
        DORIS_CHECK(child_mapping.file_local_id.has_value());
1229
2.79k
        if (!mapping.projected_file_children.empty()) {
1230
2.79k
            const auto child_it = std::ranges::find_if(
1231
4.29k
                    mapping.projected_file_children, [&](const ColumnDefinition& file_child) {
1232
4.29k
                        return file_child.file_local_id() == *child_mapping.file_local_id;
1233
4.29k
                    });
1234
2.79k
            DORIS_CHECK(child_it != mapping.projected_file_children.end());
1235
2.79k
            return static_cast<size_t>(
1236
2.79k
                    std::distance(mapping.projected_file_children.begin(), child_it));
1237
2.79k
        }
1238
4
        const auto child_it = std::ranges::find(file_ordered_children, &child_mapping);
1239
4
        DORIS_CHECK(child_it != file_ordered_children.end());
1240
4
        return static_cast<size_t>(std::distance(file_ordered_children.begin(), child_it));
1241
2.79k
    }
1242
1243
    static std::vector<const ColumnMapping*> _child_mappings_in_table_type_order(
1244
1.81k
            const ColumnMapping& mapping, const DataTypeStruct& table_type) {
1245
1.81k
        std::vector<const ColumnMapping*> result;
1246
1.81k
        result.reserve(mapping.child_mappings.size());
1247
6.41k
        for (size_t child_idx = 0; child_idx < table_type.get_elements().size(); ++child_idx) {
1248
4.60k
            const auto& child_name = table_type.get_element_name(child_idx);
1249
4.60k
            const auto child_it = std::ranges::find_if(
1250
8.81k
                    mapping.child_mappings, [&](const ColumnMapping& child_mapping) {
1251
8.81k
                        return child_mapping.table_column_name == child_name;
1252
8.81k
                    });
1253
4.60k
            DORIS_CHECK(child_it != mapping.child_mappings.end())
1254
0
                    << mapping.debug_string() << ", table_child_name=" << child_name;
1255
4.60k
            result.push_back(&*child_it);
1256
4.60k
        }
1257
1.81k
        return result;
1258
1.81k
    }
1259
1260
    static const IColumn* _nested_column_if_nullable(const ColumnPtr& column,
1261
5.82k
                                                     const NullMap** null_map) {
1262
5.82k
        DORIS_CHECK(column.get() != nullptr);
1263
5.82k
        if (const auto* nullable_column = check_and_get_column<ColumnNullable>(*column)) {
1264
5.81k
            if (null_map != nullptr) {
1265
5.81k
                *null_map = &nullable_column->get_null_map_data();
1266
5.81k
            }
1267
5.81k
            return &nullable_column->get_nested_column();
1268
5.81k
        }
1269
4
        return column.get();
1270
5.82k
    }
1271
1272
    Status _materialize_struct_mapping_column(const ColumnMapping& mapping,
1273
                                              const ColumnPtr& file_column, const size_t rows,
1274
1.81k
                                              ColumnPtr* column) {
1275
1.81k
        DORIS_CHECK(mapping.table_type != nullptr);
1276
1.81k
        const auto* table_type =
1277
1.81k
                assert_cast<const DataTypeStruct*>(remove_nullable(mapping.table_type).get());
1278
1.81k
        const auto full_file_column = file_column->convert_to_full_column_if_const();
1279
1.81k
        const NullMap* parent_null_map = nullptr;
1280
1.81k
        const auto* nested_file_column =
1281
1.81k
                _nested_column_if_nullable(full_file_column, &parent_null_map);
1282
1.81k
        const auto* file_struct = assert_cast<const ColumnStruct*>(nested_file_column);
1283
1.81k
        DORIS_CHECK(table_type->get_elements().size() == mapping.child_mappings.size());
1284
1285
1.81k
        Columns child_columns;
1286
1.81k
        child_columns.reserve(mapping.child_mappings.size());
1287
1.81k
        const auto file_ordered_children =
1288
1.81k
                _present_child_mappings_in_file_order(mapping.child_mappings);
1289
1.81k
        const auto table_ordered_children =
1290
1.81k
                _child_mappings_in_table_type_order(mapping, *table_type);
1291
4.60k
        for (const auto* child_mapping : table_ordered_children) {
1292
4.60k
            DORIS_CHECK(child_mapping != nullptr);
1293
4.60k
            if (!child_mapping->file_local_id.has_value()) {
1294
1.80k
                child_columns.push_back(
1295
1.80k
                        child_mapping->table_type->create_column_const_with_default_value(rows)
1296
1.80k
                                ->convert_to_full_column_if_const());
1297
1.80k
                continue;
1298
1.80k
            }
1299
2.79k
            const auto file_child_idx =
1300
2.79k
                    _file_child_ordinal_for_mapping(mapping, *child_mapping, file_ordered_children);
1301
2.79k
            DORIS_CHECK(file_child_idx < file_struct->get_columns().size());
1302
2.79k
            ColumnPtr child_column = file_struct->get_column_ptr(file_child_idx);
1303
2.79k
            RETURN_IF_ERROR(_materialize_present_child_mapping_column(*child_mapping, child_column,
1304
2.79k
                                                                      rows, &child_column));
1305
2.79k
            child_columns.push_back(std::move(child_column));
1306
2.79k
        }
1307
1.81k
        MutableColumns mutable_child_columns;
1308
1.81k
        mutable_child_columns.reserve(child_columns.size());
1309
4.60k
        for (auto& child_column : child_columns) {
1310
4.60k
            mutable_child_columns.push_back(IColumn::mutate(std::move(child_column)));
1311
4.60k
        }
1312
1.81k
        auto result = ColumnStruct::create(std::move(mutable_child_columns));
1313
1.81k
        if (mapping.table_type->is_nullable()) {
1314
1.81k
            auto null_map = ColumnUInt8::create();
1315
1.81k
            auto& null_map_data = null_map->get_data();
1316
1.81k
            null_map_data.resize(rows);
1317
1.81k
            if (parent_null_map != nullptr) {
1318
1.81k
                DORIS_CHECK(parent_null_map->size() == rows);
1319
1.81k
                null_map_data.assign(parent_null_map->begin(), parent_null_map->end());
1320
1.81k
            } else {
1321
0
                std::fill(null_map_data.begin(), null_map_data.end(), 0);
1322
0
            }
1323
1.81k
            *column = ColumnNullable::create(std::move(result), std::move(null_map));
1324
1.81k
        } else {
1325
2
            *column = std::move(result);
1326
2
        }
1327
1.81k
        return Status::OK();
1328
1.81k
    }
1329
1330
    Status _materialize_array_mapping_column(const ColumnMapping& mapping,
1331
                                             const ColumnPtr& file_column, const size_t rows,
1332
2.22k
                                             ColumnPtr* column) {
1333
2.22k
        DORIS_CHECK(mapping.child_mappings.size() == 1);
1334
2.22k
        const auto full_file_column = file_column->convert_to_full_column_if_const();
1335
2.22k
        const NullMap* parent_null_map = nullptr;
1336
2.22k
        const auto* nested_file_column =
1337
2.22k
                _nested_column_if_nullable(full_file_column, &parent_null_map);
1338
2.22k
        const auto* file_array = assert_cast<const ColumnArray*>(nested_file_column);
1339
2.22k
        ColumnPtr nested_column = file_array->get_data_ptr();
1340
2.22k
        const auto& element_mapping = mapping.child_mappings[0];
1341
2.22k
        RETURN_IF_ERROR(_materialize_present_child_mapping_column(
1342
2.22k
                element_mapping, nested_column, nested_column->size(), &nested_column));
1343
2.22k
        auto offsets_column = file_array->get_offsets_ptr()->convert_to_full_column_if_const();
1344
2.22k
        auto result = ColumnArray::create(IColumn::mutate(std::move(nested_column)),
1345
2.22k
                                          IColumn::mutate(std::move(offsets_column)));
1346
2.22k
        if (mapping.table_type->is_nullable()) {
1347
2.22k
            auto null_map = ColumnUInt8::create();
1348
2.22k
            auto& null_map_data = null_map->get_data();
1349
2.22k
            null_map_data.resize(rows);
1350
2.22k
            if (parent_null_map != nullptr) {
1351
2.22k
                DORIS_CHECK(parent_null_map->size() == rows);
1352
2.22k
                null_map_data.assign(parent_null_map->begin(), parent_null_map->end());
1353
2.22k
            } else {
1354
0
                std::fill(null_map_data.begin(), null_map_data.end(), 0);
1355
0
            }
1356
2.22k
            *column = ColumnNullable::create(std::move(result), std::move(null_map));
1357
2.22k
        } else {
1358
0
            *column = std::move(result);
1359
0
        }
1360
2.22k
        return Status::OK();
1361
2.22k
    }
1362
1363
    Status _materialize_map_mapping_column(const ColumnMapping& mapping,
1364
                                           const ColumnPtr& file_column, const size_t rows,
1365
1.78k
                                           ColumnPtr* column) {
1366
1.78k
        const auto full_file_column = file_column->convert_to_full_column_if_const();
1367
1.78k
        const NullMap* parent_null_map = nullptr;
1368
1.78k
        const auto* nested_file_column =
1369
1.78k
                _nested_column_if_nullable(full_file_column, &parent_null_map);
1370
1.78k
        const auto* file_map = assert_cast<const ColumnMap*>(nested_file_column);
1371
1.78k
        ColumnPtr key_column = file_map->get_keys_ptr();
1372
1.78k
        ColumnPtr value_column = file_map->get_values_ptr();
1373
1374
1.78k
        const ColumnMapping* key_mapping = nullptr;
1375
1.78k
        const ColumnMapping* value_mapping = nullptr;
1376
3.56k
        for (const auto& child_mapping : mapping.child_mappings) {
1377
3.56k
            if (!child_mapping.file_local_id.has_value()) {
1378
0
                continue;
1379
0
            }
1380
3.56k
            if (*child_mapping.file_local_id == 0) {
1381
1.78k
                key_mapping = &child_mapping;
1382
1.78k
            } else if (*child_mapping.file_local_id == 1) {
1383
1.78k
                value_mapping = &child_mapping;
1384
1.78k
            }
1385
3.56k
        }
1386
1387
1.78k
        if (key_mapping != nullptr) {
1388
1.78k
            RETURN_IF_ERROR(_materialize_present_child_mapping_column(
1389
1.78k
                    *key_mapping, key_column, key_column->size(), &key_column));
1390
1.78k
        }
1391
1.78k
        if (value_mapping != nullptr) {
1392
1.78k
            RETURN_IF_ERROR(_materialize_present_child_mapping_column(
1393
1.78k
                    *value_mapping, value_column, value_column->size(), &value_column));
1394
1.78k
        }
1395
1.78k
        auto offsets_column = file_map->get_offsets_ptr()->convert_to_full_column_if_const();
1396
1.78k
        auto result = ColumnMap::create(IColumn::mutate(std::move(key_column)),
1397
1.78k
                                        IColumn::mutate(std::move(value_column)),
1398
1.78k
                                        IColumn::mutate(std::move(offsets_column)));
1399
1.78k
        if (mapping.table_type->is_nullable()) {
1400
1.78k
            auto null_map = ColumnUInt8::create();
1401
1.78k
            auto& null_map_data = null_map->get_data();
1402
1.78k
            null_map_data.resize(rows);
1403
1.78k
            if (parent_null_map != nullptr) {
1404
1.78k
                DORIS_CHECK(parent_null_map->size() == rows);
1405
1.78k
                null_map_data.assign(parent_null_map->begin(), parent_null_map->end());
1406
1.78k
            } else {
1407
0
                std::fill(null_map_data.begin(), null_map_data.end(), 0);
1408
0
            }
1409
1.78k
            *column = ColumnNullable::create(std::move(result), std::move(null_map));
1410
1.78k
        } else {
1411
2
            *column = std::move(result);
1412
2
        }
1413
1.78k
        return Status::OK();
1414
1.78k
    }
1415
1416
29.7k
    Status _open_mapping_exprs() {
1417
29.7k
        RowDescriptor row_desc;
1418
199k
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
1419
199k
            if (mapping.projection != nullptr) {
1420
190k
                RETURN_IF_ERROR(mapping.projection->prepare(_runtime_state, row_desc));
1421
190k
                RETURN_IF_ERROR(mapping.projection->open(_runtime_state));
1422
190k
            }
1423
199k
            if (mapping.default_expr != nullptr) {
1424
8.53k
                RETURN_IF_ERROR(mapping.default_expr->prepare(_runtime_state, row_desc));
1425
8.53k
                RETURN_IF_ERROR(mapping.default_expr->open(_runtime_state));
1426
8.53k
            }
1427
199k
        }
1428
29.7k
        return Status::OK();
1429
29.7k
    }
1430
1431
    Status _build_file_aggregate_request(TPushAggOp::type agg_type,
1432
642
                                         FileAggregateRequest* request) const {
1433
642
        DORIS_CHECK(request != nullptr);
1434
642
        DORIS_CHECK(_supports_aggregate_pushdown(agg_type));
1435
642
        request->agg_type = agg_type;
1436
642
        request->columns.clear();
1437
642
        if (agg_type == TPushAggOp::type::COUNT) {
1438
            // COUNT pushdown historically meant COUNT(*) and therefore carried no columns. For
1439
            // complex COUNT(col), materializing the full MAP/LIST/STRUCT value only to test the
1440
            // top-level NULL bit can be extremely expensive. When the scan projects exactly one
1441
            // directly-mapped complex column, pass that file column to the reader so formats such
1442
            // as Parquet can count the column shape from metadata/levels without decoding payload
1443
            // values like MAP value strings. Other COUNT cases stay on the existing row-count path
1444
            // to avoid changing count(*) semantics.
1445
626
            if (_data_reader.column_mapper->mappings().size() == 1) {
1446
626
                const auto& mapping = _data_reader.column_mapper->mappings()[0];
1447
626
                if (mapping.file_local_id.has_value() && mapping.file_type != nullptr &&
1448
626
                    is_complex_type(remove_nullable(mapping.file_type)->get_primitive_type()) &&
1449
626
                    mapping.virtual_column_type == TableVirtualColumnType::INVALID &&
1450
626
                    mapping.default_expr == nullptr) {
1451
0
                    FileAggregateRequest::Column column;
1452
0
                    column.projection =
1453
0
                            LocalColumnIndex::top_level(LocalColumnId(*mapping.file_local_id));
1454
0
                    request->columns.push_back(std::move(column));
1455
0
                }
1456
626
            }
1457
625
            return Status::OK();
1458
625
        }
1459
17
        request->columns.reserve(_data_reader.column_mapper->mappings().size());
1460
25
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
1461
25
            DORIS_CHECK(mapping.file_local_id.has_value());
1462
25
            FileAggregateRequest::Column column;
1463
25
            column.projection = LocalColumnIndex::top_level(LocalColumnId(*mapping.file_local_id));
1464
25
            if (!mapping.child_mappings.empty()) {
1465
1
                RETURN_IF_ERROR(build_aggregate_projection(mapping, &column.projection));
1466
1
            }
1467
25
            request->columns.push_back(std::move(column));
1468
25
        }
1469
17
        return Status::OK();
1470
17
    }
1471
1472
    Status _materialize_aggregate_pushdown_rows(TPushAggOp::type agg_type,
1473
                                                const FileAggregateResult& file_result,
1474
637
                                                Block* block) {
1475
637
        if (agg_type == TPushAggOp::type::COUNT) {
1476
            // COUNT pushdown is not a final count value. It emits `count` default rows so the
1477
            // upper COUNT(*) aggregate can count them and produce the final result, including
1478
            // zero rows when count is 0.
1479
625
            DORIS_CHECK(file_result.count >= 0);
1480
625
            return _materialize_count_rows(cast_set<size_t>(file_result.count), block);
1481
625
        }
1482
        // MIN/MAX pushdown emits two rows, min first and max second, for each projected column.
1483
        // The upper MIN/MAX aggregate consumes those two rows to produce the final aggregate value.
1484
12
        DORIS_CHECK(file_result.columns.size() == _data_reader.column_mapper->mappings().size());
1485
12
        DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
1486
12
        Block file_block;
1487
12
        file_block.reserve(_data_reader.file_block_layout.size());
1488
15
        for (const auto& column : _data_reader.file_block_layout) {
1489
15
            file_block.insert({column.type->create_column(), column.type, column.name});
1490
15
        }
1491
27
        for (size_t column_idx = 0; column_idx < file_result.columns.size(); ++column_idx) {
1492
15
            const auto& result_column = file_result.columns[column_idx];
1493
15
            if (!result_column.has_min || !result_column.has_max) {
1494
0
                return Status::NotSupported("Missing min/max aggregate result for column {}",
1495
0
                                            _projected_columns[column_idx].name);
1496
0
            }
1497
15
            bool found_file_column = false;
1498
19
            for (size_t block_position = 0; block_position < _data_reader.file_block_layout.size();
1499
19
                 ++block_position) {
1500
19
                if (_data_reader.file_block_layout[block_position].file_column_id ==
1501
19
                    file_result.columns[column_idx].projection.column_id()) {
1502
15
                    found_file_column = true;
1503
15
                    auto column = file_block.get_by_position(block_position)
1504
15
                                          .type->create_column()
1505
15
                                          ->assert_mutable();
1506
15
                    RETURN_IF_ERROR(_insert_aggregate_projection_value(
1507
15
                            file_result.columns[column_idx].projection, result_column.min_value,
1508
15
                            column.get()));
1509
15
                    RETURN_IF_ERROR(_insert_aggregate_projection_value(
1510
15
                            file_result.columns[column_idx].projection, result_column.max_value,
1511
15
                            column.get()));
1512
15
                    file_block.replace_by_position(block_position, std::move(column));
1513
15
                    break;
1514
15
                }
1515
19
            }
1516
15
            DORIS_CHECK(found_file_column);
1517
15
        }
1518
27
        for (size_t column_idx = 0; column_idx < _data_reader.column_mapper->mappings().size();
1519
15
             ++column_idx) {
1520
15
            ColumnPtr table_column;
1521
15
            RETURN_IF_ERROR(
1522
15
                    _materialize_mapping_column(_data_reader.column_mapper->mappings()[column_idx],
1523
15
                                                &file_block, 2, &table_column));
1524
15
            block->replace_by_position(column_idx, std::move(table_column));
1525
15
        }
1526
12
        return Status::OK();
1527
12
    }
1528
1529
    struct FileBlockColumn {
1530
        LocalColumnId file_column_id = LocalColumnId::invalid();
1531
        std::string name;
1532
        DataTypePtr type;
1533
    };
1534
1535
    struct DataReader {
1536
        std::unique_ptr<FileReader> reader;
1537
        std::unique_ptr<TableColumnMapper> column_mapper;
1538
        // Schema of the data file, also including virtual column (row position).
1539
        std::vector<ColumnDefinition> file_schema;
1540
        // Layout of the block returned by file reader, determined by column mapping and file
1541
        // schema. It is used for file reader to materialize columns into correct type and position.
1542
        std::vector<FileBlockColumn> file_block_layout;
1543
        Block block_template;
1544
    };
1545
    DataReader _data_reader;
1546
    std::vector<ColumnDefinition> _projected_columns;
1547
    std::unique_ptr<ScanTask> _current_task;
1548
    std::optional<io::FileDescription> _current_file_description;
1549
    // Range-level compression has higher priority than scan-param compression. TVF/load can keep
1550
    // the logical format as CSV/TEXT while carrying the concrete compression such as GZ or LZO on
1551
    // each TFileRangeDesc, matching the old FileScanner reader contract.
1552
    TFileCompressType::type _current_range_compress_type = TFileCompressType::UNKNOWN;
1553
    std::optional<TUniqueId> _current_range_load_id;
1554
    TFileRangeDesc _current_file_range_desc;
1555
    std::shared_ptr<io::FileSystemProperties> _system_properties;
1556
    // partition key -> value
1557
    std::map<std::string, Field> _partition_values;
1558
    // Predicates built from scan conjuncts before file-level localization.
1559
    std::vector<TableFilter> _table_filters;
1560
    // Number of localized filters before the first unsafe conjunct in the original row-level
1561
    // order. This differs from scanning `_table_filters` for safety because slotless predicates are
1562
    // intentionally absent from that vector but must still act as ordering barriers.
1563
    size_t _constant_pruning_safe_filter_count = 0;
1564
    VExprContextSPtrs _conjuncts;
1565
    ReadProfile _profile;
1566
    // Parsed from row-position based delete files, including position delete and deletion vector.
1567
    DeleteRows* _delete_rows = nullptr;
1568
    DeletionVector* _deletion_vector = nullptr;
1569
    TFileScanRangeParams* _scan_params;
1570
    std::shared_ptr<io::IOContext> _io_ctx;
1571
    RuntimeState* _runtime_state;
1572
    RuntimeProfile* _scanner_profile;
1573
    const std::vector<SlotDescriptor*>* _file_slot_descs = nullptr;
1574
    FileFormat _format;
1575
    TPushAggOp::type _push_down_agg_type = TPushAggOp::type::NONE;
1576
    size_t _batch_size = 0;
1577
    uint64_t _condition_cache_digest = 0;
1578
    segment_v2::ConditionCache::ExternalCacheKey _condition_cache_key;
1579
    std::shared_ptr<std::vector<bool>> _condition_cache;
1580
    std::shared_ptr<ConditionCacheContext> _condition_cache_ctx;
1581
    int64_t _condition_cache_hit_count = 0;
1582
    bool _current_reader_reached_eof = false;
1583
    int64_t _remaining_table_level_count = -1;
1584
    // Snapshot supplied by FileScannerV2 for the active split. It gates every shortcut that emits
1585
    // irreversible aggregate rows, not only the table-level row-count shortcut in prepare_split().
1586
    bool _all_runtime_filters_applied_for_split = true;
1587
    std::optional<GlobalRowIdContext> _global_rowid_context;
1588
    bool _aggregate_pushdown_tried = false;
1589
    bool _current_split_pruned = false;
1590
    TableColumnMapperOptions _mapper_options;
1591
1592
private:
1593
    static const ColumnDefinition* _find_column_definition(
1594
197k
            const std::vector<ColumnDefinition>& schema, LocalColumnId column_id) {
1595
4.76M
        for (const auto& field : schema) {
1596
4.76M
            if (field.file_local_id() == column_id.value()) {
1597
194k
                return &field;
1598
194k
            }
1599
4.76M
        }
1600
2.98k
        return nullptr;
1601
197k
    }
1602
1603
163
    static bool _can_push_down_minmax_for_mapping(const ColumnMapping& mapping) {
1604
163
        if (mapping.child_mappings.empty()) {
1605
            // Direct mappings use a slot-ref projection to materialize the file column. The
1606
            // projection does not transform ordering; casts and other conversions are already
1607
            // represented by a non-trivial mapping and must fall back to row scanning.
1608
160
            return mapping.is_trivial;
1609
160
        }
1610
3
        const auto primitive_type = remove_nullable(mapping.file_type)->get_primitive_type();
1611
3
        if (primitive_type != TYPE_STRUCT) {
1612
1
            return false;
1613
1
        }
1614
2
        size_t mapped_children = 0;
1615
2
        const ColumnMapping* mapped_child = nullptr;
1616
2
        for (const auto& child_mapping : mapping.child_mappings) {
1617
2
            if (!child_mapping.file_local_id.has_value()) {
1618
0
                continue;
1619
0
            }
1620
2
            ++mapped_children;
1621
2
            mapped_child = &child_mapping;
1622
2
        }
1623
2
        return mapped_children == 1 && mapped_child != nullptr &&
1624
2
               _can_push_down_minmax_for_mapping(*mapped_child);
1625
3
    }
1626
1627
    static Status build_aggregate_projection(const ColumnMapping& mapping,
1628
2
                                             LocalColumnIndex* projection) {
1629
2
        DORIS_CHECK(projection != nullptr);
1630
2
        DORIS_CHECK(mapping.file_local_id.has_value());
1631
2
        *projection = LocalColumnIndex::local(*mapping.file_local_id);
1632
2
        projection->children.clear();
1633
2
        projection->project_all_children = true;
1634
2
        if (mapping.child_mappings.empty()) {
1635
1
            return Status::OK();
1636
1
        }
1637
1
        projection->project_all_children = false;
1638
1
        for (const auto& child_mapping : mapping.child_mappings) {
1639
1
            if (!child_mapping.file_local_id.has_value()) {
1640
0
                continue;
1641
0
            }
1642
1
            LocalColumnIndex child_projection;
1643
1
            RETURN_IF_ERROR(build_aggregate_projection(child_mapping, &child_projection));
1644
1
            projection->children.push_back(std::move(child_projection));
1645
1
        }
1646
1
        DORIS_CHECK(projection->children.size() == 1);
1647
1
        return Status::OK();
1648
1
    }
1649
1650
    static Status _insert_aggregate_projection_value(const LocalColumnIndex& projection,
1651
64
                                                     const Field& value, IColumn* column) {
1652
64
        DORIS_CHECK(column != nullptr);
1653
64
        if (auto* nullable_column = check_and_get_column<ColumnNullable>(*column)) {
1654
32
            RETURN_IF_ERROR(_insert_aggregate_projection_value(
1655
32
                    projection, value, &nullable_column->get_nested_column()));
1656
32
            nullable_column->get_null_map_data().push_back(0);
1657
32
            return Status::OK();
1658
32
        }
1659
32
        if (projection.project_all_children || projection.children.empty()) {
1660
30
            column->insert(value);
1661
30
            return Status::OK();
1662
30
        }
1663
2
        auto* struct_column = assert_cast<ColumnStruct*>(column);
1664
2
        DORIS_CHECK(projection.children.size() == 1);
1665
2
        const auto& child_projection = projection.children[0];
1666
2
        DORIS_CHECK(struct_column->get_columns().size() == 1);
1667
2
        RETURN_IF_ERROR(_insert_aggregate_projection_value(child_projection, value,
1668
2
                                                           &struct_column->get_column(0)));
1669
2
        return Status::OK();
1670
2
    }
1671
1672
    // Parse a DV into its compressed bitmap. Position delete files continue to use _delete_rows.
1673
    Status _parse_delete_predicates(const SplitReadOptions& options);
1674
};
1675
1676
} // namespace doris::format