Coverage Report

Created: 2026-07-23 00:36

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