Coverage Report

Created: 2026-07-22 17:02

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