Coverage Report

Created: 2026-07-21 14:20

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