Coverage Report

Created: 2026-07-14 15:37

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
60.3k
    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
39.3k
    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
229k
    virtual void set_batch_size(size_t batch_size) {
180
229k
        _batch_size = std::max<size_t>(1, batch_size);
181
229k
        if (_data_reader.reader != nullptr) {
182
112k
            _data_reader.reader->set_batch_size(_batch_size);
183
112k
        }
184
229k
    }
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
86.4k
    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
175k
    virtual Status get_block(Block* block, bool* eos) {
217
175k
        SCOPED_TIMER(_profile.exec_timer);
218
175k
        DORIS_CHECK(block->columns() == _projected_columns.size());
219
175k
        block->clear_column_data(_projected_columns.size());
220
221
243k
        while (true) {
222
243k
            if (*eos) {
223
0
                return Status::OK();
224
0
            }
225
243k
            if (_io_ctx != nullptr && _io_ctx->should_stop) {
226
6
                *eos = true;
227
6
                return Status::OK();
228
6
            }
229
243k
            if (!_data_reader.reader) {
230
118k
                if (_is_table_level_count_active()) {
231
298
                    RETURN_IF_ERROR(_read_table_level_count(block, eos));
232
298
                    return Status::OK();
233
298
                }
234
118k
                RETURN_IF_ERROR(create_next_reader(eos));
235
118k
                if (!_data_reader.reader) {
236
59.5k
                    DCHECK(*eos);
237
59.5k
                    return Status::OK();
238
59.5k
                }
239
118k
            }
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
183k
            if (!_aggregate_pushdown_tried) {
246
58.9k
                SCOPED_TIMER(_profile.pushdown_agg_timer);
247
58.9k
                bool pushed_down = false;
248
58.9k
                const auto status = _try_materialize_aggregate_pushdown_rows(block, &pushed_down);
249
58.9k
                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
58.9k
                if (pushed_down) {
258
1.27k
                    return Status::OK();
259
1.27k
                }
260
58.9k
            }
261
262
182k
            bool current_eof = false;
263
182k
            _data_reader.block_template.clear_column_data(
264
182k
                    cast_set<int64_t>(_data_reader.file_block_layout.size()));
265
182k
            size_t current_rows = 0;
266
182k
            RETURN_IF_ERROR(_data_reader.reader->get_block(&_data_reader.block_template,
267
182k
                                                           &current_rows, &current_eof));
268
182k
            const bool stopped_during_read = _io_ctx != nullptr && _io_ctx->should_stop;
269
182k
            if (current_rows == 0) {
270
68.2k
                if (current_eof) {
271
57.0k
                    _current_reader_reached_eof = !stopped_during_read;
272
57.0k
                    RETURN_IF_ERROR(close_current_reader());
273
57.0k
                }
274
68.2k
                continue;
275
68.2k
            }
276
182k
            DCHECK_EQ(_data_reader.block_template.columns(), _data_reader.file_block_layout.size())
277
0
                    << _data_reader.block_template.dump_structure();
278
114k
#ifndef NDEBUG
279
114k
            RETURN_IF_ERROR(_check_file_block_columns("after file reader get_block", current_rows));
280
114k
#endif
281
114k
            DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
282
114k
            RETURN_IF_ERROR(finalize_chunk(block, current_rows));
283
114k
#ifndef NDEBUG
284
114k
            RETURN_IF_ERROR(
285
114k
                    _check_table_block_columns("after finalize_chunk", block, current_rows));
286
114k
#endif
287
114k
            if (current_eof) {
288
14
                _current_reader_reached_eof = !stopped_during_read;
289
14
                RETURN_IF_ERROR(close_current_reader());
290
14
            }
291
114k
            return Status::OK();
292
114k
        }
293
175k
    }
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
39.3k
    virtual Status close() {
298
39.3k
        if (_data_reader.reader) {
299
630
            RETURN_IF_ERROR(close_current_reader());
300
630
        }
301
39.3k
        _current_task.reset();
302
39.3k
        _current_file_description.reset();
303
39.3k
        _remaining_table_level_count = -1;
304
39.3k
        return Status::OK();
305
39.3k
    }
306
307
78.3k
    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
19.3k
    virtual Status validate_projected_columns(const ProjectedColumnBuildContext& context) const {
316
19.3k
        (void)context;
317
19.3k
        return Status::OK();
318
19.3k
    }
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
34.4k
                                               DeleteFileDesc* desc, bool* has_delete_file) {
327
34.4k
        *has_delete_file = false;
328
34.4k
        return Status::OK();
329
34.4k
    }
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
6.58k
    virtual TableColumnMappingMode mapping_mode() const { return TableColumnMappingMode::BY_NAME; }
336
32.1k
    virtual Status annotate_file_schema(std::vector<ColumnDefinition>* file_schema) {
337
32.1k
        DORIS_CHECK(file_schema != nullptr);
338
32.1k
        return Status::OK();
339
32.1k
    }
340
341
    // Open the concrete reader for the current split/task and build the file-local scan request.
342
60.1k
    virtual Status open_reader() {
343
60.1k
        SCOPED_TIMER(_profile.open_reader_timer);
344
        // 1. Get file schema and create column mapping.
345
60.1k
        std::vector<ColumnDefinition> file_schema;
346
60.1k
        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
60.1k
        RETURN_IF_ERROR(annotate_file_schema(&file_schema));
350
60.1k
        _data_reader.file_schema = file_schema;
351
60.1k
        _mapper_options.mode = mapping_mode();
352
353
60.1k
        _data_reader.column_mapper = _data_reader.reader->create_column_mapper(_mapper_options);
354
60.1k
        DORIS_CHECK(_data_reader.column_mapper != nullptr);
355
60.1k
        RETURN_IF_ERROR(_data_reader.column_mapper->create_mapping(_projected_columns,
356
60.1k
                                                                   _partition_values, file_schema));
357
60.1k
        DORIS_CHECK(_data_reader.column_mapper->mappings().size() == _projected_columns.size());
358
359
        // 2. Build table filters based on conjuncts and column predicates.
360
60.1k
        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
60.1k
        auto file_request = std::make_shared<FileScanRequest>();
366
60.1k
        RETURN_IF_ERROR(_data_reader.column_mapper->create_scan_request(
367
60.1k
                _table_filters, _projected_columns, file_request.get(), _runtime_state));
368
60.1k
        bool constant_filter_pruned_split = false;
369
60.1k
        RETURN_IF_ERROR(_evaluate_constant_filters(&constant_filter_pruned_split));
370
60.1k
        if (constant_filter_pruned_split) {
371
1.23k
            RETURN_IF_ERROR(close_current_reader());
372
1.23k
            return Status::OK();
373
1.23k
        }
374
58.9k
        RETURN_IF_ERROR(customize_file_scan_request(file_request.get()));
375
58.9k
        RETURN_IF_ERROR(_open_local_filter_exprs(*file_request));
376
58.9k
        _data_reader.file_block_layout.clear();
377
58.9k
        _data_reader.block_template.clear();
378
58.9k
        _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
383k
        for (const auto& [file_column_id, block_position] : file_request->local_positions) {
383
383k
            DORIS_CHECK(block_position.value() < _data_reader.file_block_layout.size());
384
383k
            const auto* field = _find_column_definition(_data_reader.file_schema, file_column_id);
385
383k
            DORIS_CHECK(field != nullptr);
386
387
383k
            ColumnDefinition projected_field;
388
383k
            {
389
383k
                auto it = std::find_if(
390
383k
                        file_request->non_predicate_columns.begin(),
391
383k
                        file_request->non_predicate_columns.end(),
392
7.92M
                        [&](const LocalColumnIndex& p) { return p.column_id() == file_column_id; });
393
383k
                if (it != file_request->non_predicate_columns.end()) {
394
348k
                    RETURN_IF_ERROR(project_column_definition(*field, *it, &projected_field));
395
348k
                }
396
383k
            }
397
383k
            {
398
383k
                auto it = std::find_if(
399
383k
                        file_request->predicate_columns.begin(),
400
383k
                        file_request->predicate_columns.end(),
401
383k
                        [&](const LocalColumnIndex& p) { return p.column_id() == file_column_id; });
402
383k
                if (it != file_request->predicate_columns.end()) {
403
35.1k
                    RETURN_IF_ERROR(project_column_definition(*field, *it, &projected_field));
404
35.1k
                }
405
383k
            }
406
383k
            _data_reader.file_block_layout[block_position.value()] = {
407
383k
                    .file_column_id = file_column_id,
408
383k
                    .name = projected_field.name,
409
383k
                    .type = projected_field.type,
410
383k
            };
411
383k
            DORIS_CHECK(_data_reader.file_block_layout[block_position.value()].type != nullptr);
412
383k
        }
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
58.9k
        _data_reader.block_template.reserve(_data_reader.file_block_layout.size());
417
384k
        for (const auto& column : _data_reader.file_block_layout) {
418
384k
            _data_reader.block_template.insert(
419
384k
                    {column.type->create_column(), column.type, column.name});
420
384k
        }
421
58.9k
        if (VLOG_DEBUG_IS_ON) {
422
0
            VLOG_DEBUG << "TableReader debug: " << debug_string();
423
0
        }
424
58.9k
        RETURN_IF_ERROR(_open_mapping_exprs());
425
58.9k
        RETURN_IF_ERROR(_data_reader.reader->open(file_request));
426
58.9k
        RETURN_IF_ERROR(_init_reader_condition_cache(*file_request));
427
58.9k
        return Status::OK();
428
58.9k
    }
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
60.0k
    Status _evaluate_constant_filters(bool* can_filter_all) {
441
60.0k
        DORIS_CHECK(can_filter_all != nullptr);
442
60.0k
        DORIS_CHECK_LE(_constant_pruning_safe_filter_count, _table_filters.size());
443
60.0k
        *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
98.7k
        for (size_t i = 0; i < _constant_pruning_safe_filter_count; ++i) {
448
39.9k
            const auto& table_filter = _table_filters[i];
449
39.9k
            if (table_filter.conjunct == nullptr) {
450
0
                continue;
451
0
            }
452
39.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
39.9k
            if (table_filter.conjunct->root()->is_rf_wrapper() ||
458
39.9k
                !_table_filter_has_only_constant_entries(table_filter)) {
459
36.5k
                continue;
460
36.5k
            }
461
3.39k
            Block eval_block;
462
3.39k
            RETURN_IF_ERROR(_build_constant_filter_block(table_filter, &eval_block));
463
3.39k
            RowDescriptor row_desc;
464
3.39k
            RETURN_IF_ERROR(table_filter.conjunct->prepare(_runtime_state, row_desc));
465
3.39k
            RETURN_IF_ERROR(table_filter.conjunct->open(_runtime_state));
466
3.39k
            int result_column_id = -1;
467
3.39k
            RETURN_IF_ERROR(table_filter.conjunct->execute(&eval_block, &result_column_id));
468
3.39k
            DORIS_CHECK(result_column_id >= 0);
469
3.39k
            if (_filter_result_filters_all(eval_block.get_by_position(result_column_id).column)) {
470
1.23k
                *can_filter_all = true;
471
1.23k
                return Status::OK();
472
1.23k
            }
473
3.39k
        }
474
58.8k
        return Status::OK();
475
60.0k
    }
476
477
34.7k
    bool _table_filter_has_only_constant_entries(const TableFilter& table_filter) const {
478
34.7k
        const auto& filter_entries = _data_reader.column_mapper->filter_entries();
479
35.0k
        for (const auto global_index : table_filter.global_indices) {
480
35.0k
            const auto entry_it = filter_entries.find(global_index);
481
35.0k
            if (entry_it == filter_entries.end() || !entry_it->second.is_constant()) {
482
31.3k
                return false;
483
31.3k
            }
484
35.0k
        }
485
3.45k
        return !table_filter.global_indices.empty();
486
34.7k
    }
487
488
3.40k
    Status _build_constant_filter_block(const TableFilter& table_filter, Block* eval_block) {
489
3.40k
        DORIS_CHECK(eval_block != nullptr);
490
3.40k
        eval_block->clear();
491
3.40k
        const auto& mappings = _data_reader.column_mapper->mappings();
492
3.40k
        const auto& filter_entries = _data_reader.column_mapper->filter_entries();
493
3.40k
        DORIS_CHECK(mappings.size() == _projected_columns.size());
494
14.4k
        for (size_t column_idx = 0; column_idx < mappings.size(); ++column_idx) {
495
11.0k
            const auto global_index = GlobalIndex(column_idx);
496
11.0k
            const auto& mapping = mappings[column_idx];
497
11.0k
            const auto entry_it = filter_entries.find(global_index);
498
11.0k
            const bool referenced_by_filter =
499
11.0k
                    std::find(table_filter.global_indices.begin(),
500
11.0k
                              table_filter.global_indices.end(),
501
11.0k
                              global_index) != table_filter.global_indices.end();
502
11.0k
            if (referenced_by_filter && entry_it != filter_entries.end() &&
503
11.0k
                entry_it->second.is_constant()) {
504
3.64k
                ColumnPtr constant_column;
505
3.64k
                RETURN_IF_ERROR(_materialize_constant_filter_column(
506
3.64k
                        entry_it->second.constant_index(), &constant_column));
507
3.64k
                eval_block->insert({std::move(constant_column), mapping.table_type,
508
3.64k
                                    mapping.table_column_name});
509
7.38k
            } else {
510
7.38k
                eval_block->insert({mapping.table_type->create_column_const_with_default_value(1),
511
7.38k
                                    mapping.table_type, mapping.table_column_name});
512
7.38k
            }
513
11.0k
        }
514
3.40k
        return Status::OK();
515
3.40k
    }
516
517
3.64k
    Status _materialize_constant_filter_column(ConstantIndex constant_index, ColumnPtr* column) {
518
3.64k
        DORIS_CHECK(column != nullptr);
519
3.64k
        const auto& constant_entry = _data_reader.column_mapper->constant_map().get(constant_index);
520
3.64k
        DORIS_CHECK(constant_entry.expr != nullptr);
521
3.64k
        DORIS_CHECK(constant_entry.type != nullptr);
522
3.64k
        RowDescriptor row_desc;
523
3.64k
        RETURN_IF_ERROR(constant_entry.expr->prepare(_runtime_state, row_desc));
524
3.64k
        RETURN_IF_ERROR(constant_entry.expr->open(_runtime_state));
525
3.64k
        Block eval_block;
526
3.64k
        eval_block.insert({constant_entry.type->create_column_const_with_default_value(1),
527
3.64k
                           constant_entry.type, "__table_reader_constant_filter"});
528
3.64k
        int result_column_id = -1;
529
3.64k
        RETURN_IF_ERROR(constant_entry.expr->execute(&eval_block, &result_column_id));
530
3.64k
        DORIS_CHECK(result_column_id >= 0);
531
3.64k
        *column = eval_block.get_by_position(result_column_id).column;
532
3.64k
        DORIS_CHECK((*column)->size() == 1);
533
3.64k
        return Status::OK();
534
3.64k
    }
535
536
3.41k
    static bool _filter_result_filters_all(const ColumnPtr& filter_column) {
537
3.41k
        DORIS_CHECK(filter_column.get() != nullptr);
538
3.41k
        DORIS_CHECK(filter_column->size() == 1);
539
3.41k
        return !filter_column->get_bool(0);
540
3.41k
    }
541
542
58.8k
    virtual Status customize_file_scan_request(FileScanRequest* file_request) {
543
58.8k
        return _append_delete_predicate(file_request);
544
58.8k
    }
545
546
204k
    bool _is_table_level_count_active() const { return _remaining_table_level_count >= 0; }
547
548
1.40k
    Status _materialize_count_rows(size_t rows, Block* block) const {
549
1.40k
        DORIS_CHECK(block != nullptr);
550
1.40k
        DORIS_CHECK(block->columns() > 0 || rows == 0);
551
2.80k
        for (size_t column_idx = 0; column_idx < block->columns(); ++column_idx) {
552
1.40k
            auto column = block->get_by_position(column_idx).type->create_column();
553
1.40k
            column->resize(rows);
554
1.40k
            block->replace_by_position(column_idx, std::move(column));
555
1.40k
        }
556
1.40k
        return Status::OK();
557
1.40k
    }
558
559
298
    Status _read_table_level_count(Block* block, bool* eos) {
560
298
        DORIS_CHECK(block != nullptr);
561
298
        DORIS_CHECK(eos != nullptr);
562
298
        DORIS_CHECK(_push_down_agg_type == TPushAggOp::type::COUNT);
563
298
        DORIS_CHECK(_remaining_table_level_count >= 0);
564
298
        if (_remaining_table_level_count == 0) {
565
148
            _remaining_table_level_count = -1;
566
148
            _current_task.reset();
567
148
            *eos = true;
568
148
            return Status::OK();
569
148
        }
570
571
150
        const int64_t batch_size = _runtime_state == nullptr
572
150
                                           ? _remaining_table_level_count
573
150
                                           : static_cast<int64_t>(_runtime_state->batch_size());
574
150
        const auto rows = std::min(_remaining_table_level_count, batch_size);
575
150
        RETURN_IF_ERROR(_materialize_count_rows(cast_set<size_t>(rows), block));
576
150
        _remaining_table_level_count -= rows;
577
150
        *eos = false;
578
150
        return Status::OK();
579
150
    }
580
581
    void _append_file_scan_column(FileScanRequest* request, LocalColumnId column_id,
582
10.1k
                                  std::vector<LocalColumnIndex>* scan_columns) {
583
10.1k
        DORIS_CHECK(request != nullptr);
584
10.1k
        DORIS_CHECK(scan_columns != nullptr);
585
10.1k
        FileScanRequestBuilder builder(request);
586
10.1k
        Status status;
587
10.1k
        if (scan_columns == &request->predicate_columns) {
588
9.56k
            status = builder.add_predicate_column(column_id);
589
9.56k
        } else {
590
571
            DORIS_CHECK(scan_columns == &request->non_predicate_columns);
591
571
            status = builder.add_non_predicate_column(column_id);
592
571
        }
593
10.1k
        DORIS_CHECK(status.ok()) << status.to_string();
594
10.1k
        if (column_id == LocalColumnId(ROW_POSITION_COLUMN_ID) &&
595
10.1k
            _find_column_definition(_data_reader.file_schema, column_id) == nullptr) {
596
5.52k
            _data_reader.file_schema.push_back(row_position_column_definition());
597
5.52k
        }
598
10.1k
    }
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
58.7k
    Status _append_delete_predicate(FileScanRequest* request) {
602
58.7k
        DORIS_CHECK(request != nullptr);
603
58.7k
        if ((_delete_rows == nullptr || _delete_rows->empty()) &&
604
58.7k
            (_deletion_vector == nullptr || _deletion_vector->isEmpty())) {
605
53.7k
            return Status::OK();
606
53.7k
        }
607
5.04k
        const auto row_position_column_id = LocalColumnId(ROW_POSITION_COLUMN_ID);
608
5.04k
        _append_file_scan_column(request, row_position_column_id, &request->predicate_columns);
609
610
5.04k
        const auto block_position = request->local_positions.at(row_position_column_id);
611
5.09k
        auto append_predicate = [&](auto& deleted_rows) {
612
5.09k
            auto delete_predicate = std::make_shared<DeletePredicate>(deleted_rows);
613
5.09k
            delete_predicate->add_child(VSlotRef::create_shared(
614
5.09k
                    cast_set<int>(block_position.value()), cast_set<int>(block_position.value()),
615
5.09k
                    -1, std::make_shared<DataTypeInt64>(), ROW_POSITION_COLUMN_NAME));
616
5.09k
            request->delete_conjuncts.push_back(
617
5.09k
                    VExprContext::create_shared(std::move(delete_predicate)));
618
5.09k
        };
_ZZN5doris6format11TableReader24_append_delete_predicateEPNS0_15FileScanRequestEENKUlRT_E_clISt6vectorIlSaIlEEEEDaS5_
Line
Count
Source
611
1.49k
        auto append_predicate = [&](auto& deleted_rows) {
612
1.49k
            auto delete_predicate = std::make_shared<DeletePredicate>(deleted_rows);
613
1.49k
            delete_predicate->add_child(VSlotRef::create_shared(
614
1.49k
                    cast_set<int>(block_position.value()), cast_set<int>(block_position.value()),
615
1.49k
                    -1, std::make_shared<DataTypeInt64>(), ROW_POSITION_COLUMN_NAME));
616
1.49k
            request->delete_conjuncts.push_back(
617
1.49k
                    VExprContext::create_shared(std::move(delete_predicate)));
618
1.49k
        };
_ZZN5doris6format11TableReader24_append_delete_predicateEPNS0_15FileScanRequestEENKUlRT_E_clIN7roaring12Roaring64MapEEEDaS5_
Line
Count
Source
611
3.60k
        auto append_predicate = [&](auto& deleted_rows) {
612
3.60k
            auto delete_predicate = std::make_shared<DeletePredicate>(deleted_rows);
613
3.60k
            delete_predicate->add_child(VSlotRef::create_shared(
614
3.60k
                    cast_set<int>(block_position.value()), cast_set<int>(block_position.value()),
615
3.60k
                    -1, std::make_shared<DataTypeInt64>(), ROW_POSITION_COLUMN_NAME));
616
3.60k
            request->delete_conjuncts.push_back(
617
3.60k
                    VExprContext::create_shared(std::move(delete_predicate)));
618
3.60k
        };
619
5.04k
        if (_delete_rows != nullptr && !_delete_rows->empty()) {
620
1.49k
            append_predicate(*_delete_rows);
621
1.49k
        }
622
5.04k
        if (_deletion_vector != nullptr && !_deletion_vector->isEmpty()) {
623
3.61k
            append_predicate(*_deletion_vector);
624
3.61k
        }
625
5.04k
        return Status::OK();
626
58.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
60.2k
    virtual Status close_current_reader() {
631
60.2k
        _finalize_reader_condition_cache();
632
60.2k
        RETURN_IF_ERROR(_data_reader.reader->close());
633
60.2k
        _data_reader.reader.reset();
634
60.2k
        if (_data_reader.column_mapper != nullptr) {
635
60.1k
            _data_reader.column_mapper->clear();
636
60.1k
            _data_reader.column_mapper.reset();
637
60.1k
        }
638
60.2k
        _table_filters.clear();
639
60.2k
        _constant_pruning_safe_filter_count = 0;
640
60.2k
        _data_reader.file_schema.clear();
641
60.2k
        _data_reader.file_block_layout.clear();
642
60.2k
        _data_reader.block_template.clear();
643
60.2k
        _current_task.reset();
644
60.2k
        _current_file_description.reset();
645
60.2k
        _current_reader_reached_eof = false;
646
60.2k
        return Status::OK();
647
60.2k
    }
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
114k
    Status finalize_chunk(Block* block, const size_t rows) {
657
114k
        SCOPED_TIMER(_profile.finalize_timer);
658
114k
        size_t idx = 0;
659
633k
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
660
633k
            ColumnPtr column;
661
633k
            RETURN_IF_ERROR(_materialize_mapping_column(mapping, &_data_reader.block_template, rows,
662
633k
                                                        &column));
663
633k
            block->replace_by_position(idx, IColumn::mutate(std::move(column)));
664
633k
            idx++;
665
633k
        }
666
114k
        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
114k
        RETURN_IF_ERROR(_truncate_char_or_varchar_columns(block));
670
114k
        return Status::OK();
671
114k
    }
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
90.0k
    virtual Status materialize_virtual_columns(Block* table_block) { return Status::OK(); }
677
678
#ifndef NDEBUG
679
114k
    Status _check_file_block_columns(std::string_view stage, size_t rows) {
680
114k
        DORIS_CHECK(_data_reader.block_template.columns() == _data_reader.file_block_layout.size());
681
736k
        for (size_t idx = 0; idx < _data_reader.block_template.columns(); ++idx) {
682
622k
            const auto& file_block_column = _data_reader.file_block_layout[idx];
683
622k
            const auto& column_with_type = _data_reader.block_template.get_by_position(idx);
684
622k
            const auto* column = column_with_type.column.get();
685
622k
            try {
686
622k
                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
622k
                column->sanity_check();
699
622k
                auto st = column_with_type.check_type_and_column_match();
700
622k
                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
622k
            } 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
622k
        }
743
114k
        return Status::OK();
744
114k
    }
745
746
114k
    Status _check_table_block_columns(std::string_view stage, const Block* block, size_t rows) {
747
114k
        DORIS_CHECK(block != nullptr);
748
114k
        DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
749
747k
        for (size_t idx = 0; idx < block->columns(); ++idx) {
750
633k
            const auto& mapping = _data_reader.column_mapper->mappings()[idx];
751
633k
            const auto& column_with_type = block->get_by_position(idx);
752
633k
            const auto* column = column_with_type.column.get();
753
633k
            try {
754
633k
                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
633k
                column->sanity_check();
765
633k
                auto st = column_with_type.check_type_and_column_match();
766
633k
                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
633k
            } 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
633k
        }
804
114k
        return Status::OK();
805
114k
    }
806
#endif
807
808
114k
    Status _truncate_char_or_varchar_columns(Block* block) {
809
114k
        DORIS_CHECK(block != nullptr);
810
114k
        if (_runtime_state == nullptr ||
811
114k
            !_runtime_state->query_options().truncate_char_or_varchar_columns) {
812
114k
            return Status::OK();
813
114k
        }
814
8
        DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
815
44
        for (size_t idx = 0; idx < _data_reader.column_mapper->mappings().size(); ++idx) {
816
36
            const auto& mapping = _data_reader.column_mapper->mappings()[idx];
817
36
            if (!_should_truncate_char_or_varchar_column(mapping)) {
818
12
                continue;
819
12
            }
820
24
            const auto target_len =
821
24
                    assert_cast<const DataTypeString*>(remove_nullable(mapping.table_type).get())
822
24
                            ->len();
823
24
            _truncate_char_or_varchar_column(block, idx, target_len);
824
24
        }
825
8
        return Status::OK();
826
114k
    }
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
41
    static bool _should_truncate_char_or_varchar_column(const ColumnMapping& mapping) {
834
41
        if (mapping.table_type == nullptr) {
835
0
            return false;
836
0
        }
837
41
        const auto table_type = remove_nullable(mapping.table_type);
838
41
        const auto primitive_type = table_type->get_primitive_type();
839
41
        if (primitive_type != TYPE_VARCHAR && primitive_type != TYPE_CHAR) {
840
13
            return false;
841
13
        }
842
28
        const auto target_len = assert_cast<const DataTypeString*>(table_type.get())->len();
843
28
        if (target_len <= 0) {
844
0
            return false;
845
0
        }
846
28
        if (mapping.file_type == nullptr) {
847
0
            return true;
848
0
        }
849
28
        const auto file_type = remove_nullable(mapping.file_type);
850
28
        DORIS_CHECK(file_type != nullptr);
851
28
        int file_len = -1;
852
28
        if (file_type->get_primitive_type() == TYPE_VARCHAR ||
853
28
            file_type->get_primitive_type() == TYPE_CHAR ||
854
28
            file_type->get_primitive_type() == TYPE_STRING) {
855
27
            file_len = assert_cast<const DataTypeString*>(file_type.get())->len();
856
27
        }
857
858
28
        return file_len < 0 || target_len < file_len;
859
28
    }
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
25
    static void _truncate_char_or_varchar_column(Block* block, size_t idx, int len) {
866
25
        DORIS_CHECK(block != nullptr);
867
25
        auto int_type = std::make_shared<DataTypeInt32>();
868
25
        const auto num_columns_without_result = cast_set<uint32_t>(block->columns());
869
25
        auto& target = block->get_by_position(idx);
870
25
        const bool is_nullable = target.type->is_nullable();
871
25
        ColumnPtr input_column = target.column;
872
25
        ColumnPtr null_map_column;
873
25
        if (is_nullable) {
874
25
            const auto* nullable_column = assert_cast<const ColumnNullable*>(target.column.get());
875
25
            input_column = nullable_column->get_nested_column_ptr();
876
25
            null_map_column = nullable_column->get_null_map_column_ptr();
877
25
        }
878
25
        block->replace_by_position(idx, std::move(input_column));
879
25
        block->insert({int_type->create_column_const(block->rows(), to_field<TYPE_INT>(1)),
880
25
                       int_type, "const 1"});
881
25
        block->insert({int_type->create_column_const(block->rows(), to_field<TYPE_INT>(len)),
882
25
                       int_type, "const len"});
883
25
        block->insert({nullptr, std::make_shared<DataTypeString>(), "result"});
884
885
25
        ColumnNumbers temp_arguments(3);
886
25
        temp_arguments[0] = cast_set<uint32_t>(idx);
887
25
        temp_arguments[1] = num_columns_without_result;
888
25
        temp_arguments[2] = num_columns_without_result + 1;
889
25
        const uint32_t result_column_id = num_columns_without_result + 2;
890
25
        SubstringUtil::substring_execute(*block, temp_arguments, result_column_id, block->rows());
891
892
25
        ColumnPtr result_column = block->get_by_position(result_column_id).column;
893
25
        if (is_nullable) {
894
25
            result_column = ColumnNullable::create(std::move(result_column), null_map_column);
895
25
        }
896
25
        block->replace_by_position(idx, std::move(result_column));
897
25
        block->erase_tail(num_columns_without_result);
898
25
    }
899
900
58.7k
    Status _try_materialize_aggregate_pushdown_rows(Block* block, bool* pushed_down) {
901
58.7k
        DORIS_CHECK(block != nullptr);
902
58.7k
        DORIS_CHECK(pushed_down != nullptr);
903
58.7k
        *pushed_down = false;
904
58.7k
        block->clear_column_data(_projected_columns.size());
905
58.7k
        _aggregate_pushdown_tried = true;
906
58.7k
        if (!_supports_aggregate_pushdown(_push_down_agg_type)) {
907
57.5k
            return Status::OK();
908
57.5k
        }
909
910
1.17k
        FileAggregateRequest file_request;
911
1.17k
        RETURN_IF_ERROR(_build_file_aggregate_request(_push_down_agg_type, &file_request));
912
1.17k
        FileAggregateResult file_result;
913
1.17k
        const auto status = _data_reader.reader->get_aggregate_result(file_request, &file_result);
914
1.17k
        if (status.is<ErrorCode::NOT_IMPLEMENTED_ERROR>()) {
915
5
            return Status::OK();
916
5
        }
917
1.17k
        RETURN_IF_ERROR(status);
918
1.17k
        RETURN_IF_ERROR(
919
1.17k
                _materialize_aggregate_pushdown_rows(_push_down_agg_type, file_result, block));
920
1.17k
        *pushed_down = true;
921
1.17k
        RETURN_IF_ERROR(close_current_reader());
922
1.17k
        return Status::OK();
923
1.17k
    }
924
925
60.1k
    virtual bool _supports_aggregate_pushdown(TPushAggOp::type agg_type) const {
926
        // Only COUNT and MIN/MAX can be push down.
927
60.1k
        if (agg_type != TPushAggOp::type::COUNT && agg_type != TPushAggOp::type::MINMAX) {
928
56.6k
            return false;
929
56.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
3.52k
        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
3.52k
        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
3.52k
        if ((_delete_rows != nullptr && !_delete_rows->empty()) ||
948
3.52k
            (_deletion_vector != nullptr && !_deletion_vector->isEmpty())) {
949
597
            return false;
950
597
        }
951
2.92k
        if (!_table_filters.empty()) {
952
0
            return false;
953
0
        }
954
2.92k
        if (agg_type == TPushAggOp::type::COUNT) {
955
2.65k
            return true;
956
2.65k
        }
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
325
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
961
325
            if (!mapping.file_local_id.has_value() ||
962
325
                mapping.virtual_column_type != TableVirtualColumnType::INVALID ||
963
325
                mapping.default_expr != nullptr || mapping.file_type == nullptr ||
964
325
                mapping.table_type == nullptr) {
965
9
                return false;
966
9
            }
967
316
            if (!_can_push_down_minmax_for_mapping(mapping)) {
968
222
                return false;
969
222
            }
970
316
        }
971
42
        return true;
972
273
    }
973
974
623k
    static ColumnPtr _detach_column(ColumnPtr column) {
975
623k
        DORIS_CHECK(column.get() != nullptr);
976
623k
        return IColumn::mutate(std::move(column));
977
623k
    }
978
979
94.3k
    static Status _align_column_nullability(ColumnPtr* column, const DataTypePtr& table_type) {
980
94.3k
        DORIS_CHECK(column != nullptr);
981
94.3k
        DORIS_CHECK(column->get() != nullptr);
982
94.3k
        DORIS_CHECK(table_type != nullptr);
983
        // Must return non-const column
984
94.3k
        *column = (*column)->convert_to_full_column_if_const();
985
94.3k
        if (table_type->is_nullable()) {
986
47.1k
            const auto& nested_type =
987
47.1k
                    assert_cast<const DataTypeNullable&>(*table_type).get_nested_type();
988
47.1k
            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
47.1k
            const auto& nullable_column = assert_cast<const ColumnNullable&>(**column);
994
47.1k
            ColumnPtr nested_column = nullable_column.get_nested_column_ptr();
995
47.1k
            RETURN_IF_ERROR(_align_column_nullability(&nested_column, nested_type));
996
47.1k
            *column = ColumnNullable::create(nested_column,
997
47.1k
                                             nullable_column.get_null_map_column_ptr());
998
47.1k
            return Status::OK();
999
47.1k
        }
1000
47.2k
        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
47.2k
        if (const auto* array_type = typeid_cast<const DataTypeArray*>(table_type.get())) {
1012
129
            const auto& array_column = assert_cast<const ColumnArray&>(**column);
1013
129
            ColumnPtr nested_column = array_column.get_data_ptr();
1014
129
            RETURN_IF_ERROR(
1015
129
                    _align_column_nullability(&nested_column, array_type->get_nested_type()));
1016
129
            *column = ColumnArray::create(nested_column, array_column.get_offsets_ptr());
1017
129
            return Status::OK();
1018
129
        }
1019
47.0k
        if (const auto* map_type = typeid_cast<const DataTypeMap*>(table_type.get())) {
1020
8
            const auto& map_column = assert_cast<const ColumnMap&>(**column);
1021
8
            ColumnPtr key_column = map_column.get_keys_ptr();
1022
8
            ColumnPtr value_column = map_column.get_values_ptr();
1023
8
            RETURN_IF_ERROR(_align_column_nullability(&key_column, map_type->get_key_type()));
1024
8
            RETURN_IF_ERROR(_align_column_nullability(&value_column, map_type->get_value_type()));
1025
8
            *column = ColumnMap::create(key_column, value_column, map_column.get_offsets_ptr());
1026
8
            return Status::OK();
1027
8
        }
1028
47.0k
        if (const auto* struct_type = typeid_cast<const DataTypeStruct*>(table_type.get())) {
1029
5.62k
            const auto& struct_column = assert_cast<const ColumnStruct&>(**column);
1030
5.62k
            Columns columns = struct_column.get_columns_copy();
1031
5.62k
            DORIS_CHECK(columns.size() == struct_type->get_elements().size());
1032
19.0k
            for (size_t i = 0; i < columns.size(); ++i) {
1033
13.3k
                RETURN_IF_ERROR(
1034
13.3k
                        _align_column_nullability(&columns[i], struct_type->get_element(i)));
1035
13.3k
            }
1036
5.62k
            *column = ColumnStruct::create(columns);
1037
5.62k
            return Status::OK();
1038
5.62k
        }
1039
41.4k
        return Status::OK();
1040
47.0k
    }
1041
1042
    static Status _execute_default_expr_without_root_type_check(
1043
            const VExprContextSPtr& default_expr, const Block* block,
1044
16.5k
            ColumnWithTypeAndName* result_data) {
1045
16.5k
        DORIS_CHECK(default_expr != nullptr);
1046
16.5k
        DORIS_CHECK(block != nullptr);
1047
16.5k
        DORIS_CHECK(result_data != nullptr);
1048
16.5k
        ColumnPtr result_column;
1049
16.5k
        Status st;
1050
16.5k
        RETURN_IF_CATCH_EXCEPTION({
1051
16.5k
            st = default_expr->root()->execute_column_impl(default_expr.get(), block, nullptr,
1052
16.5k
                                                           block->rows(), result_column);
1053
16.5k
        });
1054
16.5k
        RETURN_IF_ERROR(st);
1055
16.5k
        DORIS_CHECK(result_column.get() != nullptr);
1056
16.5k
        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
16.5k
        result_data->column = result_column;
1062
16.5k
        result_data->type = default_expr->execute_type(block);
1063
16.5k
        result_data->name = default_expr->expr_name();
1064
16.5k
        return Status::OK();
1065
16.5k
    }
1066
1067
    Status _cast_column_to_type(ColumnPtr* column, const DataTypePtr& file_type,
1068
                                const DataTypePtr& table_type,
1069
8.77k
                                const std::string& column_name) const {
1070
8.77k
        DORIS_CHECK(column != nullptr);
1071
8.77k
        DORIS_CHECK(column->get() != nullptr);
1072
8.77k
        DORIS_CHECK(file_type != nullptr);
1073
8.77k
        DORIS_CHECK(table_type != nullptr);
1074
8.77k
        if (file_type->equals(*table_type)) {
1075
0
            return Status::OK();
1076
0
        }
1077
1078
8.77k
        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
8.77k
        if ((*column)->is_nullable() && !input_type->is_nullable()) {
1082
0
            input_type = make_nullable(input_type);
1083
8.77k
        } else if (!(*column)->is_nullable() && input_type->is_nullable()) {
1084
1
            input_type = remove_nullable(input_type);
1085
1
        }
1086
8.77k
        Block cast_block;
1087
8.77k
        cast_block.insert({*column, input_type, column_name});
1088
8.77k
        auto slot_ref = VSlotRef::create_shared(0, 0, -1, input_type, column_name);
1089
8.77k
        auto cast_expr = Cast::create_shared(table_type);
1090
8.77k
        cast_expr->add_child(std::move(slot_ref));
1091
8.77k
        auto cast_ctx = VExprContext::create_shared(std::move(cast_expr));
1092
8.77k
        RowDescriptor row_desc;
1093
8.77k
        RETURN_IF_ERROR(cast_ctx->prepare(_runtime_state, row_desc));
1094
8.77k
        RETURN_IF_ERROR(cast_ctx->open(_runtime_state));
1095
8.77k
        ColumnPtr cast_column;
1096
8.77k
        RETURN_IF_ERROR(cast_ctx->execute(&cast_block, cast_column));
1097
8.77k
        *column = std::move(cast_column);
1098
8.77k
        return Status::OK();
1099
8.77k
    }
1100
1101
    Status _materialize_present_child_mapping_column(const ColumnMapping& mapping,
1102
                                                     const ColumnPtr& file_column,
1103
17.1k
                                                     const size_t rows, ColumnPtr* column) {
1104
17.1k
        DORIS_CHECK(column != nullptr);
1105
17.1k
        DORIS_CHECK(mapping.file_type != nullptr);
1106
17.1k
        DORIS_CHECK(mapping.table_type != nullptr);
1107
17.1k
        *column = file_column;
1108
17.1k
        if (!mapping.is_trivial) {
1109
10.7k
            if (!mapping.child_mappings.empty()) {
1110
1.99k
                RETURN_IF_ERROR(
1111
1.99k
                        _materialize_complex_mapping_column(mapping, *column, rows, column));
1112
8.77k
            } else {
1113
8.77k
                RETURN_IF_ERROR(_cast_column_to_type(column, mapping.file_type, mapping.table_type,
1114
8.77k
                                                     mapping.file_column_name));
1115
8.77k
            }
1116
10.7k
        }
1117
17.1k
        RETURN_IF_ERROR(_align_column_nullability(column, mapping.table_type));
1118
17.1k
        return Status::OK();
1119
17.1k
    }
1120
1121
    Status _materialize_mapping_column(const ColumnMapping& mapping, Block* current_block,
1122
633k
                                       const size_t rows, ColumnPtr* column) {
1123
633k
        if (!mapping.is_trivial && mapping.file_local_id.has_value() &&
1124
633k
            !mapping.child_mappings.empty()) {
1125
9.63k
            DCHECK(mapping.projection != nullptr);
1126
9.63k
            int res_id;
1127
9.63k
            auto st = mapping.projection->execute(current_block, &res_id);
1128
9.63k
            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
9.63k
            ColumnPtr result_column = current_block->get_by_position(res_id).column;
1136
9.63k
            RETURN_IF_ERROR(
1137
9.63k
                    _materialize_complex_mapping_column(mapping, result_column, rows, column));
1138
9.63k
            return Status::OK();
1139
9.63k
        }
1140
623k
        if (mapping.projection != nullptr) {
1141
606k
            int res_id;
1142
606k
            auto st = mapping.projection->execute(current_block, &res_id);
1143
606k
            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
606k
            ColumnPtr result_column = current_block->get_by_position(res_id).column;
1155
606k
            *column = _detach_column(std::move(result_column));
1156
606k
            return Status::OK();
1157
606k
        }
1158
17.2k
        if (mapping.default_expr != nullptr) {
1159
16.5k
            if (current_block->rows() == rows) {
1160
14.5k
                ColumnWithTypeAndName result;
1161
14.5k
                RETURN_IF_ERROR(_execute_default_expr_without_root_type_check(
1162
14.5k
                        mapping.default_expr, current_block, &result));
1163
14.5k
                ColumnPtr result_column = result.column;
1164
14.5k
                RETURN_IF_ERROR(_align_column_nullability(&result_column, mapping.table_type));
1165
14.5k
                *column = _detach_column(std::move(result_column));
1166
14.5k
            } else {
1167
1.99k
                DORIS_CHECK(mapping.constant_index.has_value());
1168
1.99k
                Block eval_block;
1169
1.99k
                eval_block.insert({mapping.table_type->create_column_const_with_default_value(rows),
1170
1.99k
                                   mapping.table_type, "__table_reader_const_rows"});
1171
1.99k
                ColumnWithTypeAndName result;
1172
1.99k
                RETURN_IF_ERROR(_execute_default_expr_without_root_type_check(
1173
1.99k
                        mapping.default_expr, &eval_block, &result));
1174
1.99k
                ColumnPtr result_column = result.column;
1175
1.99k
                RETURN_IF_ERROR(_align_column_nullability(&result_column, mapping.table_type));
1176
1.99k
                *column = _detach_column(std::move(result_column));
1177
1.99k
            }
1178
16.5k
            return Status::OK();
1179
16.5k
        }
1180
709
        ColumnPtr result_column = mapping.table_type->create_column_const_with_default_value(rows);
1181
709
        *column = _detach_column(std::move(result_column));
1182
709
        return Status::OK();
1183
17.2k
    }
1184
1185
    Status _materialize_complex_mapping_column(const ColumnMapping& mapping,
1186
                                               const ColumnPtr& file_column, const size_t rows,
1187
11.6k
                                               ColumnPtr* column) {
1188
11.6k
        DORIS_CHECK(mapping.table_type != nullptr);
1189
11.6k
        DORIS_CHECK(file_column.get() != nullptr);
1190
11.6k
        const auto table_type = remove_nullable(mapping.table_type);
1191
11.6k
        switch (table_type->get_primitive_type()) {
1192
3.62k
        case TYPE_STRUCT:
1193
3.62k
            RETURN_IF_ERROR(_materialize_struct_mapping_column(mapping, file_column, rows, column));
1194
3.62k
            break;
1195
4.44k
        case TYPE_ARRAY:
1196
4.44k
            RETURN_IF_ERROR(_materialize_array_mapping_column(mapping, file_column, rows, column));
1197
4.44k
            break;
1198
4.44k
        case TYPE_MAP:
1199
3.55k
            RETURN_IF_ERROR(_materialize_map_mapping_column(mapping, file_column, rows, column));
1200
3.55k
            break;
1201
3.55k
        default:
1202
0
            *column = _detach_column(file_column);
1203
0
            break;
1204
11.6k
        }
1205
11.6k
        return Status::OK();
1206
11.6k
    }
1207
1208
    static std::vector<const ColumnMapping*> _present_child_mappings_in_file_order(
1209
3.62k
            const std::vector<ColumnMapping>& child_mappings) {
1210
3.62k
        std::vector<const ColumnMapping*> result;
1211
3.62k
        result.reserve(child_mappings.size());
1212
9.19k
        for (const auto& child_mapping : child_mappings) {
1213
9.19k
            if (child_mapping.file_local_id.has_value()) {
1214
5.57k
                result.push_back(&child_mapping);
1215
5.57k
            }
1216
9.19k
        }
1217
4.05k
        std::ranges::sort(result, [](const ColumnMapping* lhs, const ColumnMapping* rhs) {
1218
4.05k
            DORIS_CHECK(lhs->file_local_id.has_value());
1219
4.05k
            DORIS_CHECK(rhs->file_local_id.has_value());
1220
4.05k
            return *lhs->file_local_id < *rhs->file_local_id;
1221
4.05k
        });
1222
3.62k
        return result;
1223
3.62k
    }
1224
1225
    static size_t _file_child_ordinal_for_mapping(
1226
            const ColumnMapping& mapping, const ColumnMapping& child_mapping,
1227
5.57k
            const std::vector<const ColumnMapping*>& file_ordered_children) {
1228
5.57k
        DORIS_CHECK(child_mapping.file_local_id.has_value());
1229
5.57k
        if (!mapping.projected_file_children.empty()) {
1230
5.57k
            const auto child_it = std::ranges::find_if(
1231
8.57k
                    mapping.projected_file_children, [&](const ColumnDefinition& file_child) {
1232
8.57k
                        return file_child.file_local_id() == *child_mapping.file_local_id;
1233
8.57k
                    });
1234
5.57k
            DORIS_CHECK(child_it != mapping.projected_file_children.end());
1235
5.57k
            return static_cast<size_t>(
1236
5.57k
                    std::distance(mapping.projected_file_children.begin(), child_it));
1237
5.57k
        }
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
5.57k
    }
1242
1243
    static std::vector<const ColumnMapping*> _child_mappings_in_table_type_order(
1244
3.62k
            const ColumnMapping& mapping, const DataTypeStruct& table_type) {
1245
3.62k
        std::vector<const ColumnMapping*> result;
1246
3.62k
        result.reserve(mapping.child_mappings.size());
1247
12.8k
        for (size_t child_idx = 0; child_idx < table_type.get_elements().size(); ++child_idx) {
1248
9.19k
            const auto& child_name = table_type.get_element_name(child_idx);
1249
9.19k
            const auto child_it = std::ranges::find_if(
1250
17.6k
                    mapping.child_mappings, [&](const ColumnMapping& child_mapping) {
1251
17.6k
                        return child_mapping.table_column_name == child_name;
1252
17.6k
                    });
1253
9.19k
            DORIS_CHECK(child_it != mapping.child_mappings.end())
1254
0
                    << mapping.debug_string() << ", table_child_name=" << child_name;
1255
9.19k
            result.push_back(&*child_it);
1256
9.19k
        }
1257
3.62k
        return result;
1258
3.62k
    }
1259
1260
    static const IColumn* _nested_column_if_nullable(const ColumnPtr& column,
1261
11.6k
                                                     const NullMap** null_map) {
1262
11.6k
        DORIS_CHECK(column.get() != nullptr);
1263
11.6k
        if (const auto* nullable_column = check_and_get_column<ColumnNullable>(*column)) {
1264
11.6k
            if (null_map != nullptr) {
1265
11.6k
                *null_map = &nullable_column->get_null_map_data();
1266
11.6k
            }
1267
11.6k
            return &nullable_column->get_nested_column();
1268
11.6k
        }
1269
4
        return column.get();
1270
11.6k
    }
1271
1272
    Status _materialize_struct_mapping_column(const ColumnMapping& mapping,
1273
                                              const ColumnPtr& file_column, const size_t rows,
1274
3.62k
                                              ColumnPtr* column) {
1275
3.62k
        DORIS_CHECK(mapping.table_type != nullptr);
1276
3.62k
        const auto* table_type =
1277
3.62k
                assert_cast<const DataTypeStruct*>(remove_nullable(mapping.table_type).get());
1278
3.62k
        const auto full_file_column = file_column->convert_to_full_column_if_const();
1279
3.62k
        const NullMap* parent_null_map = nullptr;
1280
3.62k
        const auto* nested_file_column =
1281
3.62k
                _nested_column_if_nullable(full_file_column, &parent_null_map);
1282
3.62k
        const auto* file_struct = assert_cast<const ColumnStruct*>(nested_file_column);
1283
3.62k
        DORIS_CHECK(table_type->get_elements().size() == mapping.child_mappings.size());
1284
1285
3.62k
        Columns child_columns;
1286
3.62k
        child_columns.reserve(mapping.child_mappings.size());
1287
3.62k
        const auto file_ordered_children =
1288
3.62k
                _present_child_mappings_in_file_order(mapping.child_mappings);
1289
3.62k
        const auto table_ordered_children =
1290
3.62k
                _child_mappings_in_table_type_order(mapping, *table_type);
1291
9.18k
        for (const auto* child_mapping : table_ordered_children) {
1292
9.18k
            DORIS_CHECK(child_mapping != nullptr);
1293
9.18k
            if (!child_mapping->file_local_id.has_value()) {
1294
3.61k
                child_columns.push_back(
1295
3.61k
                        child_mapping->table_type->create_column_const_with_default_value(rows)
1296
3.61k
                                ->convert_to_full_column_if_const());
1297
3.61k
                continue;
1298
3.61k
            }
1299
5.57k
            const auto file_child_idx =
1300
5.57k
                    _file_child_ordinal_for_mapping(mapping, *child_mapping, file_ordered_children);
1301
5.57k
            DORIS_CHECK(file_child_idx < file_struct->get_columns().size());
1302
5.57k
            ColumnPtr child_column = file_struct->get_column_ptr(file_child_idx);
1303
5.57k
            RETURN_IF_ERROR(_materialize_present_child_mapping_column(*child_mapping, child_column,
1304
5.57k
                                                                      rows, &child_column));
1305
5.57k
            child_columns.push_back(std::move(child_column));
1306
5.57k
        }
1307
3.62k
        MutableColumns mutable_child_columns;
1308
3.62k
        mutable_child_columns.reserve(child_columns.size());
1309
9.19k
        for (auto& child_column : child_columns) {
1310
9.19k
            mutable_child_columns.push_back(IColumn::mutate(std::move(child_column)));
1311
9.19k
        }
1312
3.62k
        auto result = ColumnStruct::create(std::move(mutable_child_columns));
1313
3.62k
        if (mapping.table_type->is_nullable()) {
1314
3.62k
            auto null_map = ColumnUInt8::create();
1315
3.62k
            auto& null_map_data = null_map->get_data();
1316
3.62k
            null_map_data.resize(rows);
1317
3.62k
            if (parent_null_map != nullptr) {
1318
3.62k
                DORIS_CHECK(parent_null_map->size() == rows);
1319
3.62k
                null_map_data.assign(parent_null_map->begin(), parent_null_map->end());
1320
3.62k
            } else {
1321
0
                std::fill(null_map_data.begin(), null_map_data.end(), 0);
1322
0
            }
1323
3.62k
            *column = ColumnNullable::create(std::move(result), std::move(null_map));
1324
3.62k
        } else {
1325
2
            *column = std::move(result);
1326
2
        }
1327
3.62k
        return Status::OK();
1328
3.62k
    }
1329
1330
    Status _materialize_array_mapping_column(const ColumnMapping& mapping,
1331
                                             const ColumnPtr& file_column, const size_t rows,
1332
4.44k
                                             ColumnPtr* column) {
1333
4.44k
        DORIS_CHECK(mapping.child_mappings.size() == 1);
1334
4.44k
        const auto full_file_column = file_column->convert_to_full_column_if_const();
1335
4.44k
        const NullMap* parent_null_map = nullptr;
1336
4.44k
        const auto* nested_file_column =
1337
4.44k
                _nested_column_if_nullable(full_file_column, &parent_null_map);
1338
4.44k
        const auto* file_array = assert_cast<const ColumnArray*>(nested_file_column);
1339
4.44k
        ColumnPtr nested_column = file_array->get_data_ptr();
1340
4.44k
        const auto& element_mapping = mapping.child_mappings[0];
1341
4.44k
        RETURN_IF_ERROR(_materialize_present_child_mapping_column(
1342
4.44k
                element_mapping, nested_column, nested_column->size(), &nested_column));
1343
4.44k
        auto offsets_column = file_array->get_offsets_ptr()->convert_to_full_column_if_const();
1344
4.44k
        auto result = ColumnArray::create(IColumn::mutate(std::move(nested_column)),
1345
4.44k
                                          IColumn::mutate(std::move(offsets_column)));
1346
4.44k
        if (mapping.table_type->is_nullable()) {
1347
4.44k
            auto null_map = ColumnUInt8::create();
1348
4.44k
            auto& null_map_data = null_map->get_data();
1349
4.44k
            null_map_data.resize(rows);
1350
4.44k
            if (parent_null_map != nullptr) {
1351
4.44k
                DORIS_CHECK(parent_null_map->size() == rows);
1352
4.44k
                null_map_data.assign(parent_null_map->begin(), parent_null_map->end());
1353
4.44k
            } else {
1354
0
                std::fill(null_map_data.begin(), null_map_data.end(), 0);
1355
0
            }
1356
4.44k
            *column = ColumnNullable::create(std::move(result), std::move(null_map));
1357
4.44k
        } else {
1358
0
            *column = std::move(result);
1359
0
        }
1360
4.44k
        return Status::OK();
1361
4.44k
    }
1362
1363
    Status _materialize_map_mapping_column(const ColumnMapping& mapping,
1364
                                           const ColumnPtr& file_column, const size_t rows,
1365
3.56k
                                           ColumnPtr* column) {
1366
3.56k
        const auto full_file_column = file_column->convert_to_full_column_if_const();
1367
3.56k
        const NullMap* parent_null_map = nullptr;
1368
3.56k
        const auto* nested_file_column =
1369
3.56k
                _nested_column_if_nullable(full_file_column, &parent_null_map);
1370
3.56k
        const auto* file_map = assert_cast<const ColumnMap*>(nested_file_column);
1371
3.56k
        ColumnPtr key_column = file_map->get_keys_ptr();
1372
3.56k
        ColumnPtr value_column = file_map->get_values_ptr();
1373
1374
3.56k
        const ColumnMapping* key_mapping = nullptr;
1375
3.56k
        const ColumnMapping* value_mapping = nullptr;
1376
7.12k
        for (const auto& child_mapping : mapping.child_mappings) {
1377
7.12k
            if (!child_mapping.file_local_id.has_value()) {
1378
0
                continue;
1379
0
            }
1380
7.12k
            if (*child_mapping.file_local_id == 0) {
1381
3.56k
                key_mapping = &child_mapping;
1382
3.56k
            } else if (*child_mapping.file_local_id == 1) {
1383
3.56k
                value_mapping = &child_mapping;
1384
3.56k
            }
1385
7.12k
        }
1386
1387
3.56k
        if (key_mapping != nullptr) {
1388
3.56k
            RETURN_IF_ERROR(_materialize_present_child_mapping_column(
1389
3.56k
                    *key_mapping, key_column, key_column->size(), &key_column));
1390
3.56k
        }
1391
3.56k
        if (value_mapping != nullptr) {
1392
3.56k
            RETURN_IF_ERROR(_materialize_present_child_mapping_column(
1393
3.56k
                    *value_mapping, value_column, value_column->size(), &value_column));
1394
3.56k
        }
1395
3.56k
        auto offsets_column = file_map->get_offsets_ptr()->convert_to_full_column_if_const();
1396
3.56k
        auto result = ColumnMap::create(IColumn::mutate(std::move(key_column)),
1397
3.56k
                                        IColumn::mutate(std::move(value_column)),
1398
3.56k
                                        IColumn::mutate(std::move(offsets_column)));
1399
3.56k
        if (mapping.table_type->is_nullable()) {
1400
3.55k
            auto null_map = ColumnUInt8::create();
1401
3.55k
            auto& null_map_data = null_map->get_data();
1402
3.55k
            null_map_data.resize(rows);
1403
3.55k
            if (parent_null_map != nullptr) {
1404
3.55k
                DORIS_CHECK(parent_null_map->size() == rows);
1405
3.55k
                null_map_data.assign(parent_null_map->begin(), parent_null_map->end());
1406
3.55k
            } else {
1407
0
                std::fill(null_map_data.begin(), null_map_data.end(), 0);
1408
0
            }
1409
3.55k
            *column = ColumnNullable::create(std::move(result), std::move(null_map));
1410
3.55k
        } else {
1411
2
            *column = std::move(result);
1412
2
        }
1413
3.56k
        return Status::OK();
1414
3.56k
    }
1415
1416
58.7k
    Status _open_mapping_exprs() {
1417
58.7k
        RowDescriptor row_desc;
1418
395k
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
1419
395k
            if (mapping.projection != nullptr) {
1420
377k
                RETURN_IF_ERROR(mapping.projection->prepare(_runtime_state, row_desc));
1421
377k
                RETURN_IF_ERROR(mapping.projection->open(_runtime_state));
1422
377k
            }
1423
395k
            if (mapping.default_expr != nullptr) {
1424
17.0k
                RETURN_IF_ERROR(mapping.default_expr->prepare(_runtime_state, row_desc));
1425
17.0k
                RETURN_IF_ERROR(mapping.default_expr->open(_runtime_state));
1426
17.0k
            }
1427
395k
        }
1428
58.7k
        return Status::OK();
1429
58.7k
    }
1430
1431
    Status _build_file_aggregate_request(TPushAggOp::type agg_type,
1432
1.28k
                                         FileAggregateRequest* request) const {
1433
1.28k
        DORIS_CHECK(request != nullptr);
1434
1.28k
        DORIS_CHECK(_supports_aggregate_pushdown(agg_type));
1435
1.28k
        request->agg_type = agg_type;
1436
1.28k
        request->columns.clear();
1437
1.28k
        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
1.25k
            if (_data_reader.column_mapper->mappings().size() == 1) {
1446
1.25k
                const auto& mapping = _data_reader.column_mapper->mappings()[0];
1447
1.25k
                if (mapping.file_local_id.has_value() && mapping.file_type != nullptr &&
1448
1.25k
                    is_complex_type(remove_nullable(mapping.file_type)->get_primitive_type()) &&
1449
1.25k
                    mapping.virtual_column_type == TableVirtualColumnType::INVALID &&
1450
1.25k
                    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
1.25k
            }
1457
1.25k
            return Status::OK();
1458
1.25k
        }
1459
26
        request->columns.reserve(_data_reader.column_mapper->mappings().size());
1460
47
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
1461
47
            DORIS_CHECK(mapping.file_local_id.has_value());
1462
47
            FileAggregateRequest::Column column;
1463
47
            column.projection = LocalColumnIndex::top_level(LocalColumnId(*mapping.file_local_id));
1464
47
            if (!mapping.child_mappings.empty()) {
1465
1
                RETURN_IF_ERROR(build_aggregate_projection(mapping, &column.projection));
1466
1
            }
1467
47
            request->columns.push_back(std::move(column));
1468
47
        }
1469
26
        return Status::OK();
1470
26
    }
1471
1472
    Status _materialize_aggregate_pushdown_rows(TPushAggOp::type agg_type,
1473
                                                const FileAggregateResult& file_result,
1474
1.27k
                                                Block* block) {
1475
1.27k
        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
1.25k
            DORIS_CHECK(file_result.count >= 0);
1480
1.25k
            return _materialize_count_rows(cast_set<size_t>(file_result.count), block);
1481
1.25k
        }
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
21
        DORIS_CHECK(file_result.columns.size() == _data_reader.column_mapper->mappings().size());
1485
21
        DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
1486
21
        Block file_block;
1487
21
        file_block.reserve(_data_reader.file_block_layout.size());
1488
26
        for (const auto& column : _data_reader.file_block_layout) {
1489
26
            file_block.insert({column.type->create_column(), column.type, column.name});
1490
26
        }
1491
47
        for (size_t column_idx = 0; column_idx < file_result.columns.size(); ++column_idx) {
1492
26
            const auto& result_column = file_result.columns[column_idx];
1493
26
            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
26
            bool found_file_column = false;
1498
33
            for (size_t block_position = 0; block_position < _data_reader.file_block_layout.size();
1499
33
                 ++block_position) {
1500
33
                if (_data_reader.file_block_layout[block_position].file_column_id ==
1501
33
                    file_result.columns[column_idx].projection.column_id()) {
1502
26
                    found_file_column = true;
1503
26
                    auto column = file_block.get_by_position(block_position)
1504
26
                                          .type->create_column()
1505
26
                                          ->assert_mutable();
1506
26
                    RETURN_IF_ERROR(_insert_aggregate_projection_value(
1507
26
                            file_result.columns[column_idx].projection, result_column.min_value,
1508
26
                            column.get()));
1509
26
                    RETURN_IF_ERROR(_insert_aggregate_projection_value(
1510
26
                            file_result.columns[column_idx].projection, result_column.max_value,
1511
26
                            column.get()));
1512
26
                    file_block.replace_by_position(block_position, std::move(column));
1513
26
                    break;
1514
26
                }
1515
33
            }
1516
26
            DORIS_CHECK(found_file_column);
1517
26
        }
1518
47
        for (size_t column_idx = 0; column_idx < _data_reader.column_mapper->mappings().size();
1519
26
             ++column_idx) {
1520
26
            ColumnPtr table_column;
1521
26
            RETURN_IF_ERROR(
1522
26
                    _materialize_mapping_column(_data_reader.column_mapper->mappings()[column_idx],
1523
26
                                                &file_block, 2, &table_column));
1524
26
            block->replace_by_position(column_idx, std::move(table_column));
1525
26
        }
1526
21
        return Status::OK();
1527
21
    }
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
389k
            const std::vector<ColumnDefinition>& schema, LocalColumnId column_id) {
1595
9.45M
        for (const auto& field : schema) {
1596
9.45M
            if (field.file_local_id() == column_id.value()) {
1597
383k
                return &field;
1598
383k
            }
1599
9.45M
        }
1600
5.81k
        return nullptr;
1601
389k
    }
1602
1603
318
    static bool _can_push_down_minmax_for_mapping(const ColumnMapping& mapping) {
1604
318
        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
315
            return mapping.is_trivial;
1609
315
        }
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
108
                                                     const Field& value, IColumn* column) {
1652
108
        DORIS_CHECK(column != nullptr);
1653
108
        if (auto* nullable_column = check_and_get_column<ColumnNullable>(*column)) {
1654
54
            RETURN_IF_ERROR(_insert_aggregate_projection_value(
1655
54
                    projection, value, &nullable_column->get_nested_column()));
1656
54
            nullable_column->get_null_map_data().push_back(0);
1657
54
            return Status::OK();
1658
54
        }
1659
54
        if (projection.project_all_children || projection.children.empty()) {
1660
52
            column->insert(value);
1661
52
            return Status::OK();
1662
52
        }
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