Coverage Report

Created: 2026-07-23 08:47

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