Coverage Report

Created: 2026-07-17 15:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format_v2/table_reader.h
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#pragma once
19
20
#include <bvar/status.h>
21
22
#include <algorithm>
23
#include <exception>
24
#include <map>
25
#include <memory>
26
#include <optional>
27
#include <string>
28
#include <string_view>
29
#include <utility>
30
#include <vector>
31
32
#include "common/cast_set.h"
33
#include "common/exception.h"
34
#include "common/logging.h"
35
#include "common/status.h"
36
#include "core/assert_cast.h"
37
#include "core/block/block.h"
38
#include "core/column/column_array.h"
39
#include "core/column/column_const.h"
40
#include "core/column/column_map.h"
41
#include "core/column/column_nullable.h"
42
#include "core/column/column_struct.h"
43
#include "core/column/column_vector.h"
44
#include "core/data_type/data_type.h"
45
#include "core/data_type/data_type_array.h"
46
#include "core/data_type/data_type_map.h"
47
#include "core/data_type/data_type_nullable.h"
48
#include "core/data_type/data_type_number.h"
49
#include "core/data_type/data_type_string.h"
50
#include "core/data_type/data_type_struct.h"
51
#include "core/field.h"
52
#include "exec/common/stringop_substring.h"
53
#include "exprs/vexpr.h"
54
#include "exprs/vexpr_context.h"
55
#include "exprs/vexpr_fwd.h"
56
#include "exprs/vslot_ref.h"
57
#include "format/table/deletion_vector.h"
58
#include "format_v2/column_data.h"
59
#include "format_v2/column_mapper.h"
60
#include "format_v2/expr/cast.h"
61
#include "format_v2/expr/delete_predicate.h"
62
#include "format_v2/file_reader.h"
63
#include "format_v2/parquet/reader/column_reader.h"
64
#include "format_v2/schema_projection.h"
65
#include "gen_cpp/PlanNodes_types.h"
66
#include "io/io_common.h"
67
#include "runtime/descriptors.h"
68
#include "storage/segment/condition_cache.h"
69
70
namespace doris {
71
class Block;
72
struct DeleteFileDesc;
73
class RuntimeState;
74
} // namespace doris
75
76
namespace doris::format {
77
78
using DeleteRows = std::vector<int64_t>;
79
80
// Row-level predicates on table/global schema. They are rewritten to file-local expressions when
81
// possible, and remain the source of row-level filtering after localization.
82
struct TableFilter {
83
    VExprContextSPtr conjunct;
84
    std::vector<GlobalIndex> global_indices;
85
};
86
87
struct ScanTask {
88
60.0k
    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.2k
    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
233k
    virtual void set_batch_size(size_t batch_size) {
186
233k
        _batch_size = std::max<size_t>(1, batch_size);
187
233k
        if (_data_reader.reader != nullptr) {
188
117k
            _data_reader.reader->set_batch_size(_batch_size);
189
117k
        }
190
233k
    }
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
86.2k
    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
179k
    virtual Status get_block(Block* block, bool* eos) {
231
179k
        SCOPED_TIMER(_profile.exec_timer);
232
179k
        DORIS_CHECK(block->columns() == _projected_columns.size());
233
179k
        block->clear_column_data(_projected_columns.size());
234
235
247k
        while (true) {
236
247k
            if (*eos) {
237
0
                return Status::OK();
238
0
            }
239
247k
            if (_io_ctx != nullptr && _io_ctx->should_stop) {
240
4
                *eos = true;
241
4
                return Status::OK();
242
4
            }
243
247k
            if (!_data_reader.reader) {
244
118k
                if (_is_table_level_count_active()) {
245
298
                    RETURN_IF_ERROR(_read_table_level_count(block, eos));
246
298
                    return Status::OK();
247
298
                }
248
117k
                RETURN_IF_ERROR(create_next_reader(eos));
249
117k
                if (!_data_reader.reader) {
250
59.2k
                    DCHECK(*eos);
251
59.2k
                    return Status::OK();
252
59.2k
                }
253
117k
            }
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
187k
            if (!_aggregate_pushdown_tried) {
260
58.6k
                SCOPED_TIMER(_profile.pushdown_agg_timer);
261
58.6k
                bool pushed_down = false;
262
58.6k
                const auto status = _try_materialize_aggregate_pushdown_rows(block, &pushed_down);
263
58.6k
                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
58.6k
                if (pushed_down) {
272
1.23k
                    return Status::OK();
273
1.23k
                }
274
58.6k
            }
275
276
186k
            bool current_eof = false;
277
186k
            _data_reader.block_template.clear_column_data(
278
186k
                    cast_set<int64_t>(_data_reader.file_block_layout.size()));
279
186k
            size_t current_rows = 0;
280
186k
            RETURN_IF_ERROR(_data_reader.reader->get_block(&_data_reader.block_template,
281
186k
                                                           &current_rows, &current_eof));
282
186k
            const bool stopped_during_read = _io_ctx != nullptr && _io_ctx->should_stop;
283
186k
            if (current_rows == 0) {
284
67.9k
                if (current_eof) {
285
56.7k
                    _current_reader_reached_eof = !stopped_during_read;
286
56.7k
                    RETURN_IF_ERROR(close_current_reader());
287
56.7k
                }
288
67.9k
                continue;
289
67.9k
            }
290
186k
            DCHECK_EQ(_data_reader.block_template.columns(), _data_reader.file_block_layout.size())
291
0
                    << _data_reader.block_template.dump_structure();
292
118k
#ifndef NDEBUG
293
118k
            RETURN_IF_ERROR(_check_file_block_columns("after file reader get_block", current_rows));
294
118k
#endif
295
118k
            DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
296
118k
            RETURN_IF_ERROR(finalize_chunk(block, current_rows));
297
118k
#ifndef NDEBUG
298
118k
            RETURN_IF_ERROR(
299
118k
                    _check_table_block_columns("after finalize_chunk", block, current_rows));
300
118k
#endif
301
118k
            if (current_eof) {
302
15
                _current_reader_reached_eof = !stopped_during_read;
303
15
                RETURN_IF_ERROR(close_current_reader());
304
15
            }
305
118k
            return Status::OK();
306
118k
        }
307
179k
    }
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.2k
    virtual Status close() {
312
39.2k
        if (_data_reader.reader) {
313
636
            RETURN_IF_ERROR(close_current_reader());
314
636
        }
315
39.2k
        _current_task.reset();
316
39.2k
        _current_file_description.reset();
317
39.2k
        _remaining_table_level_count = -1;
318
39.2k
        return Status::OK();
319
39.2k
    }
320
321
78.1k
    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.2k
    virtual Status validate_projected_columns(const ProjectedColumnBuildContext& context) const {
330
19.2k
        (void)context;
331
19.2k
        return Status::OK();
332
19.2k
    }
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
34.0k
                                               DeleteFileDesc* desc, bool* has_delete_file) {
354
34.0k
        *has_delete_file = false;
355
34.0k
        return Status::OK();
356
34.0k
    }
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.43k
    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
59.8k
    virtual Status open_reader() {
370
59.8k
        SCOPED_TIMER(_profile.open_reader_timer);
371
        // 1. Get file schema and create column mapping.
372
59.8k
        std::vector<ColumnDefinition> file_schema;
373
59.8k
        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
59.8k
        RETURN_IF_ERROR(annotate_file_schema(&file_schema));
377
59.8k
        _data_reader.file_schema = file_schema;
378
59.8k
        _mapper_options.mode = mapping_mode();
379
380
59.8k
        _data_reader.column_mapper = _data_reader.reader->create_column_mapper(_mapper_options);
381
59.8k
        DORIS_CHECK(_data_reader.column_mapper != nullptr);
382
59.8k
        RETURN_IF_ERROR(_data_reader.column_mapper->create_mapping(_projected_columns,
383
59.8k
                                                                   _partition_values, file_schema));
384
59.8k
        DORIS_CHECK(_data_reader.column_mapper->mappings().size() == _projected_columns.size());
385
386
        // 2. Build table filters based on conjuncts and column predicates.
387
59.8k
        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
59.8k
        auto file_request = std::make_shared<FileScanRequest>();
393
59.8k
        RETURN_IF_ERROR(_data_reader.column_mapper->create_scan_request(
394
59.8k
                _table_filters, _projected_columns, file_request.get(), _runtime_state));
395
59.8k
        bool constant_filter_pruned_split = false;
396
59.8k
        RETURN_IF_ERROR(_evaluate_constant_filters(&constant_filter_pruned_split));
397
59.8k
        if (constant_filter_pruned_split) {
398
1.24k
            RETURN_IF_ERROR(close_current_reader());
399
1.24k
            return Status::OK();
400
1.24k
        }
401
58.5k
        RETURN_IF_ERROR(customize_file_scan_request(file_request.get()));
402
58.5k
        RETURN_IF_ERROR(_open_local_filter_exprs(*file_request));
403
58.5k
        _data_reader.file_block_layout.clear();
404
58.5k
        _data_reader.block_template.clear();
405
58.5k
        _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
370k
        for (const auto& [file_column_id, block_position] : file_request->local_positions) {
410
370k
            DORIS_CHECK(block_position.value() < _data_reader.file_block_layout.size());
411
370k
            const auto* field = _find_column_definition(_data_reader.file_schema, file_column_id);
412
370k
            DORIS_CHECK(field != nullptr);
413
414
370k
            ColumnDefinition projected_field;
415
370k
            {
416
370k
                auto it = std::find_if(
417
370k
                        file_request->non_predicate_columns.begin(),
418
370k
                        file_request->non_predicate_columns.end(),
419
7.55M
                        [&](const LocalColumnIndex& p) { return p.column_id() == file_column_id; });
420
370k
                if (it != file_request->non_predicate_columns.end()) {
421
337k
                    RETURN_IF_ERROR(project_column_definition(*field, *it, &projected_field));
422
337k
                }
423
370k
            }
424
370k
            {
425
370k
                auto it = std::find_if(
426
370k
                        file_request->predicate_columns.begin(),
427
370k
                        file_request->predicate_columns.end(),
428
370k
                        [&](const LocalColumnIndex& p) { return p.column_id() == file_column_id; });
429
370k
                if (it != file_request->predicate_columns.end()) {
430
32.7k
                    RETURN_IF_ERROR(project_column_definition(*field, *it, &projected_field));
431
32.7k
                }
432
370k
            }
433
370k
            _data_reader.file_block_layout[block_position.value()] = {
434
370k
                    .file_column_id = file_column_id,
435
370k
                    .name = projected_field.name,
436
370k
                    .type = projected_field.type,
437
370k
            };
438
370k
            DORIS_CHECK(_data_reader.file_block_layout[block_position.value()].type != nullptr);
439
370k
        }
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
58.5k
        _data_reader.block_template.reserve(_data_reader.file_block_layout.size());
444
370k
        for (const auto& column : _data_reader.file_block_layout) {
445
370k
            _data_reader.block_template.insert(
446
370k
                    {column.type->create_column(), column.type, column.name});
447
370k
        }
448
58.5k
        if (VLOG_DEBUG_IS_ON) {
449
0
            VLOG_DEBUG << "TableReader debug: " << debug_string();
450
0
        }
451
58.5k
        RETURN_IF_ERROR(_open_mapping_exprs());
452
58.5k
        RETURN_IF_ERROR(_data_reader.reader->open(file_request));
453
58.5k
        RETURN_IF_ERROR(_init_reader_condition_cache(*file_request));
454
58.5k
        return Status::OK();
455
58.5k
    }
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
59.7k
    Status _evaluate_constant_filters(bool* can_filter_all) {
468
59.7k
        DORIS_CHECK(can_filter_all != nullptr);
469
59.7k
        DORIS_CHECK_LE(_constant_pruning_safe_filter_count, _table_filters.size());
470
59.7k
        *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
98.3k
        for (size_t i = 0; i < _constant_pruning_safe_filter_count; ++i) {
475
39.8k
            const auto& table_filter = _table_filters[i];
476
39.8k
            if (table_filter.conjunct == nullptr) {
477
0
                continue;
478
0
            }
479
39.8k
            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
39.8k
            if (table_filter.conjunct->root()->is_rf_wrapper() ||
485
39.8k
                !_table_filter_has_only_constant_entries(table_filter)) {
486
36.3k
                continue;
487
36.3k
            }
488
3.43k
            Block eval_block;
489
3.43k
            RETURN_IF_ERROR(_build_constant_filter_block(table_filter, &eval_block));
490
3.43k
            RowDescriptor row_desc;
491
3.43k
            RETURN_IF_ERROR(table_filter.conjunct->prepare(_runtime_state, row_desc));
492
3.43k
            RETURN_IF_ERROR(table_filter.conjunct->open(_runtime_state));
493
3.43k
            int result_column_id = -1;
494
3.43k
            RETURN_IF_ERROR(table_filter.conjunct->execute(&eval_block, &result_column_id));
495
3.43k
            DORIS_CHECK(result_column_id >= 0);
496
3.43k
            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.43k
        }
501
58.5k
        return Status::OK();
502
59.7k
    }
503
504
34.7k
    bool _table_filter_has_only_constant_entries(const TableFilter& table_filter) const {
505
34.7k
        const auto& filter_entries = _data_reader.column_mapper->filter_entries();
506
34.9k
        for (const auto global_index : table_filter.global_indices) {
507
34.9k
            const auto entry_it = filter_entries.find(global_index);
508
35.0k
            if (entry_it == filter_entries.end() || !entry_it->second.is_constant()) {
509
31.3k
                return false;
510
31.3k
            }
511
34.9k
        }
512
3.43k
        return !table_filter.global_indices.empty();
513
34.7k
    }
514
515
3.42k
    Status _build_constant_filter_block(const TableFilter& table_filter, Block* eval_block) {
516
3.42k
        DORIS_CHECK(eval_block != nullptr);
517
3.42k
        eval_block->clear();
518
3.42k
        const auto& mappings = _data_reader.column_mapper->mappings();
519
3.42k
        const auto& filter_entries = _data_reader.column_mapper->filter_entries();
520
3.42k
        DORIS_CHECK(mappings.size() == _projected_columns.size());
521
14.7k
        for (size_t column_idx = 0; column_idx < mappings.size(); ++column_idx) {
522
11.2k
            const auto global_index = GlobalIndex(column_idx);
523
11.2k
            const auto& mapping = mappings[column_idx];
524
11.2k
            const auto entry_it = filter_entries.find(global_index);
525
11.2k
            const bool referenced_by_filter =
526
11.2k
                    std::find(table_filter.global_indices.begin(),
527
11.2k
                              table_filter.global_indices.end(),
528
11.2k
                              global_index) != table_filter.global_indices.end();
529
11.2k
            if (referenced_by_filter && entry_it != filter_entries.end() &&
530
11.2k
                entry_it->second.is_constant()) {
531
3.67k
                ColumnPtr constant_column;
532
3.67k
                RETURN_IF_ERROR(_materialize_constant_filter_column(
533
3.67k
                        entry_it->second.constant_index(), &constant_column));
534
3.67k
                eval_block->insert({std::move(constant_column), mapping.table_type,
535
3.67k
                                    mapping.table_column_name});
536
7.60k
            } else {
537
7.60k
                eval_block->insert({mapping.table_type->create_column_const_with_default_value(1),
538
7.60k
                                    mapping.table_type, mapping.table_column_name});
539
7.60k
            }
540
11.2k
        }
541
3.42k
        return Status::OK();
542
3.42k
    }
543
544
3.67k
    Status _materialize_constant_filter_column(ConstantIndex constant_index, ColumnPtr* column) {
545
3.67k
        DORIS_CHECK(column != nullptr);
546
3.67k
        const auto& constant_entry = _data_reader.column_mapper->constant_map().get(constant_index);
547
3.67k
        DORIS_CHECK(constant_entry.expr != nullptr);
548
3.67k
        DORIS_CHECK(constant_entry.type != nullptr);
549
3.67k
        RowDescriptor row_desc;
550
3.67k
        RETURN_IF_ERROR(constant_entry.expr->prepare(_runtime_state, row_desc));
551
3.67k
        RETURN_IF_ERROR(constant_entry.expr->open(_runtime_state));
552
3.67k
        Block eval_block;
553
3.67k
        eval_block.insert({constant_entry.type->create_column_const_with_default_value(1),
554
3.67k
                           constant_entry.type, "__table_reader_constant_filter"});
555
3.67k
        int result_column_id = -1;
556
3.67k
        RETURN_IF_ERROR(constant_entry.expr->execute(&eval_block, &result_column_id));
557
3.67k
        DORIS_CHECK(result_column_id >= 0);
558
3.67k
        *column = eval_block.get_by_position(result_column_id).column;
559
3.67k
        DORIS_CHECK((*column)->size() == 1);
560
3.67k
        return Status::OK();
561
3.67k
    }
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
58.5k
    virtual Status customize_file_scan_request(FileScanRequest* file_request) {
570
58.5k
        return _append_delete_predicate(file_request);
571
58.5k
    }
572
573
204k
    bool _is_table_level_count_active() const { return _remaining_table_level_count >= 0; }
574
575
1.36k
    Status _materialize_count_rows(size_t rows, Block* block) const {
576
1.36k
        DORIS_CHECK(block != nullptr);
577
1.36k
        DORIS_CHECK(block->columns() > 0 || rows == 0);
578
2.72k
        for (size_t column_idx = 0; column_idx < block->columns(); ++column_idx) {
579
1.36k
            auto column = block->get_by_position(column_idx).type->create_column();
580
1.36k
            column->resize(rows);
581
1.36k
            block->replace_by_position(column_idx, std::move(column));
582
1.36k
        }
583
1.36k
        return Status::OK();
584
1.36k
    }
585
586
298
    Status _read_table_level_count(Block* block, bool* eos) {
587
298
        DORIS_CHECK(block != nullptr);
588
298
        DORIS_CHECK(eos != nullptr);
589
298
        DORIS_CHECK(_push_down_agg_type == TPushAggOp::type::COUNT);
590
298
        DORIS_CHECK(_remaining_table_level_count >= 0);
591
298
        if (_remaining_table_level_count == 0) {
592
148
            _remaining_table_level_count = -1;
593
148
            _current_task.reset();
594
148
            *eos = true;
595
148
            return Status::OK();
596
148
        }
597
598
150
        const int64_t batch_size = _runtime_state == nullptr
599
150
                                           ? _remaining_table_level_count
600
150
                                           : static_cast<int64_t>(_runtime_state->batch_size());
601
150
        const auto rows = std::min(_remaining_table_level_count, batch_size);
602
150
        RETURN_IF_ERROR(_materialize_count_rows(cast_set<size_t>(rows), block));
603
150
        _remaining_table_level_count -= rows;
604
150
        *eos = false;
605
150
        return Status::OK();
606
150
    }
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
565
            DORIS_CHECK(scan_columns == &request->non_predicate_columns);
618
565
            status = builder.add_non_predicate_column(column_id);
619
565
        }
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.56k
            _data_reader.file_schema.push_back(row_position_column_definition());
624
5.56k
        }
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
58.3k
    Status _append_delete_predicate(FileScanRequest* request) {
629
58.3k
        DORIS_CHECK(request != nullptr);
630
58.3k
        if ((_delete_rows == nullptr || _delete_rows->empty()) &&
631
58.3k
            (_deletion_vector == nullptr || _deletion_vector->isEmpty())) {
632
53.3k
            return Status::OK();
633
53.3k
        }
634
5.00k
        const auto row_position_column_id = LocalColumnId(ROW_POSITION_COLUMN_ID);
635
5.00k
        _append_file_scan_column(request, row_position_column_id, &request->predicate_columns);
636
637
5.00k
        const auto block_position = request->local_positions.at(row_position_column_id);
638
5.12k
        auto append_predicate = [&](auto& deleted_rows) {
639
5.12k
            auto delete_predicate = std::make_shared<DeletePredicate>(deleted_rows);
640
5.12k
            delete_predicate->add_child(VSlotRef::create_shared(
641
5.12k
                    cast_set<int>(block_position.value()), cast_set<int>(block_position.value()),
642
5.12k
                    -1, std::make_shared<DataTypeInt64>(), ROW_POSITION_COLUMN_NAME));
643
5.12k
            request->delete_conjuncts.push_back(
644
5.12k
                    VExprContext::create_shared(std::move(delete_predicate)));
645
5.12k
        };
_ZZN5doris6format11TableReader24_append_delete_predicateEPNS0_15FileScanRequestEENKUlRT_E_clISt6vectorIlSaIlEEEEDaS5_
Line
Count
Source
638
1.51k
        auto append_predicate = [&](auto& deleted_rows) {
639
1.51k
            auto delete_predicate = std::make_shared<DeletePredicate>(deleted_rows);
640
1.51k
            delete_predicate->add_child(VSlotRef::create_shared(
641
1.51k
                    cast_set<int>(block_position.value()), cast_set<int>(block_position.value()),
642
1.51k
                    -1, std::make_shared<DataTypeInt64>(), ROW_POSITION_COLUMN_NAME));
643
1.51k
            request->delete_conjuncts.push_back(
644
1.51k
                    VExprContext::create_shared(std::move(delete_predicate)));
645
1.51k
        };
_ZZN5doris6format11TableReader24_append_delete_predicateEPNS0_15FileScanRequestEENKUlRT_E_clIN7roaring12Roaring64MapEEEDaS5_
Line
Count
Source
638
3.60k
        auto append_predicate = [&](auto& deleted_rows) {
639
3.60k
            auto delete_predicate = std::make_shared<DeletePredicate>(deleted_rows);
640
3.60k
            delete_predicate->add_child(VSlotRef::create_shared(
641
3.60k
                    cast_set<int>(block_position.value()), cast_set<int>(block_position.value()),
642
3.60k
                    -1, std::make_shared<DataTypeInt64>(), ROW_POSITION_COLUMN_NAME));
643
3.60k
            request->delete_conjuncts.push_back(
644
3.60k
                    VExprContext::create_shared(std::move(delete_predicate)));
645
3.60k
        };
646
5.00k
        if (_delete_rows != nullptr && !_delete_rows->empty()) {
647
1.51k
            append_predicate(*_delete_rows);
648
1.51k
        }
649
5.00k
        if (_deletion_vector != nullptr && !_deletion_vector->isEmpty()) {
650
3.62k
            append_predicate(*_deletion_vector);
651
3.62k
        }
652
5.00k
        return Status::OK();
653
58.3k
    }
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
59.8k
    virtual Status close_current_reader() {
658
59.8k
        _finalize_reader_condition_cache();
659
59.8k
        RETURN_IF_ERROR(_data_reader.reader->close());
660
59.8k
        _data_reader.reader.reset();
661
59.8k
        if (_data_reader.column_mapper != nullptr) {
662
59.8k
            _data_reader.column_mapper->clear();
663
59.8k
            _data_reader.column_mapper.reset();
664
59.8k
        }
665
59.8k
        _table_filters.clear();
666
59.8k
        _constant_pruning_safe_filter_count = 0;
667
59.8k
        _data_reader.file_schema.clear();
668
59.8k
        _data_reader.file_block_layout.clear();
669
59.8k
        _data_reader.block_template.clear();
670
59.8k
        _current_task.reset();
671
59.8k
        _current_file_description.reset();
672
59.8k
        _current_reader_reached_eof = false;
673
59.8k
        return Status::OK();
674
59.8k
    }
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
118k
    Status finalize_chunk(Block* block, const size_t rows) {
684
118k
        SCOPED_TIMER(_profile.finalize_timer);
685
118k
        size_t idx = 0;
686
645k
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
687
645k
            ColumnPtr column;
688
645k
            RETURN_IF_ERROR(_materialize_mapping_column(mapping, &_data_reader.block_template, rows,
689
645k
                                                        &column));
690
645k
            block->replace_by_position(idx, IColumn::mutate(std::move(column)));
691
645k
            idx++;
692
645k
        }
693
118k
        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
118k
        RETURN_IF_ERROR(_truncate_char_or_varchar_columns(block));
697
118k
        return Status::OK();
698
118k
    }
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
94.4k
    virtual Status materialize_virtual_columns(Block* table_block) { return Status::OK(); }
704
705
#ifndef NDEBUG
706
118k
    Status _check_file_block_columns(std::string_view stage, size_t rows) {
707
118k
        DORIS_CHECK(_data_reader.block_template.columns() == _data_reader.file_block_layout.size());
708
753k
        for (size_t idx = 0; idx < _data_reader.block_template.columns(); ++idx) {
709
634k
            const auto& file_block_column = _data_reader.file_block_layout[idx];
710
634k
            const auto& column_with_type = _data_reader.block_template.get_by_position(idx);
711
634k
            const auto* column = column_with_type.column.get();
712
634k
            try {
713
634k
                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
634k
                column->sanity_check();
726
634k
                auto st = column_with_type.check_type_and_column_match();
727
634k
                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
634k
            } 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
634k
        }
770
118k
        return Status::OK();
771
118k
    }
772
773
118k
    Status _check_table_block_columns(std::string_view stage, const Block* block, size_t rows) {
774
118k
        DORIS_CHECK(block != nullptr);
775
118k
        DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
776
764k
        for (size_t idx = 0; idx < block->columns(); ++idx) {
777
645k
            const auto& mapping = _data_reader.column_mapper->mappings()[idx];
778
645k
            const auto& column_with_type = block->get_by_position(idx);
779
645k
            const auto* column = column_with_type.column.get();
780
645k
            try {
781
645k
                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
645k
                column->sanity_check();
792
645k
                auto st = column_with_type.check_type_and_column_match();
793
645k
                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
645k
            } 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
645k
        }
831
118k
        return Status::OK();
832
118k
    }
833
#endif
834
835
118k
    Status _truncate_char_or_varchar_columns(Block* block) {
836
118k
        DORIS_CHECK(block != nullptr);
837
118k
        if (_runtime_state == nullptr ||
838
118k
            !_runtime_state->query_options().truncate_char_or_varchar_columns) {
839
118k
            return Status::OK();
840
118k
        }
841
8
        DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
842
44
        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
8
        return Status::OK();
853
118k
    }
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
58.4k
    Status _try_materialize_aggregate_pushdown_rows(Block* block, bool* pushed_down) {
928
58.4k
        DORIS_CHECK(block != nullptr);
929
58.4k
        DORIS_CHECK(pushed_down != nullptr);
930
58.4k
        *pushed_down = false;
931
58.4k
        block->clear_column_data(_projected_columns.size());
932
58.4k
        _aggregate_pushdown_tried = true;
933
58.4k
        if (!_supports_aggregate_pushdown(_push_down_agg_type)) {
934
57.2k
            return Status::OK();
935
57.2k
        }
936
937
1.22k
        FileAggregateRequest file_request;
938
1.22k
        RETURN_IF_ERROR(_build_file_aggregate_request(_push_down_agg_type, &file_request));
939
1.22k
        FileAggregateResult file_result;
940
1.22k
        const auto status = _data_reader.reader->get_aggregate_result(file_request, &file_result);
941
1.22k
        if (status.is<ErrorCode::NOT_IMPLEMENTED_ERROR>()) {
942
5
            return Status::OK();
943
5
        }
944
1.22k
        RETURN_IF_ERROR(status);
945
1.21k
        RETURN_IF_ERROR(
946
1.21k
                _materialize_aggregate_pushdown_rows(_push_down_agg_type, file_result, block));
947
1.21k
        *pushed_down = true;
948
1.21k
        RETURN_IF_ERROR(close_current_reader());
949
1.21k
        return Status::OK();
950
1.21k
    }
951
952
59.7k
    virtual bool _supports_aggregate_pushdown(TPushAggOp::type agg_type) const {
953
        // Only COUNT and MIN/MAX can be push down.
954
59.7k
        if (agg_type != TPushAggOp::type::COUNT && agg_type != TPushAggOp::type::MINMAX) {
955
56.3k
            return false;
956
56.3k
        }
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.43k
        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.43k
        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.42k
        if ((_delete_rows != nullptr && !_delete_rows->empty()) ||
975
3.42k
            (_deletion_vector != nullptr && !_deletion_vector->isEmpty())) {
976
597
            return false;
977
597
        }
978
2.82k
        if (!_table_filters.empty()) {
979
0
            return false;
980
0
        }
981
2.82k
        if (agg_type == TPushAggOp::type::COUNT) {
982
2.56k
            return true;
983
2.56k
        }
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
325
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
988
325
            if (!mapping.file_local_id.has_value() ||
989
325
                mapping.virtual_column_type != TableVirtualColumnType::INVALID ||
990
325
                mapping.default_expr != nullptr || mapping.file_type == nullptr ||
991
325
                mapping.table_type == nullptr) {
992
9
                return false;
993
9
            }
994
316
            if (!_can_push_down_minmax_for_mapping(mapping)) {
995
222
                return false;
996
222
            }
997
316
        }
998
30
        return true;
999
261
    }
1000
1001
636k
    static ColumnPtr _detach_column(ColumnPtr column) {
1002
636k
        DORIS_CHECK(column.get() != nullptr);
1003
636k
        return IColumn::mutate(std::move(column));
1004
636k
    }
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.1k
        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.1k
        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.1k
    }
1068
1069
    static Status _execute_default_expr_without_root_type_check(
1070
            const VExprContextSPtr& default_expr, const Block* block,
1071
16.5k
            ColumnWithTypeAndName* result_data) {
1072
16.5k
        DORIS_CHECK(default_expr != nullptr);
1073
16.5k
        DORIS_CHECK(block != nullptr);
1074
16.5k
        DORIS_CHECK(result_data != nullptr);
1075
16.5k
        ColumnPtr result_column;
1076
16.5k
        Status st;
1077
16.5k
        RETURN_IF_CATCH_EXCEPTION({
1078
16.5k
            st = default_expr->root()->execute_column_impl(default_expr.get(), block, nullptr,
1079
16.5k
                                                           block->rows(), result_column);
1080
16.5k
        });
1081
16.5k
        RETURN_IF_ERROR(st);
1082
16.5k
        DORIS_CHECK(result_column.get() != nullptr);
1083
16.5k
        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.5k
        result_data->column = result_column;
1089
16.5k
        result_data->type = default_expr->execute_type(block);
1090
16.5k
        result_data->name = default_expr->expr_name();
1091
16.5k
        return Status::OK();
1092
16.5k
    }
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.1k
                                                     const size_t rows, ColumnPtr* column) {
1131
17.1k
        DORIS_CHECK(column != nullptr);
1132
17.1k
        DORIS_CHECK(mapping.file_type != nullptr);
1133
17.1k
        DORIS_CHECK(mapping.table_type != nullptr);
1134
17.1k
        *column = file_column;
1135
17.1k
        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.1k
        RETURN_IF_ERROR(_align_column_nullability(column, mapping.table_type));
1145
17.1k
        return Status::OK();
1146
17.1k
    }
1147
1148
    Status _materialize_mapping_column(const ColumnMapping& mapping, Block* current_block,
1149
645k
                                       const size_t rows, ColumnPtr* column) {
1150
645k
        if (!mapping.is_trivial && mapping.file_local_id.has_value() &&
1151
645k
            !mapping.child_mappings.empty()) {
1152
9.63k
            DCHECK(mapping.projection != nullptr);
1153
9.63k
            int res_id;
1154
9.63k
            auto st = mapping.projection->execute(current_block, &res_id);
1155
9.63k
            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.63k
            ColumnPtr result_column = current_block->get_by_position(res_id).column;
1163
9.63k
            RETURN_IF_ERROR(
1164
9.63k
                    _materialize_complex_mapping_column(mapping, result_column, rows, column));
1165
9.63k
            return Status::OK();
1166
9.63k
        }
1167
636k
        if (mapping.projection != nullptr) {
1168
619k
            int res_id;
1169
619k
            auto st = mapping.projection->execute(current_block, &res_id);
1170
619k
            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
619k
            ColumnPtr result_column = current_block->get_by_position(res_id).column;
1182
619k
            *column = _detach_column(std::move(result_column));
1183
619k
            return Status::OK();
1184
619k
        }
1185
17.2k
        if (mapping.default_expr != nullptr) {
1186
16.5k
            if (current_block->rows() == rows) {
1187
14.5k
                ColumnWithTypeAndName result;
1188
14.5k
                RETURN_IF_ERROR(_execute_default_expr_without_root_type_check(
1189
14.5k
                        mapping.default_expr, current_block, &result));
1190
14.5k
                ColumnPtr result_column = result.column;
1191
14.5k
                RETURN_IF_ERROR(_align_column_nullability(&result_column, mapping.table_type));
1192
14.5k
                *column = _detach_column(std::move(result_column));
1193
14.5k
            } 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.5k
            return Status::OK();
1206
16.5k
        }
1207
691
        ColumnPtr result_column = mapping.table_type->create_column_const_with_default_value(rows);
1208
691
        *column = _detach_column(std::move(result_column));
1209
691
        return Status::OK();
1210
17.2k
    }
1211
1212
    Status _materialize_complex_mapping_column(const ColumnMapping& mapping,
1213
                                               const ColumnPtr& file_column, const size_t rows,
1214
11.6k
                                               ColumnPtr* column) {
1215
11.6k
        DORIS_CHECK(mapping.table_type != nullptr);
1216
11.6k
        DORIS_CHECK(file_column.get() != nullptr);
1217
11.6k
        const auto table_type = remove_nullable(mapping.table_type);
1218
11.6k
        switch (table_type->get_primitive_type()) {
1219
3.62k
        case TYPE_STRUCT:
1220
3.62k
            RETURN_IF_ERROR(_materialize_struct_mapping_column(mapping, file_column, rows, column));
1221
3.62k
            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.6k
        }
1232
11.6k
        return Status::OK();
1233
11.6k
    }
1234
1235
    static std::vector<const ColumnMapping*> _present_child_mappings_in_file_order(
1236
3.62k
            const std::vector<ColumnMapping>& child_mappings) {
1237
3.62k
        std::vector<const ColumnMapping*> result;
1238
3.62k
        result.reserve(child_mappings.size());
1239
9.19k
        for (const auto& child_mapping : child_mappings) {
1240
9.19k
            if (child_mapping.file_local_id.has_value()) {
1241
5.57k
                result.push_back(&child_mapping);
1242
5.57k
            }
1243
9.19k
        }
1244
4.05k
        std::ranges::sort(result, [](const ColumnMapping* lhs, const ColumnMapping* rhs) {
1245
4.05k
            DORIS_CHECK(lhs->file_local_id.has_value());
1246
4.05k
            DORIS_CHECK(rhs->file_local_id.has_value());
1247
4.05k
            return *lhs->file_local_id < *rhs->file_local_id;
1248
4.05k
        });
1249
3.62k
        return result;
1250
3.62k
    }
1251
1252
    static size_t _file_child_ordinal_for_mapping(
1253
            const ColumnMapping& mapping, const ColumnMapping& child_mapping,
1254
5.57k
            const std::vector<const ColumnMapping*>& file_ordered_children) {
1255
5.57k
        DORIS_CHECK(child_mapping.file_local_id.has_value());
1256
5.57k
        if (!mapping.projected_file_children.empty()) {
1257
5.57k
            const auto child_it = std::ranges::find_if(
1258
8.57k
                    mapping.projected_file_children, [&](const ColumnDefinition& file_child) {
1259
8.57k
                        return file_child.file_local_id() == *child_mapping.file_local_id;
1260
8.57k
                    });
1261
5.57k
            DORIS_CHECK(child_it != mapping.projected_file_children.end());
1262
5.57k
            return static_cast<size_t>(
1263
5.57k
                    std::distance(mapping.projected_file_children.begin(), child_it));
1264
5.57k
        }
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.57k
    }
1269
1270
    static std::vector<const ColumnMapping*> _child_mappings_in_table_type_order(
1271
3.62k
            const ColumnMapping& mapping, const DataTypeStruct& table_type) {
1272
3.62k
        std::vector<const ColumnMapping*> result;
1273
3.62k
        result.reserve(mapping.child_mappings.size());
1274
12.8k
        for (size_t child_idx = 0; child_idx < table_type.get_elements().size(); ++child_idx) {
1275
9.19k
            const auto& child_name = table_type.get_element_name(child_idx);
1276
9.19k
            const auto child_it = std::ranges::find_if(
1277
17.6k
                    mapping.child_mappings, [&](const ColumnMapping& child_mapping) {
1278
17.6k
                        return child_mapping.table_column_name == child_name;
1279
17.6k
                    });
1280
9.19k
            DORIS_CHECK(child_it != mapping.child_mappings.end())
1281
0
                    << mapping.debug_string() << ", table_child_name=" << child_name;
1282
9.19k
            result.push_back(&*child_it);
1283
9.19k
        }
1284
3.62k
        return result;
1285
3.62k
    }
1286
1287
    static const IColumn* _nested_column_if_nullable(const ColumnPtr& column,
1288
11.6k
                                                     const NullMap** null_map) {
1289
11.6k
        DORIS_CHECK(column.get() != nullptr);
1290
11.6k
        if (const auto* nullable_column = check_and_get_column<ColumnNullable>(*column)) {
1291
11.6k
            if (null_map != nullptr) {
1292
11.6k
                *null_map = &nullable_column->get_null_map_data();
1293
11.6k
            }
1294
11.6k
            return &nullable_column->get_nested_column();
1295
11.6k
        }
1296
4
        return column.get();
1297
11.6k
    }
1298
1299
    Status _materialize_struct_mapping_column(const ColumnMapping& mapping,
1300
                                              const ColumnPtr& file_column, const size_t rows,
1301
3.62k
                                              ColumnPtr* column) {
1302
3.62k
        DORIS_CHECK(mapping.table_type != nullptr);
1303
3.62k
        const auto* table_type =
1304
3.62k
                assert_cast<const DataTypeStruct*>(remove_nullable(mapping.table_type).get());
1305
3.62k
        const auto full_file_column = file_column->convert_to_full_column_if_const();
1306
3.62k
        const NullMap* parent_null_map = nullptr;
1307
3.62k
        const auto* nested_file_column =
1308
3.62k
                _nested_column_if_nullable(full_file_column, &parent_null_map);
1309
3.62k
        const auto* file_struct = assert_cast<const ColumnStruct*>(nested_file_column);
1310
3.62k
        DORIS_CHECK(table_type->get_elements().size() == mapping.child_mappings.size());
1311
1312
3.62k
        Columns child_columns;
1313
3.62k
        child_columns.reserve(mapping.child_mappings.size());
1314
3.62k
        const auto file_ordered_children =
1315
3.62k
                _present_child_mappings_in_file_order(mapping.child_mappings);
1316
3.62k
        const auto table_ordered_children =
1317
3.62k
                _child_mappings_in_table_type_order(mapping, *table_type);
1318
9.19k
        for (const auto* child_mapping : table_ordered_children) {
1319
9.19k
            DORIS_CHECK(child_mapping != nullptr);
1320
9.19k
            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.57k
            const auto file_child_idx =
1327
5.57k
                    _file_child_ordinal_for_mapping(mapping, *child_mapping, file_ordered_children);
1328
5.57k
            DORIS_CHECK(file_child_idx < file_struct->get_columns().size());
1329
5.57k
            ColumnPtr child_column = file_struct->get_column_ptr(file_child_idx);
1330
5.57k
            RETURN_IF_ERROR(_materialize_present_child_mapping_column(*child_mapping, child_column,
1331
5.57k
                                                                      rows, &child_column));
1332
5.57k
            child_columns.push_back(std::move(child_column));
1333
5.57k
        }
1334
3.62k
        MutableColumns mutable_child_columns;
1335
3.62k
        mutable_child_columns.reserve(child_columns.size());
1336
9.19k
        for (auto& child_column : child_columns) {
1337
9.19k
            mutable_child_columns.push_back(IColumn::mutate(std::move(child_column)));
1338
9.19k
        }
1339
3.62k
        auto result = ColumnStruct::create(std::move(mutable_child_columns));
1340
3.62k
        if (mapping.table_type->is_nullable()) {
1341
3.62k
            auto null_map = ColumnUInt8::create();
1342
3.62k
            auto& null_map_data = null_map->get_data();
1343
3.62k
            null_map_data.resize(rows);
1344
3.62k
            if (parent_null_map != nullptr) {
1345
3.62k
                DORIS_CHECK(parent_null_map->size() == rows);
1346
3.62k
                null_map_data.assign(parent_null_map->begin(), parent_null_map->end());
1347
3.62k
            } else {
1348
0
                std::fill(null_map_data.begin(), null_map_data.end(), 0);
1349
0
            }
1350
3.62k
            *column = ColumnNullable::create(std::move(result), std::move(null_map));
1351
3.62k
        } else {
1352
2
            *column = std::move(result);
1353
2
        }
1354
3.62k
        return Status::OK();
1355
3.62k
    }
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.56k
                                           ColumnPtr* column) {
1393
3.56k
        const auto full_file_column = file_column->convert_to_full_column_if_const();
1394
3.56k
        const NullMap* parent_null_map = nullptr;
1395
3.56k
        const auto* nested_file_column =
1396
3.56k
                _nested_column_if_nullable(full_file_column, &parent_null_map);
1397
3.56k
        const auto* file_map = assert_cast<const ColumnMap*>(nested_file_column);
1398
3.56k
        ColumnPtr key_column = file_map->get_keys_ptr();
1399
3.56k
        ColumnPtr value_column = file_map->get_values_ptr();
1400
1401
3.56k
        const ColumnMapping* key_mapping = nullptr;
1402
3.56k
        const ColumnMapping* value_mapping = nullptr;
1403
7.12k
        for (const auto& child_mapping : mapping.child_mappings) {
1404
7.12k
            if (!child_mapping.file_local_id.has_value()) {
1405
0
                continue;
1406
0
            }
1407
7.12k
            if (*child_mapping.file_local_id == 0) {
1408
3.56k
                key_mapping = &child_mapping;
1409
3.56k
            } else if (*child_mapping.file_local_id == 1) {
1410
3.56k
                value_mapping = &child_mapping;
1411
3.56k
            }
1412
7.12k
        }
1413
1414
3.56k
        if (key_mapping != nullptr) {
1415
3.56k
            RETURN_IF_ERROR(_materialize_present_child_mapping_column(
1416
3.56k
                    *key_mapping, key_column, key_column->size(), &key_column));
1417
3.56k
        }
1418
3.56k
        if (value_mapping != nullptr) {
1419
3.56k
            RETURN_IF_ERROR(_materialize_present_child_mapping_column(
1420
3.56k
                    *value_mapping, value_column, value_column->size(), &value_column));
1421
3.56k
        }
1422
3.56k
        auto offsets_column = file_map->get_offsets_ptr()->convert_to_full_column_if_const();
1423
3.56k
        auto result = ColumnMap::create(IColumn::mutate(std::move(key_column)),
1424
3.56k
                                        IColumn::mutate(std::move(value_column)),
1425
3.56k
                                        IColumn::mutate(std::move(offsets_column)));
1426
3.56k
        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.56k
        return Status::OK();
1441
3.56k
    }
1442
1443
58.4k
    Status _open_mapping_exprs() {
1444
58.4k
        RowDescriptor row_desc;
1445
382k
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
1446
382k
            if (mapping.projection != nullptr) {
1447
364k
                RETURN_IF_ERROR(mapping.projection->prepare(_runtime_state, row_desc));
1448
364k
                RETURN_IF_ERROR(mapping.projection->open(_runtime_state));
1449
364k
            }
1450
382k
            if (mapping.default_expr != nullptr) {
1451
17.0k
                RETURN_IF_ERROR(mapping.default_expr->prepare(_runtime_state, row_desc));
1452
17.0k
                RETURN_IF_ERROR(mapping.default_expr->open(_runtime_state));
1453
17.0k
            }
1454
382k
        }
1455
58.4k
        return Status::OK();
1456
58.4k
    }
1457
1458
    Status _build_file_aggregate_request(TPushAggOp::type agg_type,
1459
1.24k
                                         FileAggregateRequest* request) const {
1460
1.24k
        DORIS_CHECK(request != nullptr);
1461
1.24k
        DORIS_CHECK(_supports_aggregate_pushdown(agg_type));
1462
1.24k
        request->agg_type = agg_type;
1463
1.24k
        request->columns.clear();
1464
1.24k
        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.21k
            if (_data_reader.column_mapper->mappings().size() == 1) {
1473
1.21k
                const auto& mapping = _data_reader.column_mapper->mappings()[0];
1474
1.21k
                if (mapping.file_local_id.has_value() && mapping.file_type != nullptr &&
1475
1.21k
                    is_complex_type(remove_nullable(mapping.file_type)->get_primitive_type()) &&
1476
1.21k
                    mapping.virtual_column_type == TableVirtualColumnType::INVALID &&
1477
1.21k
                    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.21k
            }
1484
1.21k
            return Status::OK();
1485
1.21k
        }
1486
28
        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
28
        return Status::OK();
1497
28
    }
1498
1499
    Status _materialize_aggregate_pushdown_rows(TPushAggOp::type agg_type,
1500
                                                const FileAggregateResult& file_result,
1501
1.23k
                                                Block* block) {
1502
1.23k
        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.21k
            DORIS_CHECK(file_result.count >= 0);
1507
1.21k
            return _materialize_count_rows(cast_set<size_t>(file_result.count), block);
1508
1.21k
        }
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
376k
            const std::vector<ColumnDefinition>& schema, LocalColumnId column_id) {
1627
9.08M
        for (const auto& field : schema) {
1628
9.08M
            if (field.file_local_id() == column_id.value()) {
1629
370k
                return &field;
1630
370k
            }
1631
9.08M
        }
1632
5.73k
        return nullptr;
1633
376k
    }
1634
1635
318
    static bool _can_push_down_minmax_for_mapping(const ColumnMapping& mapping) {
1636
318
        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
315
            return mapping.is_trivial;
1641
315
        }
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