Coverage Report

Created: 2026-07-22 16:37

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.6k
    virtual ~TableReader() = default;
192
193
    // Initialize common runtime options for the table reader. Subclasses may call this from their
194
    // own init(options); table-format schema and split metadata are provided later per split.
195
    virtual Status init(TableReadOptions&& options);
196
197
    // FileScannerV2 adjusts this before each get_block() using an adaptive bytes-per-row estimate.
198
    // Store it here as well as forwarding to the current reader so newly opened split readers start
199
    // with the latest predicted batch size.
200
246k
    virtual void set_batch_size(size_t batch_size) {
201
246k
        _batch_size = std::max<size_t>(1, batch_size);
202
246k
        if (_data_reader.reader != nullptr) {
203
123k
            _data_reader.reader->set_batch_size(_batch_size);
204
123k
        }
205
246k
    }
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
315k
    virtual bool current_split_uses_metadata_count() const {
226
315k
        return _current_split_uses_metadata_count;
227
315k
    }
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
188k
    virtual Status get_block(Block* block, bool* eos) {
255
188k
        SCOPED_TIMER(_profile.total_timer);
256
188k
        SCOPED_TIMER(_profile.exec_timer);
257
188k
        DORIS_CHECK(block->columns() == _projected_columns.size());
258
188k
        block->clear_column_data(_projected_columns.size());
259
260
258k
        while (true) {
261
258k
            if (*eos) {
262
0
                return Status::OK();
263
0
            }
264
258k
            if (_io_ctx != nullptr && _io_ctx->should_stop) {
265
4
                *eos = true;
266
4
                return Status::OK();
267
4
            }
268
258k
            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
119k
                if (!_data_reader.reader) {
279
59.2k
                    DCHECK(*eos);
280
59.2k
                    return Status::OK();
281
59.2k
                }
282
119k
            }
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
195k
            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
194k
            bool current_eof = false;
306
194k
            _data_reader.block_template.clear_column_data(
307
194k
                    cast_set<int64_t>(_data_reader.file_block_layout.size()));
308
194k
            size_t current_rows = 0;
309
194k
            {
310
194k
                SCOPED_TIMER(_profile.file_reader_total_timer);
311
194k
                SCOPED_TIMER(_profile.file_reader_get_block_timer);
312
194k
                RETURN_IF_ERROR(_data_reader.reader->get_block(&_data_reader.block_template,
313
194k
                                                               &current_rows, &current_eof));
314
194k
            }
315
194k
            const bool stopped_during_read = _io_ctx != nullptr && _io_ctx->should_stop;
316
194k
            if (current_rows == 0) {
317
69.9k
                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
69.9k
                continue;
322
69.9k
            }
323
194k
            DCHECK_EQ(_data_reader.block_template.columns(), _data_reader.file_block_layout.size())
324
0
                    << _data_reader.block_template.dump_structure();
325
124k
#ifndef NDEBUG
326
124k
            RETURN_IF_ERROR(_check_file_block_columns("after file reader get_block", current_rows));
327
124k
#endif
328
124k
            DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
329
124k
            RETURN_IF_ERROR(finalize_chunk(block, current_rows));
330
124k
#ifndef NDEBUG
331
124k
            RETURN_IF_ERROR(
332
124k
                    _check_table_block_columns("after finalize_chunk", block, current_rows));
333
124k
#endif
334
124k
            if (current_eof) {
335
19
                _current_reader_reached_eof = !stopped_during_read;
336
19
                RETURN_IF_ERROR(close_current_reader());
337
19
            }
338
124k
            return Status::OK();
339
124k
        }
340
188k
    }
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.5k
    virtual Status close() {
345
40.5k
        SCOPED_TIMER(_profile.total_timer);
346
40.5k
        SCOPED_TIMER(_profile.close_timer);
347
40.5k
        if (_data_reader.reader) {
348
652
            RETURN_IF_ERROR(close_current_reader());
349
652
        }
350
40.5k
        _current_task.reset();
351
40.5k
        _current_file_description.reset();
352
40.5k
        _remaining_table_level_count = -1;
353
40.5k
        _remaining_file_level_count = -1;
354
40.5k
        _current_split_uses_metadata_count = false;
355
40.5k
        return Status::OK();
356
40.5k
    }
357
358
80.8k
    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.9k
    virtual Status validate_projected_columns(const ProjectedColumnBuildContext& context) const {
367
21.9k
        (void)context;
368
21.9k
        return Status::OK();
369
21.9k
    }
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.7k
                                               DeleteFileDesc* desc, bool* has_delete_file) {
391
32.7k
        *has_delete_file = false;
392
32.7k
        return Status::OK();
393
32.7k
    }
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.46k
    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.3k
    virtual Status open_reader() {
408
61.3k
        SCOPED_TIMER(_profile.open_reader_timer);
409
        // 1. Get file schema and create column mapping.
410
61.3k
        std::vector<ColumnDefinition> file_schema;
411
61.3k
        {
412
61.3k
            SCOPED_TIMER(_profile.file_reader_total_timer);
413
61.3k
            SCOPED_TIMER(_profile.file_reader_schema_timer);
414
61.3k
            RETURN_IF_ERROR(_data_reader.reader->get_schema(&file_schema));
415
61.3k
        }
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.3k
        RETURN_IF_ERROR(annotate_file_schema(&file_schema));
419
61.3k
        _data_reader.file_schema = file_schema;
420
61.3k
        _mapper_options.mode = mapping_mode();
421
61.3k
        configure_mapper_options(&_mapper_options);
422
423
61.3k
        {
424
61.3k
            SCOPED_TIMER(_profile.file_reader_total_timer);
425
61.3k
            SCOPED_TIMER(_profile.file_reader_mapper_timer);
426
61.3k
            _data_reader.column_mapper = _data_reader.reader->create_column_mapper(_mapper_options);
427
61.3k
        }
428
61.3k
        DORIS_CHECK(_data_reader.column_mapper != nullptr);
429
61.3k
        RETURN_IF_ERROR(_data_reader.column_mapper->create_mapping(_projected_columns,
430
61.3k
                                                                   _partition_values, file_schema));
431
61.3k
        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.3k
        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.3k
        auto file_request = std::make_shared<FileScanRequest>();
440
61.3k
        RETURN_IF_ERROR(_data_reader.column_mapper->create_scan_request(
441
61.3k
                _table_filters, _projected_columns, file_request.get(), _runtime_state));
442
61.3k
        bool constant_filter_pruned_split = false;
443
61.3k
        RETURN_IF_ERROR(_evaluate_constant_filters(&constant_filter_pruned_split));
444
61.3k
        if (constant_filter_pruned_split) {
445
647
            RETURN_IF_ERROR(close_current_reader());
446
647
            return Status::OK();
447
647
        }
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.93k
            file_request->count_star_placeholder_columns.reserve(
457
1.93k
                    file_request->non_predicate_columns.size());
458
1.93k
            for (const auto& column : file_request->non_predicate_columns) {
459
1.90k
                file_request->count_star_placeholder_columns.push_back(column.column_id());
460
1.90k
            }
461
1.93k
        }
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
377k
        for (const auto& [file_column_id, block_position] : file_request->local_positions) {
471
377k
            DORIS_CHECK(block_position.value() < _data_reader.file_block_layout.size());
472
377k
            const auto* field = _find_column_definition(_data_reader.file_schema, file_column_id);
473
377k
            DORIS_CHECK(field != nullptr);
474
475
377k
            ColumnDefinition projected_field;
476
377k
            {
477
377k
                auto it = std::find_if(
478
377k
                        file_request->non_predicate_columns.begin(),
479
377k
                        file_request->non_predicate_columns.end(),
480
7.63M
                        [&](const LocalColumnIndex& p) { return p.column_id() == file_column_id; });
481
377k
                if (it != file_request->non_predicate_columns.end()) {
482
343k
                    RETURN_IF_ERROR(project_column_definition(*field, *it, &projected_field));
483
343k
                }
484
377k
            }
485
377k
            {
486
377k
                auto it = std::find_if(
487
377k
                        file_request->predicate_columns.begin(),
488
377k
                        file_request->predicate_columns.end(),
489
377k
                        [&](const LocalColumnIndex& p) { return p.column_id() == file_column_id; });
490
377k
                if (it != file_request->predicate_columns.end()) {
491
34.0k
                    RETURN_IF_ERROR(project_column_definition(*field, *it, &projected_field));
492
34.0k
                }
493
377k
            }
494
377k
            _data_reader.file_block_layout[block_position.value()] = {
495
377k
                    .file_column_id = file_column_id,
496
377k
                    .name = projected_field.name,
497
377k
                    .type = projected_field.type,
498
377k
            };
499
377k
            DORIS_CHECK(_data_reader.file_block_layout[block_position.value()].type != nullptr);
500
377k
        }
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
377k
        for (const auto& column : _data_reader.file_block_layout) {
506
377k
            _data_reader.block_template.insert(
507
377k
                    {column.type->create_column(), column.type, column.name});
508
377k
        }
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.2k
    Status _evaluate_constant_filters(bool* can_filter_all) {
533
61.2k
        DORIS_CHECK(can_filter_all != nullptr);
534
61.2k
        DORIS_CHECK_LE(_constant_pruning_safe_filter_count, _table_filters.size());
535
61.2k
        *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.1k
        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.82k
            Block eval_block;
554
2.82k
            RETURN_IF_ERROR(_build_constant_filter_block(table_filter, &eval_block));
555
2.82k
            RowDescriptor row_desc;
556
2.82k
            RETURN_IF_ERROR(table_filter.conjunct->prepare(_runtime_state, row_desc));
557
2.82k
            RETURN_IF_ERROR(table_filter.conjunct->open(_runtime_state));
558
2.82k
            int result_column_id = -1;
559
2.82k
            RETURN_IF_ERROR(table_filter.conjunct->execute(&eval_block, &result_column_id));
560
2.82k
            DORIS_CHECK(result_column_id >= 0);
561
2.82k
            if (_filter_result_filters_all(eval_block.get_by_position(result_column_id).column)) {
562
647
                *can_filter_all = true;
563
647
                return Status::OK();
564
647
            }
565
2.82k
        }
566
60.6k
        return Status::OK();
567
61.2k
    }
568
569
25.3k
    bool _table_filter_has_only_constant_entries(const TableFilter& table_filter) const {
570
25.3k
        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.83k
        return !table_filter.global_indices.empty();
578
25.3k
    }
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.23k
            const auto global_index = GlobalIndex(column_idx);
588
9.23k
            const auto& mapping = mappings[column_idx];
589
9.23k
            const auto entry_it = filter_entries.find(global_index);
590
9.23k
            const bool referenced_by_filter =
591
9.23k
                    std::find(table_filter.global_indices.begin(),
592
9.23k
                              table_filter.global_indices.end(),
593
9.23k
                              global_index) != table_filter.global_indices.end();
594
9.23k
            if (referenced_by_filter && entry_it != filter_entries.end() &&
595
9.23k
                entry_it->second.is_constant()) {
596
2.88k
                ColumnPtr constant_column;
597
2.88k
                RETURN_IF_ERROR(_materialize_constant_filter_column(
598
2.88k
                        entry_it->second.constant_index(), &constant_column));
599
2.88k
                eval_block->insert({std::move(constant_column), mapping.table_type,
600
2.88k
                                    mapping.table_column_name});
601
6.35k
            } else {
602
6.35k
                eval_block->insert({mapping.table_type->create_column_const_with_default_value(1),
603
6.35k
                                    mapping.table_type, mapping.table_column_name});
604
6.35k
            }
605
9.23k
        }
606
2.82k
        return Status::OK();
607
2.82k
    }
608
609
2.88k
    Status _materialize_constant_filter_column(ConstantIndex constant_index, ColumnPtr* column) {
610
2.88k
        DORIS_CHECK(column != nullptr);
611
2.88k
        const auto& constant_entry = _data_reader.column_mapper->constant_map().get(constant_index);
612
2.88k
        DORIS_CHECK(constant_entry.expr != nullptr);
613
2.88k
        DORIS_CHECK(constant_entry.type != nullptr);
614
2.88k
        RowDescriptor row_desc;
615
2.88k
        RETURN_IF_ERROR(constant_entry.expr->prepare(_runtime_state, row_desc));
616
2.88k
        RETURN_IF_ERROR(constant_entry.expr->open(_runtime_state));
617
2.88k
        Block eval_block;
618
2.88k
        eval_block.insert({constant_entry.type->create_column_const_with_default_value(1),
619
2.88k
                           constant_entry.type, "__table_reader_constant_filter"});
620
2.88k
        int result_column_id = -1;
621
2.88k
        RETURN_IF_ERROR(constant_entry.expr->execute(&eval_block, &result_column_id));
622
2.88k
        DORIS_CHECK(result_column_id >= 0);
623
2.88k
        *column = eval_block.get_by_position(result_column_id).column;
624
2.88k
        DORIS_CHECK((*column)->size() == 1);
625
2.88k
        return Status::OK();
626
2.88k
    }
627
628
2.83k
    static bool _filter_result_filters_all(const ColumnPtr& filter_column) {
629
2.83k
        DORIS_CHECK(filter_column.get() != nullptr);
630
2.83k
        DORIS_CHECK(filter_column->size() == 1);
631
2.83k
        return !filter_column->get_bool(0);
632
2.83k
    }
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.08k
    Status _materialize_count_rows(size_t rows, Block* block) const {
643
3.08k
        DORIS_CHECK(block != nullptr);
644
3.08k
        DORIS_CHECK(block->columns() > 0 || rows == 0);
645
6.17k
        for (size_t column_idx = 0; column_idx < block->columns(); ++column_idx) {
646
3.08k
            auto column = block->get_by_position(column_idx).type->create_column();
647
3.08k
            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.08k
                nullable->get_nested_column().insert_many_defaults(rows);
652
3.08k
                nullable->get_null_map_data().resize_fill(rows, 0);
653
3.08k
            } else {
654
4
                column->insert_many_defaults(rows);
655
4
            }
656
3.08k
            block->replace_by_position(column_idx, std::move(column));
657
3.08k
        }
658
3.08k
        return Status::OK();
659
3.08k
    }
660
661
3.08k
    Status _materialize_next_count_batch(int64_t* remaining_rows, Block* block) const {
662
3.08k
        DORIS_CHECK(remaining_rows != nullptr);
663
3.08k
        DORIS_CHECK(*remaining_rows > 0);
664
3.08k
        const int64_t batch_size = _runtime_state == nullptr
665
3.08k
                                           ? *remaining_rows
666
3.08k
                                           : static_cast<int64_t>(_runtime_state->batch_size());
667
3.08k
        const auto rows = std::min(*remaining_rows, batch_size);
668
3.08k
        RETURN_IF_ERROR(_materialize_count_rows(cast_set<size_t>(rows), block));
669
3.08k
        *remaining_rows -= rows;
670
3.08k
        return Status::OK();
671
3.08k
    }
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.60k
            *remaining_rows = -1;
681
1.60k
            _current_task.reset();
682
1.60k
            *eos = true;
683
1.60k
            return Status::OK();
684
1.60k
        }
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.7k
                                  std::vector<LocalColumnIndex>* scan_columns) {
700
12.7k
        DORIS_CHECK(request != nullptr);
701
12.7k
        DORIS_CHECK(scan_columns != nullptr);
702
12.7k
        FileScanRequestBuilder builder(request);
703
12.7k
        Status status;
704
12.7k
        if (scan_columns == &request->predicate_columns) {
705
10.6k
            status = builder.add_predicate_column(column_id);
706
10.6k
        } 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.7k
        DORIS_CHECK(status.ok()) << status.to_string();
711
12.7k
        if (column_id == LocalColumnId(ROW_POSITION_COLUMN_ID) &&
712
12.7k
            _find_column_definition(_data_reader.file_schema, column_id) == nullptr) {
713
7.43k
            _data_reader.file_schema.push_back(row_position_column_definition());
714
7.43k
        }
715
12.7k
    }
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.5k
    Status _append_delete_predicate(FileScanRequest* request) {
719
60.5k
        DORIS_CHECK(request != nullptr);
720
60.5k
        if ((_delete_rows == nullptr || _delete_rows->empty()) &&
721
60.5k
            (_deletion_vector == nullptr || _deletion_vector->isEmpty())) {
722
54.8k
            return Status::OK();
723
54.8k
        }
724
5.74k
        const auto row_position_column_id = LocalColumnId(ROW_POSITION_COLUMN_ID);
725
5.74k
        _append_file_scan_column(request, row_position_column_id, &request->predicate_columns);
726
727
5.74k
        const auto block_position = request->local_positions.at(row_position_column_id);
728
5.82k
        auto append_predicate = [&](auto& deleted_rows) {
729
5.82k
            auto delete_predicate = std::make_shared<DeletePredicate>(deleted_rows);
730
5.82k
            delete_predicate->add_child(VSlotRef::create_shared(
731
5.82k
                    cast_set<int>(block_position.value()), cast_set<int>(block_position.value()),
732
5.82k
                    -1, std::make_shared<DataTypeInt64>(), ROW_POSITION_COLUMN_NAME));
733
5.82k
            request->delete_conjuncts.push_back(
734
5.82k
                    VExprContext::create_shared(std::move(delete_predicate)));
735
5.82k
        };
_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.16k
        auto append_predicate = [&](auto& deleted_rows) {
729
4.16k
            auto delete_predicate = std::make_shared<DeletePredicate>(deleted_rows);
730
4.16k
            delete_predicate->add_child(VSlotRef::create_shared(
731
4.16k
                    cast_set<int>(block_position.value()), cast_set<int>(block_position.value()),
732
4.16k
                    -1, std::make_shared<DataTypeInt64>(), ROW_POSITION_COLUMN_NAME));
733
4.16k
            request->delete_conjuncts.push_back(
734
4.16k
                    VExprContext::create_shared(std::move(delete_predicate)));
735
4.16k
        };
736
5.74k
        if (_delete_rows != nullptr && !_delete_rows->empty()) {
737
1.65k
            append_predicate(*_delete_rows);
738
1.65k
        }
739
5.74k
        if (_deletion_vector != nullptr && !_deletion_vector->isEmpty()) {
740
4.17k
            append_predicate(*_deletion_vector);
741
4.17k
        }
742
5.74k
        return Status::OK();
743
60.5k
    }
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
124k
    Status finalize_chunk(Block* block, const size_t rows) {
778
124k
        SCOPED_TIMER(_profile.finalize_timer);
779
124k
        size_t idx = 0;
780
124k
        const auto& mappings = _data_reader.column_mapper->mappings();
781
677k
        for (const auto& mapping : mappings) {
782
677k
            ColumnPtr column;
783
677k
            RETURN_IF_ERROR(_materialize_mapping_column(mapping, &_data_reader.block_template, rows,
784
677k
                                                        &column, idx + 1 == mappings.size()));
785
677k
            block->replace_by_position(idx, IColumn::mutate(std::move(column)));
786
677k
            idx++;
787
677k
        }
788
124k
        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
124k
        RETURN_IF_ERROR(_truncate_char_or_varchar_columns(block));
792
124k
        return Status::OK();
793
124k
    }
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
96.8k
    virtual Status materialize_virtual_columns(Block* table_block) { return Status::OK(); }
799
800
#ifndef NDEBUG
801
124k
    Status _check_file_block_columns(std::string_view stage, size_t rows) {
802
124k
        DORIS_CHECK(_data_reader.block_template.columns() == _data_reader.file_block_layout.size());
803
789k
        for (size_t idx = 0; idx < _data_reader.block_template.columns(); ++idx) {
804
665k
            const auto& file_block_column = _data_reader.file_block_layout[idx];
805
665k
            const auto& column_with_type = _data_reader.block_template.get_by_position(idx);
806
665k
            const auto* column = column_with_type.column.get();
807
665k
            try {
808
665k
                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
665k
                column->sanity_check();
821
665k
                auto st = column_with_type.check_type_and_column_match();
822
665k
                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
665k
            } 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
665k
        }
865
124k
        return Status::OK();
866
124k
    }
867
868
124k
    Status _check_table_block_columns(std::string_view stage, const Block* block, size_t rows) {
869
124k
        DORIS_CHECK(block != nullptr);
870
124k
        DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
871
802k
        for (size_t idx = 0; idx < block->columns(); ++idx) {
872
677k
            const auto& mapping = _data_reader.column_mapper->mappings()[idx];
873
677k
            const auto& column_with_type = block->get_by_position(idx);
874
677k
            const auto* column = column_with_type.column.get();
875
677k
            try {
876
677k
                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
677k
                column->sanity_check();
887
677k
                auto st = column_with_type.check_type_and_column_match();
888
677k
                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
677k
            } 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
677k
        }
926
124k
        return Status::OK();
927
124k
    }
928
#endif
929
930
124k
    Status _truncate_char_or_varchar_columns(Block* block) {
931
124k
        DORIS_CHECK(block != nullptr);
932
124k
        if (_runtime_state == nullptr ||
933
124k
            !_runtime_state->query_options().truncate_char_or_varchar_columns) {
934
124k
            return Status::OK();
935
124k
        }
936
4
        DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
937
40
        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
4
        return Status::OK();
948
124k
    }
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.6k
    Status _try_materialize_aggregate_pushdown_rows(Block* block, bool* pushed_down) {
1023
60.6k
        DORIS_CHECK(block != nullptr);
1024
60.6k
        DORIS_CHECK(pushed_down != nullptr);
1025
60.6k
        *pushed_down = false;
1026
60.6k
        block->clear_column_data(_projected_columns.size());
1027
60.6k
        _aggregate_pushdown_tried = true;
1028
60.6k
        if (!_supports_aggregate_pushdown(_push_down_agg_type)) {
1029
59.1k
            return Status::OK();
1030
59.1k
        }
1031
1032
1.49k
        FileAggregateRequest file_request;
1033
1.49k
        RETURN_IF_ERROR(_build_file_aggregate_request(_push_down_agg_type, &file_request));
1034
1.49k
        FileAggregateResult file_result;
1035
1.49k
        Status status;
1036
1.49k
        {
1037
1.49k
            SCOPED_TIMER(_profile.file_reader_total_timer);
1038
1.49k
            SCOPED_TIMER(_profile.file_reader_aggregate_timer);
1039
1.49k
            status = _data_reader.reader->get_aggregate_result(file_request, &file_result);
1040
1.49k
        }
1041
1.49k
        if (status.is<ErrorCode::NOT_IMPLEMENTED_ERROR>()) {
1042
13
            return Status::OK();
1043
13
        }
1044
1.48k
        RETURN_IF_ERROR(status);
1045
1.47k
        if (_push_down_agg_type == TPushAggOp::type::COUNT) {
1046
1.46k
            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.46k
            _remaining_file_level_count = file_result.count;
1051
1.46k
            _current_split_uses_metadata_count = true;
1052
1.46k
            if (_remaining_file_level_count > 0) {
1053
1.46k
                RETURN_IF_ERROR(_materialize_next_count_batch(&_remaining_file_level_count, block));
1054
1.46k
            }
1055
1.46k
        } else {
1056
15
            RETURN_IF_ERROR(
1057
15
                    _materialize_aggregate_pushdown_rows(_push_down_agg_type, file_result, block));
1058
15
        }
1059
1.47k
        *pushed_down = true;
1060
1.47k
        RETURN_IF_ERROR(close_current_reader());
1061
1.47k
        return Status::OK();
1062
1.47k
    }
1063
1064
62.2k
    virtual bool _supports_aggregate_pushdown(TPushAggOp::type agg_type) const {
1065
        // Only COUNT and MIN/MAX can be push down.
1066
62.2k
        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.75k
        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.74k
        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.74k
        if ((_delete_rows != nullptr && !_delete_rows->empty()) ||
1087
4.74k
            (_deletion_vector != nullptr && !_deletion_vector->isEmpty())) {
1088
1.08k
            return false;
1089
1.08k
        }
1090
3.65k
        if (!_table_filters.empty()) {
1091
0
            return false;
1092
0
        }
1093
3.65k
        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.39k
            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.39k
            if (_push_down_count_columns->empty()) {
1105
2.57k
                return true;
1106
2.57k
            }
1107
821
            if (_push_down_count_columns->size() != 1) {
1108
37
                return false;
1109
37
            }
1110
784
            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
784
            return mapping.file_local_id.has_value() && mapping.file_type != nullptr &&
1116
784
                   mapping.table_type != nullptr && mapping.is_trivial &&
1117
784
                   mapping.virtual_column_type == TableVirtualColumnType::INVALID &&
1118
784
                   mapping.default_expr == nullptr;
1119
821
        }
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
30
        return true;
1135
261
    }
1136
1137
669k
    static ColumnPtr _detach_column(ColumnPtr column) {
1138
669k
        DORIS_CHECK(column.get() != nullptr);
1139
669k
        return IColumn::mutate(std::move(column));
1140
669k
    }
1141
1142
112k
    static ColumnPtr _take_and_detach_block_column(Block* block, int position) {
1143
112k
        DORIS_CHECK(block != nullptr);
1144
112k
        DORIS_CHECK(position >= 0 && position < static_cast<int>(block->columns()));
1145
112k
        auto& source = block->get_by_position(position);
1146
112k
        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
112k
        block->replace_by_position(position, source.type->create_column());
1150
112k
        return _detach_column(std::move(column));
1151
112k
    }
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.1k
            const auto& nested_type =
1161
52.1k
                    assert_cast<const DataTypeNullable&>(*table_type).get_nested_type();
1162
52.1k
            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.1k
            const auto& nullable_column = assert_cast<const ColumnNullable&>(**column);
1168
52.1k
            ColumnPtr nested_column = nullable_column.get_nested_column_ptr();
1169
52.1k
            RETURN_IF_ERROR(_align_column_nullability(&nested_column, nested_type));
1170
52.1k
            *column = ColumnNullable::create(nested_column,
1171
52.1k
                                             nullable_column.get_null_map_column_ptr());
1172
52.1k
            return Status::OK();
1173
52.1k
        }
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
52.0k
        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.1k
                                                     const size_t rows, ColumnPtr* column) {
1278
18.1k
        DORIS_CHECK(column != nullptr);
1279
18.1k
        DORIS_CHECK(mapping.file_type != nullptr);
1280
18.1k
        DORIS_CHECK(mapping.table_type != nullptr);
1281
18.1k
        *column = file_column;
1282
18.1k
        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.1k
        RETURN_IF_ERROR(_align_column_nullability(column, mapping.table_type));
1292
18.1k
        return Status::OK();
1293
18.1k
    }
1294
1295
    Status _materialize_mapping_column(const ColumnMapping& mapping, Block* current_block,
1296
                                       const size_t rows, ColumnPtr* column,
1297
677k
                                       bool take_projection_result = false) {
1298
677k
        if (!mapping.is_trivial && mapping.file_local_id.has_value() &&
1299
677k
            !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
667k
        if (mapping.projection != nullptr) {
1318
647k
            int res_id;
1319
647k
            auto st = mapping.projection->execute(current_block, &res_id);
1320
647k
            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
647k
            if (take_projection_result) {
1332
110k
                *column = _take_and_detach_block_column(current_block, res_id);
1333
536k
            } else {
1334
536k
                ColumnPtr result_column = current_block->get_by_position(res_id).column;
1335
536k
                *column = _detach_column(std::move(result_column));
1336
536k
            }
1337
647k
            return Status::OK();
1338
647k
        }
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.03k
                DORIS_CHECK(mapping.constant_index.has_value());
1349
2.03k
                Block eval_block;
1350
2.03k
                eval_block.insert({mapping.table_type->create_column_const_with_default_value(rows),
1351
2.03k
                                   mapping.table_type, "__table_reader_const_rows"});
1352
2.03k
                ColumnWithTypeAndName result;
1353
2.03k
                RETURN_IF_ERROR(_execute_default_expr_without_root_type_check(
1354
2.03k
                        mapping.default_expr, &eval_block, &result));
1355
2.03k
                ColumnPtr result_column = result.column;
1356
2.03k
                RETURN_IF_ERROR(_align_column_nullability(&result_column, mapping.table_type));
1357
2.03k
                *column = _detach_column(std::move(result_column));
1358
2.03k
            }
1359
17.5k
            return Status::OK();
1360
17.5k
        }
1361
2.86k
        ColumnPtr result_column = mapping.table_type->create_column_const_with_default_value(rows);
1362
2.86k
        *column = _detach_column(std::move(result_column));
1363
2.86k
        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.68k
            RETURN_IF_ERROR(_materialize_map_mapping_column(mapping, file_column, rows, column));
1381
3.68k
            break;
1382
3.68k
        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
4
        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.68k
            auto null_map = ColumnUInt8::create();
1585
3.68k
            auto& null_map_data = null_map->get_data();
1586
3.68k
            null_map_data.resize(rows);
1587
3.68k
            if (parent_null_map != nullptr) {
1588
3.68k
                DORIS_CHECK(parent_null_map->size() == rows);
1589
3.68k
                null_map_data.assign(parent_null_map->begin(), parent_null_map->end());
1590
3.68k
            } else {
1591
0
                std::fill(null_map_data.begin(), null_map_data.end(), 0);
1592
0
            }
1593
3.68k
            *column = ColumnNullable::create(std::move(result), std::move(null_map));
1594
3.68k
        } 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
391k
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
1603
391k
            if (mapping.projection != nullptr) {
1604
369k
                RETURN_IF_ERROR(mapping.projection->prepare(_runtime_state, row_desc));
1605
369k
                RETURN_IF_ERROR(mapping.projection->open(_runtime_state));
1606
369k
            }
1607
391k
            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
391k
        }
1612
60.6k
        return Status::OK();
1613
60.6k
    }
1614
1615
    Status _build_file_aggregate_request(TPushAggOp::type agg_type,
1616
1.49k
                                         FileAggregateRequest* request) const {
1617
1.49k
        DORIS_CHECK(request != nullptr);
1618
1.49k
        DORIS_CHECK(_supports_aggregate_pushdown(agg_type));
1619
1.49k
        request->agg_type = agg_type;
1620
1.49k
        request->columns.clear();
1621
1.49k
        if (agg_type == TPushAggOp::type::COUNT) {
1622
1.47k
            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.47k
            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.47k
            return Status::OK();
1637
1.47k
        }
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
385k
            const std::vector<ColumnDefinition>& schema, LocalColumnId column_id) {
1792
9.18M
        for (const auto& field : schema) {
1793
9.18M
            if (field.file_local_id() == column_id.value()) {
1794
378k
                return &field;
1795
378k
            }
1796
9.18M
        }
1797
7.54k
        return nullptr;
1798
385k
    }
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