Coverage Report

Created: 2026-07-23 02:28

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