Coverage Report

Created: 2026-07-23 18:24

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