Coverage Report

Created: 2026-07-12 20:28

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