Coverage Report

Created: 2026-07-15 08:09

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