Coverage Report

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