Coverage Report

Created: 2026-07-20 04:02

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