Coverage Report

Created: 2026-07-24 16:20

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