Coverage Report

Created: 2026-10-26 20:02

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/delete_predicate.h"
61
#include "format_v2/expr/evolution_cast.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
namespace parquet {
79
class ParquetProfileCache;
80
}
81
82
using DeleteRows = std::vector<int64_t>;
83
84
// Row-level predicates on table/global schema. They are rewritten to file-local expressions when
85
// possible, and remain the source of row-level filtering after localization.
86
struct TableFilter {
87
    VExprContextSPtr conjunct;
88
    std::vector<GlobalIndex> global_indices;
89
    bool metadata_pruning_safe = true;
90
};
91
92
struct ScanTask {
93
3.66k
    virtual ~ScanTask() = default;
94
95
    std::unique_ptr<io::FileDescription> data_file;
96
};
97
98
struct ProjectedColumnBuildContext {
99
    const TFileScanRangeParams* scan_params = nullptr;
100
    const TFileRangeDesc* range = nullptr;
101
    RuntimeState* runtime_state = nullptr;
102
    const SlotDescriptor* slot_desc = nullptr;
103
    std::optional<ColumnDefinition> schema_column = std::nullopt;
104
    size_t next_file_column_idx = 0;
105
};
106
107
struct ReadProfile {
108
    RuntimeProfile::Counter* total_timer = nullptr;
109
    RuntimeProfile::Counter* init_timer = nullptr;
110
    RuntimeProfile::Counter* num_delete_files = nullptr;
111
    RuntimeProfile::Counter* num_delete_rows = nullptr;
112
    RuntimeProfile::Counter* parse_delete_file_time = nullptr;
113
    RuntimeProfile::Counter* equality_delete_index_cache_hit_count = nullptr;
114
    RuntimeProfile::Counter* equality_delete_index_cache_miss_count = nullptr;
115
    RuntimeProfile::Counter* equality_delete_hash_index_memory = nullptr;
116
    RuntimeProfile::Counter* decoded_dv_cache_hit_count = nullptr;
117
    RuntimeProfile::Counter* decoded_dv_cache_miss_count = nullptr;
118
    RuntimeProfile::Counter* dv_file_cache_hit_count = nullptr;
119
    RuntimeProfile::Counter* dv_file_cache_miss_count = nullptr;
120
    RuntimeProfile::Counter* dv_file_cache_peer_read_count = nullptr;
121
    RuntimeProfile::Counter* exec_timer = nullptr;
122
    RuntimeProfile::Counter* prepare_split_timer = nullptr;
123
    RuntimeProfile::Counter* finalize_timer = nullptr;
124
    RuntimeProfile::Counter* create_reader_timer = nullptr;
125
    RuntimeProfile::Counter* pushdown_agg_timer = nullptr;
126
    RuntimeProfile::Counter* open_reader_timer = nullptr;
127
    RuntimeProfile::Counter* refresh_conjuncts_timer = nullptr;
128
    RuntimeProfile::Counter* runtime_filter_partition_prune_timer = nullptr;
129
    RuntimeProfile::Counter* runtime_filter_partition_pruned_range_counter = nullptr;
130
    RuntimeProfile::Counter* close_timer = nullptr;
131
    RuntimeProfile::Counter* file_reader_total_timer = nullptr;
132
    RuntimeProfile::Counter* file_reader_init_timer = nullptr;
133
    RuntimeProfile::Counter* file_reader_schema_timer = nullptr;
134
    RuntimeProfile::Counter* file_reader_mapper_timer = nullptr;
135
    RuntimeProfile::Counter* file_reader_open_timer = nullptr;
136
    RuntimeProfile::Counter* file_reader_refresh_timer = nullptr;
137
    RuntimeProfile::Counter* file_reader_get_block_timer = nullptr;
138
    RuntimeProfile::Counter* file_reader_aggregate_timer = nullptr;
139
    RuntimeProfile::Counter* file_reader_close_timer = nullptr;
140
};
141
142
struct TableReadOptions {
143
    // Columns need to be read from file and output by table reader. They are all in table/global
144
    // schema semantics.
145
    const std::vector<ColumnDefinition> projected_columns;
146
    // All complex conjuncts from scan operator
147
    const VExprContextSPtrs conjuncts;
148
    // File format of the underlying data files, needed for reader initialization and reader-level
149
    // filter pushdown.
150
    const FileFormat format;
151
    TFileScanRangeParams* scan_params;
152
    std::shared_ptr<io::IOContext> io_ctx;
153
    RuntimeState* runtime_state;
154
    RuntimeProfile* scanner_profile;
155
    parquet::ParquetProfileCache* parquet_profile_cache = nullptr;
156
    // File formats without complete self-describing metadata, such as CSV, Text, and JSON, need
157
    // the FE-planned physical file slots to build their file-local schema and deserialize values.
158
    const std::vector<SlotDescriptor*>* file_slot_descs = nullptr;
159
    // Push-down aggregate type.
160
    const TPushAggOp::type push_down_agg_type = TPushAggOp::type::NONE;
161
    // Table/global indices of explicit COUNT arguments. nullopt means an old FE did not send the
162
    // semantic argument field, while an explicit empty vector means COUNT(*)/COUNT(1). Keeping
163
    // those states separate prevents a rolling-upgrade plan from being reinterpreted by a new BE.
164
    const std::optional<std::vector<GlobalIndex>> push_down_count_columns = std::nullopt;
165
    // Initial digest of predicates available during scanner open. Scanner-driven splits override it
166
    // with SplitReadOptions::condition_cache_digest after collecting late-arrival runtime filters.
167
    // A zero digest disables condition cache.
168
    uint64_t condition_cache_digest = 0;
169
};
170
171
struct SplitReadOptions {
172
    // 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.
173
    std::map<std::string, Field> partition_values;
174
    // Latest scanner conjuncts rewritten to table/global column indices. Runtime filters may
175
    // arrive after TableReader::init(), so scanner-driven splits replace the initial snapshot.
176
    // nullopt preserves the initial snapshot for standalone TableReader callers.
177
    std::optional<VExprContextSPtrs> conjuncts = std::nullopt;
178
    // Independent clones used for partition pruning because evaluation prepares and opens them
179
    // against a synthetic partition block before the file reader opens its row-level conjuncts.
180
    VExprContextSPtrs partition_prune_conjuncts;
181
    // Table-level COUNT may emit one metadata-derived batch and resume on a later scheduler turn.
182
    // It is safe only after every runtime filter assigned to the scanner has arrived; otherwise a
183
    // filter could arrive after synthetic rows have already been returned and those rows cannot be
184
    // retracted. Standalone TableReader callers have no scanner runtime-filter lifecycle.
185
    bool all_runtime_filters_applied = true;
186
    // Digest for the exact scanner conjunct snapshot attached to this split. FileScannerV2 rebuilds
187
    // it after collecting late-arrival RFs, so different RF payloads cannot share a cache entry. A
188
    // zero value explicitly disables condition cache for this split.
189
    std::optional<uint64_t> condition_cache_digest;
190
    ShardedKVCache* cache = nullptr;
191
    TFileRangeDesc current_range;
192
    FileFormat current_split_format = FileFormat::PARQUET;
193
    std::optional<GlobalRowIdContext> global_rowid_context;
194
    // Optional absolute file-row selection used by TopN two-phase materialization. TableReader
195
    // carries it unchanged into the format-neutral FileScanRequest.
196
    std::optional<std::vector<int64_t>> row_ids = std::nullopt;
197
};
198
199
// Base class for table-level readers.
200
// This layer owns common table-level orchestration, such as split iteration, dynamic partition
201
// pruning, delete handling and conversion from file-local blocks to table-schema blocks. Concrete
202
// table-format readers only need to provide format-specific hooks for opening readers and parsing
203
// split metadata.
204
class TableReader {
205
public:
206
3.91k
    virtual ~TableReader() = default;
207
208
    // Initialize common runtime options for the table reader. Subclasses may call this from their
209
    // own init(options); table-format schema and split metadata are provided later per split.
210
    virtual Status init(TableReadOptions&& options);
211
212
    // FileScannerV2 adjusts this before each get_block() using an adaptive bytes-per-row estimate.
213
    // Store it here as well as forwarding to the current reader so newly opened split readers start
214
    // with the latest predicted batch size.
215
13.3k
    virtual void set_batch_size(size_t batch_size) {
216
13.3k
        _batch_size = std::max<size_t>(1, batch_size);
217
13.3k
        if (_data_reader.reader != nullptr) {
218
6.71k
            _data_reader.reader->set_batch_size(_batch_size);
219
6.71k
        }
220
13.3k
    }
221
222
#ifdef BE_TEST
223
    size_t TEST_batch_size() const { return _batch_size; }
224
    void TEST_set_condition_cache_hit_count(int64_t hits) { _condition_cache_hit_count = hits; }
225
    bool TEST_current_data_file_is_immutable() const {
226
        DORIS_CHECK(_current_task != nullptr);
227
        DORIS_CHECK(_current_task->data_file != nullptr);
228
        DORIS_CHECK(_current_file_description.has_value());
229
        DORIS_CHECK(_current_task->data_file->is_immutable ==
230
                    _current_file_description->is_immutable);
231
        return _current_task->data_file->is_immutable;
232
    }
233
#endif
234
235
    // Prepare for reading a new split/task.
236
    // 1. Pass a new split/task to reader, which will be used in subsequent open_reader() to initialize the underlying file reader.
237
    // 2. Parse delete predicates from split/task information, which will be used for later dynamic filtering and delete handling.
238
    virtual Status prepare_split(const SplitReadOptions& options);
239
240
    // Refresh row-level predicates for an already prepared split. Physical readers that support
241
    // this operation decide the safe boundary at which the new immutable request becomes active.
242
    virtual Status refresh_conjuncts(VExprContextSPtrs conjuncts);
243
244
3.53k
    virtual bool current_split_pruned() const { return _current_split_pruned; }
245
2.98k
    virtual bool current_split_uses_metadata_count() const {
246
2.98k
        return _current_split_uses_metadata_count;
247
2.98k
    }
248
249
    // Discard the active split after the caller decides an error is ignorable, for example a
250
    // stale external-table file listing that returns NOT_FOUND. The next prepare_split() must start
251
    // with no concrete reader or split-local state left from the failed split.
252
4
    virtual Status abort_split() {
253
        // Ignored open failures still spend time closing partially initialized readers. Include
254
        // that recovery path in the common lifecycle profile so NOT_FOUND cannot become invisible.
255
4
        SCOPED_TIMER(_profile.total_timer);
256
4
        SCOPED_TIMER(_profile.close_timer);
257
4
        if (_data_reader.reader != nullptr) {
258
1
            RETURN_IF_ERROR(close_current_reader());
259
3
        } else {
260
3
            _current_task.reset();
261
3
            _current_file_description.reset();
262
3
        }
263
4
        _delete_rows = nullptr;
264
4
        _remaining_table_level_count = -1;
265
4
        _remaining_file_level_count = -1;
266
4
        _current_split_uses_metadata_count = false;
267
4
        _current_split_pruned = false;
268
4
        return Status::OK();
269
4
    }
270
271
    // Public entry point for reading a table-schema block. The base class opens the current reader,
272
    // advances across EOF, and closes exhausted readers. Subclasses provide protected hooks for
273
    // table-format-specific behavior.
274
10.5k
    virtual Status get_block(Block* block, bool* eos) {
275
10.5k
        SCOPED_TIMER(_profile.total_timer);
276
10.5k
        SCOPED_TIMER(_profile.exec_timer);
277
10.5k
        block->clear_column_data();
278
279
13.9k
        while (true) {
280
13.9k
            if (*eos) {
281
0
                return Status::OK();
282
0
            }
283
13.9k
            if (_io_ctx != nullptr && _io_ctx->should_stop) {
284
0
                *eos = true;
285
0
                return Status::OK();
286
0
            }
287
13.9k
            if (!_data_reader.reader) {
288
7.07k
                if (_is_table_level_count_active()) {
289
6
                    RETURN_IF_ERROR(_read_table_level_count(block, eos));
290
6
                    return Status::OK();
291
6
                }
292
7.07k
                if (_is_file_level_count_active()) {
293
11
                    RETURN_IF_ERROR(_read_file_level_count(block, eos));
294
11
                    return Status::OK();
295
11
                }
296
7.06k
                RETURN_IF_ERROR(create_next_reader(eos));
297
7.05k
                if (!_data_reader.reader) {
298
3.45k
                    DCHECK(*eos);
299
3.45k
                    return Status::OK();
300
3.45k
                }
301
7.05k
            }
302
303
            // Materialize a reduced row set for upper aggregate operators when aggregate
304
            // pushdown can be applied. This is not the final aggregate result: COUNT emits
305
            // `count` default rows for the upper COUNT(*), and MIN/MAX emits two rows containing
306
            // file-level min/max values for the upper MIN/MAX.
307
10.5k
            if (!_aggregate_pushdown_tried) {
308
3.61k
                SCOPED_TIMER(_profile.pushdown_agg_timer);
309
3.61k
                bool pushed_down = false;
310
3.61k
                const auto status = _try_materialize_aggregate_pushdown_rows(block, &pushed_down);
311
3.61k
                if (!status.ok()) {
312
1
                    if (_io_ctx != nullptr && _io_ctx->should_stop &&
313
1
                        status.is<ErrorCode::END_OF_FILE>()) {
314
1
                        *eos = true;
315
1
                        return Status::OK();
316
1
                    }
317
0
                    return status;
318
1
                }
319
3.60k
                if (pushed_down) {
320
15
                    return Status::OK();
321
15
                }
322
3.60k
            }
323
324
10.5k
            bool current_eof = false;
325
10.5k
            _data_reader.block_template.clear_column_data(
326
10.5k
                    cast_set<int64_t>(_data_reader.file_block_layout.size()));
327
10.5k
            size_t current_rows = 0;
328
10.5k
            {
329
10.5k
                SCOPED_TIMER(_profile.file_reader_total_timer);
330
10.5k
                SCOPED_TIMER(_profile.file_reader_get_block_timer);
331
10.5k
                RETURN_IF_ERROR(_data_reader.reader->get_block(&_data_reader.block_template,
332
10.5k
                                                               &current_rows, &current_eof));
333
10.5k
            }
334
10.4k
            const bool stopped_during_read = _io_ctx != nullptr && _io_ctx->should_stop;
335
10.4k
            if (current_rows == 0) {
336
3.45k
                if (current_eof) {
337
3.44k
                    _current_reader_reached_eof = !stopped_during_read;
338
3.44k
                    RETURN_IF_ERROR(close_current_reader());
339
3.44k
                }
340
3.45k
                continue;
341
3.45k
            }
342
10.4k
            DCHECK_EQ(_data_reader.block_template.columns(), _data_reader.file_block_layout.size())
343
0
                    << _data_reader.block_template.dump_structure();
344
7.03k
#ifndef NDEBUG
345
7.03k
            RETURN_IF_ERROR(_check_file_block_columns("after file reader get_block", current_rows));
346
7.03k
#endif
347
7.03k
            RETURN_IF_ERROR(finalize_chunk(block, current_rows));
348
7.02k
            if (current_eof) {
349
33
                _current_reader_reached_eof = !stopped_during_read;
350
33
                RETURN_IF_ERROR(close_current_reader());
351
33
            }
352
7.02k
            return Status::OK();
353
7.02k
        }
354
10.5k
    }
355
356
    // Close the table reader and the currently active file reader. Subclasses that hold additional
357
    // table-format resources should override this and call TableReader::close() first.
358
3.59k
    virtual Status close() {
359
3.59k
        SCOPED_TIMER(_profile.total_timer);
360
3.59k
        SCOPED_TIMER(_profile.close_timer);
361
3.59k
        if (_data_reader.reader) {
362
120
            RETURN_IF_ERROR(close_current_reader());
363
120
        }
364
3.59k
        _current_task.reset();
365
3.59k
        _current_file_description.reset();
366
3.59k
        _remaining_table_level_count = -1;
367
3.59k
        _remaining_file_level_count = -1;
368
3.59k
        _current_split_uses_metadata_count = false;
369
3.59k
        return Status::OK();
370
3.59k
    }
371
372
1.46k
    virtual int64_t condition_cache_hit_count() const { return _condition_cache_hit_count; }
373
374
    virtual std::string debug_string() const;
375
376
    virtual Status annotate_projected_column(const TFileScanSlotInfo& slot_info,
377
                                             ProjectedColumnBuildContext* context,
378
                                             ColumnDefinition* column) const;
379
380
742
    virtual Status validate_projected_columns(const ProjectedColumnBuildContext& context) const {
381
742
        (void)context;
382
742
        return Status::OK();
383
742
    }
384
385
protected:
386
    // TableReader keeps the active file description both in the scan task and separately for
387
    // creating the physical reader. Table-format readers must update both copies when their
388
    // snapshot protocol guarantees that a file path is never overwritten with different bytes.
389
    // This guarantee lets readers safely build cache keys without mtime; it must not be used for
390
    // ordinary Hive/TVF files whose paths may be overwritten in place.
391
197
    void mark_current_data_file_immutable() {
392
197
        DORIS_CHECK(_current_task != nullptr);
393
197
        DORIS_CHECK(_current_task->data_file != nullptr);
394
197
        DORIS_CHECK(_current_file_description.has_value());
395
197
        _current_task->data_file->is_immutable = true;
396
197
        _current_file_description->is_immutable = true;
397
197
    }
398
399
    std::optional<ColumnDefinition> _find_table_column_by_field_id(
400
            int32_t field_id, DataTypePtr type, bool include_historical_schemas) const;
401
    std::optional<std::vector<ColumnDefinition>> _find_table_column_path_by_field_id(
402
            int32_t field_id, DataTypePtr leaf_type, bool include_historical_schemas) const;
403
    std::optional<std::vector<ColumnDefinition>> _find_table_column_identity_path_by_field_id(
404
            int32_t field_id, bool include_historical_schemas) const;
405
406
    // Parse deletion vector information from table format specific file description.
407
    virtual Status _parse_deletion_vector_file(const TTableFormatFileDesc& t_desc,
408
3.46k
                                               DeleteFileDesc* desc, bool* has_delete_file) {
409
3.46k
        *has_delete_file = false;
410
3.46k
        return Status::OK();
411
3.46k
    }
412
413
    // Advance to the next reader. This closes the current reader first and then opens the next
414
    // concrete reader. Subclasses should not duplicate this loop.
415
    Status create_next_reader(bool* eos);
416
    virtual Status create_file_reader(std::unique_ptr<FileReader>* reader);
417
    // Some table formats expose UUID as raw bytes rather than the native Doris UUID type.
418
293
    virtual bool preserve_binary_uuid() const { return false; }
419
838
    virtual TableColumnMappingMode mapping_mode() const { return TableColumnMappingMode::BY_NAME; }
420
3.43k
    virtual void configure_mapper_options(TableColumnMapperOptions*) const {}
421
3.43k
    virtual Status annotate_file_schema(std::vector<ColumnDefinition>* file_schema) {
422
3.43k
        DORIS_CHECK(file_schema != nullptr);
423
3.43k
        return Status::OK();
424
3.43k
    }
425
3.43k
    virtual Status validate_file_mapping(const TableColumnMapper&) const { return Status::OK(); }
426
427
    // Open the concrete reader for the current split/task and build the file-local scan request.
428
3.61k
    virtual Status open_reader() {
429
3.61k
        SCOPED_TIMER(_profile.open_reader_timer);
430
        // 1. Get file schema and create column mapping.
431
3.61k
        std::vector<ColumnDefinition> file_schema;
432
3.61k
        {
433
3.61k
            SCOPED_TIMER(_profile.file_reader_total_timer);
434
3.61k
            SCOPED_TIMER(_profile.file_reader_schema_timer);
435
3.61k
            RETURN_IF_ERROR(_data_reader.reader->get_schema(&file_schema));
436
3.61k
        }
437
        // For Paimon/Hudi, FE can provide field ids through `history_schema_info`. Annotate the
438
        // file schema before column mapping when the table format maps columns by field id.
439
3.61k
        RETURN_IF_ERROR(annotate_file_schema(&file_schema));
440
3.61k
        _data_reader.file_schema = file_schema;
441
3.61k
        _mapper_options.mode = mapping_mode();
442
3.61k
        configure_mapper_options(&_mapper_options);
443
444
3.61k
        {
445
3.61k
            SCOPED_TIMER(_profile.file_reader_total_timer);
446
3.61k
            SCOPED_TIMER(_profile.file_reader_mapper_timer);
447
3.61k
            _data_reader.column_mapper = _data_reader.reader->create_column_mapper(_mapper_options);
448
3.61k
        }
449
3.61k
        DORIS_CHECK(_data_reader.column_mapper != nullptr);
450
3.61k
        RETURN_IF_ERROR(_data_reader.column_mapper->create_mapping(_projected_columns,
451
3.61k
                                                                   _partition_values, file_schema));
452
3.61k
        DORIS_CHECK(_data_reader.column_mapper->mappings().size() == _projected_columns.size());
453
454
        // 2. Build table filters based on conjuncts and column predicates.
455
3.61k
        RETURN_IF_ERROR(_build_table_filters_from_conjuncts());
456
457
        // 3. Create file scan request based on column mapping and table filters, then open file
458
        // reader with the request. File scan request carries row-level expression filters and
459
        // file-level pruning hints. Only expression filters decide returned rows.
460
3.61k
        auto file_request = std::make_shared<FileScanRequest>();
461
3.61k
        RETURN_IF_ERROR(_data_reader.column_mapper->create_scan_request(
462
3.61k
                _table_filters, _projected_columns, file_request.get(), _runtime_state));
463
3.61k
        file_request->row_ids = _row_ids;
464
3.61k
        file_request->allow_row_reordering =
465
3.61k
                std::ranges::all_of(_conjuncts, _is_safe_to_pre_execute);
466
3.61k
        _constant_pruning_safe_filter_count =
467
3.61k
                std::min(_constant_pruning_safe_filter_count,
468
3.61k
                         file_request->constant_pruning_safe_table_filter_count);
469
3.61k
        bool constant_filter_pruned_split = false;
470
3.61k
        RETURN_IF_ERROR(_evaluate_constant_filters(&constant_filter_pruned_split));
471
3.61k
        if (constant_filter_pruned_split) {
472
1
            RETURN_IF_ERROR(close_current_reader());
473
1
            return Status::OK();
474
1
        }
475
        // COUNT(*) has no semantic column argument, but Nereids retains a minimum-width scan slot
476
        // so the scan node still has an output tuple. Record only the current non-predicate file
477
        // columns before table-format hooks add row-position or equality-delete dependencies. This
478
        // marker is independent of aggregate eligibility: with position deletes, for example,
479
        // metadata COUNT must fall back to reading rows, but an arbitrary unsupported TIME_MILLIS
480
        // placeholder still must not be validated or decoded merely to carry the surviving count.
481
        // Pending runtime filters may later target this retained slot, so placeholder values are
482
        // safe only after every filter for the split has arrived.
483
3.61k
        if (_push_down_agg_type == TPushAggOp::type::COUNT &&
484
3.61k
            _push_down_count_columns.has_value() && _push_down_count_columns->empty() &&
485
3.61k
            _all_runtime_filters_applied_for_split) {
486
16
            file_request->count_star_placeholder_columns.reserve(
487
16
                    file_request->non_predicate_columns.size());
488
16
            for (const auto& column : file_request->non_predicate_columns) {
489
13
                file_request->count_star_placeholder_columns.push_back(column.column_id());
490
13
            }
491
16
        }
492
3.61k
        RETURN_IF_ERROR(customize_file_scan_request(file_request.get()));
493
3.61k
        RETURN_IF_ERROR(_data_reader.column_mapper->reconcile_scan_request_after_customization(
494
3.61k
                file_request.get()));
495
3.61k
        RETURN_IF_ERROR(validate_file_mapping(*_data_reader.column_mapper));
496
3.60k
        RETURN_IF_ERROR(_open_local_filter_exprs(*file_request));
497
3.60k
        _data_reader.file_block_layout.clear();
498
3.60k
        _data_reader.block_template.clear();
499
3.60k
        _file_scan_request.reset();
500
3.60k
        _data_reader.file_block_layout.resize(file_request->block_column_count());
501
502
        // 4. Build file block layout from file schema and column mapping. The layout describes
503
        // the block returned by file reader before table-column materialization.
504
3.60k
        auto add_file_block_column = [&](const LocalColumnIndex& projection,
505
23.1k
                                         LocalIndex block_position) -> Status {
506
23.1k
            DORIS_CHECK(block_position.value() < _data_reader.file_block_layout.size());
507
23.1k
            const auto file_column_id = projection.column_id();
508
23.1k
            const auto* field = _find_column_definition(_data_reader.file_schema, file_column_id);
509
23.1k
            DORIS_CHECK(field != nullptr);
510
511
23.1k
            ColumnDefinition projected_field;
512
23.1k
            RETURN_IF_ERROR(project_column_definition(*field, projection, &projected_field));
513
23.1k
            _data_reader.file_block_layout[block_position.value()] = {
514
23.1k
                    .file_column_id = file_column_id,
515
23.1k
                    .name = projected_field.name,
516
23.1k
                    .type = projected_field.type,
517
23.1k
            };
518
23.1k
            DORIS_CHECK(_data_reader.file_block_layout[block_position.value()].type != nullptr);
519
23.1k
            return Status::OK();
520
23.1k
        };
521
3.60k
        for (const auto& projection : file_request->predicate_columns) {
522
242
            RETURN_IF_ERROR(add_file_block_column(
523
242
                    projection, file_request->local_positions.at(projection.column_id())));
524
242
        }
525
22.8k
        for (const auto& projection : file_request->non_predicate_columns) {
526
22.8k
            RETURN_IF_ERROR(add_file_block_column(
527
22.8k
                    projection, file_request->non_predicate_position(projection.column_id())));
528
22.8k
        }
529
530
        // 5. Prepare block template from file block layout. The block template stores the block
531
        // returned by file reader before table-column materialization.
532
3.60k
        _data_reader.block_template.reserve(_data_reader.file_block_layout.size());
533
23.1k
        for (const auto& column : _data_reader.file_block_layout) {
534
23.1k
            _data_reader.block_template.insert(
535
23.1k
                    {column.type->create_column(), column.type, column.name});
536
23.1k
        }
537
3.60k
        if (VLOG_DEBUG_IS_ON) {
538
0
            VLOG_DEBUG << "TableReader debug: " << debug_string();
539
0
        }
540
3.60k
        RETURN_IF_ERROR(_open_mapping_exprs());
541
3.60k
        {
542
3.60k
            SCOPED_TIMER(_profile.file_reader_total_timer);
543
3.60k
            SCOPED_TIMER(_profile.file_reader_open_timer);
544
3.60k
            RETURN_IF_ERROR(_data_reader.reader->open(file_request));
545
3.60k
        }
546
3.60k
        _file_scan_request = std::move(file_request);
547
3.60k
        RETURN_IF_ERROR(_init_reader_condition_cache(*_file_scan_request));
548
3.60k
        return Status::OK();
549
3.60k
    }
550
551
    Status _build_table_filters_from_conjuncts();
552
    Status _evaluate_partition_prune_conjuncts(const VExprContextSPtrs& conjuncts,
553
                                               bool* can_filter_all);
554
    static bool _is_safe_to_pre_execute(const VExprContextSPtr& conjunct);
555
    Status _build_partition_prune_block(Block* block) const;
556
    Status _open_local_filter_exprs(const FileScanRequest& file_request);
557
    Status _init_reader_condition_cache(const FileScanRequest& file_request);
558
    void _finalize_reader_condition_cache();
559
    // The condition cache key covers the file identity plus the table-level predicate/session
560
    // digest only. A table format whose file-column binding can change without changing that digest
561
    // (Iceberg's positional fallback resolves an ID-less file by physical ordinals) must override
562
    // this to false: a stale all-false granule would silently drop matching rows, because the
563
    // cached verdict was produced by a different physical binding.
564
    virtual bool _should_enable_condition_cache(const FileScanRequest& file_request) const;
565
566
3.61k
    Status _evaluate_constant_filters(bool* can_filter_all) {
567
3.61k
        DORIS_CHECK(can_filter_all != nullptr);
568
3.61k
        DORIS_CHECK_LE(_constant_pruning_safe_filter_count, _table_filters.size());
569
3.61k
        *can_filter_all = false;
570
        // The bound was derived from the original `_conjuncts` order, which includes slotless
571
        // expressions omitted from `_table_filters`. Iterating only this prefix therefore cannot
572
        // skip an unsafe row-level predicate and pre-execute a later constant predicate.
573
3.75k
        for (size_t i = 0; i < _constant_pruning_safe_filter_count; ++i) {
574
140
            const auto& table_filter = _table_filters[i];
575
140
            if (table_filter.conjunct == nullptr) {
576
0
                continue;
577
0
            }
578
140
            DORIS_CHECK(_is_safe_to_pre_execute(table_filter.conjunct));
579
            // RuntimeFilterExpr does not implement execute_column_impl(); it is evaluated by the
580
            // row-level filter path through execute_filter(). Constant split pruning uses
581
            // VExprContext::execute() on a one-row synthetic block, so runtime filters must not be
582
            // pre-executed here even when their referenced slot maps to a constant value.
583
140
            if (table_filter.conjunct->root()->is_rf_wrapper() ||
584
140
                !_table_filter_has_only_constant_entries(table_filter)) {
585
138
                continue;
586
138
            }
587
2
            Block eval_block;
588
2
            RETURN_IF_ERROR(_build_constant_filter_block(table_filter, &eval_block));
589
2
            RowDescriptor row_desc;
590
2
            RETURN_IF_ERROR(table_filter.conjunct->prepare(_runtime_state, row_desc));
591
2
            RETURN_IF_ERROR(table_filter.conjunct->open(_runtime_state));
592
2
            int result_column_id = -1;
593
2
            RETURN_IF_ERROR(table_filter.conjunct->execute(&eval_block, &result_column_id));
594
2
            DORIS_CHECK(result_column_id >= 0);
595
2
            if (_filter_result_filters_all(eval_block.get_by_position(result_column_id).column)) {
596
1
                *can_filter_all = true;
597
1
                return Status::OK();
598
1
            }
599
2
        }
600
3.61k
        return Status::OK();
601
3.61k
    }
602
603
135
    bool _table_filter_has_only_constant_entries(const TableFilter& table_filter) const {
604
135
        const auto& filter_entries = _data_reader.column_mapper->filter_entries();
605
135
        for (const auto global_index : table_filter.global_indices) {
606
134
            const auto entry_it = filter_entries.find(global_index);
607
134
            if (entry_it == filter_entries.end() || !entry_it->second.is_constant()) {
608
132
                return false;
609
132
            }
610
134
        }
611
3
        return !table_filter.global_indices.empty();
612
135
    }
613
614
2
    Status _build_constant_filter_block(const TableFilter& table_filter, Block* eval_block) {
615
2
        DORIS_CHECK(eval_block != nullptr);
616
2
        eval_block->clear();
617
2
        const auto& mappings = _data_reader.column_mapper->mappings();
618
2
        const auto& filter_entries = _data_reader.column_mapper->filter_entries();
619
2
        DORIS_CHECK(mappings.size() == _projected_columns.size());
620
4
        for (size_t column_idx = 0; column_idx < mappings.size(); ++column_idx) {
621
2
            const auto global_index = GlobalIndex(column_idx);
622
2
            const auto& mapping = mappings[column_idx];
623
2
            const auto entry_it = filter_entries.find(global_index);
624
2
            const bool referenced_by_filter =
625
2
                    std::find(table_filter.global_indices.begin(),
626
2
                              table_filter.global_indices.end(),
627
2
                              global_index) != table_filter.global_indices.end();
628
2
            if (referenced_by_filter && entry_it != filter_entries.end() &&
629
2
                entry_it->second.is_constant()) {
630
2
                ColumnPtr constant_column;
631
2
                RETURN_IF_ERROR(_materialize_constant_filter_column(
632
2
                        entry_it->second.constant_index(), &constant_column));
633
2
                eval_block->insert({std::move(constant_column), mapping.table_type,
634
2
                                    mapping.table_column_name});
635
2
            } else {
636
0
                eval_block->insert({mapping.table_type->create_column_const_with_default_value(1),
637
0
                                    mapping.table_type, mapping.table_column_name});
638
0
            }
639
2
        }
640
2
        return Status::OK();
641
2
    }
642
643
2
    Status _materialize_constant_filter_column(ConstantIndex constant_index, ColumnPtr* column) {
644
2
        DORIS_CHECK(column != nullptr);
645
2
        const auto& constant_entry = _data_reader.column_mapper->constant_map().get(constant_index);
646
2
        DORIS_CHECK(constant_entry.expr != nullptr);
647
2
        DORIS_CHECK(constant_entry.type != nullptr);
648
2
        RowDescriptor row_desc;
649
2
        RETURN_IF_ERROR(constant_entry.expr->prepare(_runtime_state, row_desc));
650
2
        RETURN_IF_ERROR(constant_entry.expr->open(_runtime_state));
651
2
        Block eval_block;
652
2
        eval_block.insert({constant_entry.type->create_column_const_with_default_value(1),
653
2
                           constant_entry.type, "__table_reader_constant_filter"});
654
2
        int result_column_id = -1;
655
2
        RETURN_IF_ERROR(constant_entry.expr->execute(&eval_block, &result_column_id));
656
2
        DORIS_CHECK(result_column_id >= 0);
657
2
        *column = eval_block.get_by_position(result_column_id).column;
658
2
        DORIS_CHECK((*column)->size() == 1);
659
2
        return Status::OK();
660
2
    }
661
662
2
    static bool _filter_result_filters_all(const ColumnPtr& filter_column) {
663
2
        DORIS_CHECK(filter_column.get() != nullptr);
664
2
        DORIS_CHECK(filter_column->size() == 1);
665
2
        return !filter_column->get_bool(0);
666
2
    }
667
668
1.02k
    virtual Status customize_file_scan_request(FileScanRequest* file_request) {
669
1.02k
        return _append_delete_predicate(file_request);
670
1.02k
    }
671
672
10.9k
    bool _is_table_level_count_active() const { return _remaining_table_level_count >= 0; }
673
674
7.07k
    bool _is_file_level_count_active() const { return _remaining_file_level_count >= 0; }
675
676
23
    Status _materialize_count_rows(size_t rows, Block* block) const {
677
23
        DORIS_CHECK(block != nullptr);
678
23
        DORIS_CHECK(block->columns() > 0 || rows == 0);
679
46
        for (size_t column_idx = 0; column_idx < block->columns(); ++column_idx) {
680
23
            auto column = block->get_by_position(column_idx).type->create_column();
681
23
            if (auto* nullable = check_and_get_column<ColumnNullable>(*column)) {
682
                // Metadata COUNT emits synthetic input rows for the unchanged upper aggregate.
683
                // They must be non-NULL for COUNT(nullable_col), and constructing them explicitly
684
                // also keeps every nullable null map boolean-valid in debug/ASAN block checks.
685
23
                nullable->get_nested_column().insert_many_defaults(rows);
686
23
                nullable->get_null_map_data().resize_fill(rows, 0);
687
23
            } else {
688
0
                column->insert_many_defaults(rows);
689
0
            }
690
23
            block->replace_by_position(column_idx, std::move(column));
691
23
        }
692
23
        return Status::OK();
693
23
    }
694
695
23
    Status _materialize_next_count_batch(int64_t* remaining_rows, Block* block) const {
696
23
        DORIS_CHECK(remaining_rows != nullptr);
697
23
        DORIS_CHECK(*remaining_rows > 0);
698
23
        const int64_t batch_size = _runtime_state == nullptr
699
23
                                           ? *remaining_rows
700
23
                                           : static_cast<int64_t>(_runtime_state->batch_size());
701
23
        const auto rows = std::min(*remaining_rows, batch_size);
702
23
        RETURN_IF_ERROR(_materialize_count_rows(cast_set<size_t>(rows), block));
703
23
        *remaining_rows -= rows;
704
23
        return Status::OK();
705
23
    }
706
707
20
    Status _read_count_batch(int64_t* remaining_rows, Block* block, bool* eos) {
708
20
        DORIS_CHECK(block != nullptr);
709
20
        DORIS_CHECK(eos != nullptr);
710
20
        DORIS_CHECK(_push_down_agg_type == TPushAggOp::type::COUNT);
711
20
        DORIS_CHECK(remaining_rows != nullptr);
712
20
        DORIS_CHECK(*remaining_rows >= 0);
713
20
        if (*remaining_rows == 0) {
714
9
            *remaining_rows = -1;
715
9
            _current_task.reset();
716
9
            *eos = true;
717
9
            return Status::OK();
718
9
        }
719
11
        RETURN_IF_ERROR(_materialize_next_count_batch(remaining_rows, block));
720
11
        *eos = false;
721
11
        return Status::OK();
722
11
    }
723
724
9
    Status _read_table_level_count(Block* block, bool* eos) {
725
9
        return _read_count_batch(&_remaining_table_level_count, block, eos);
726
9
    }
727
728
11
    Status _read_file_level_count(Block* block, bool* eos) {
729
11
        return _read_count_batch(&_remaining_file_level_count, block, eos);
730
11
    }
731
732
    void _append_file_scan_column(FileScanRequest* request, LocalColumnId column_id,
733
129
                                  std::vector<LocalColumnIndex>* scan_columns) {
734
129
        DORIS_CHECK(request != nullptr);
735
129
        DORIS_CHECK(scan_columns != nullptr);
736
129
        FileScanRequestBuilder builder(request);
737
129
        Status status;
738
129
        if (scan_columns == &request->predicate_columns) {
739
114
            status = builder.add_predicate_column(column_id);
740
114
        } else {
741
15
            DORIS_CHECK(scan_columns == &request->non_predicate_columns);
742
15
            status = builder.add_non_predicate_column(column_id);
743
15
        }
744
129
        DORIS_CHECK(status.ok()) << status.to_string();
745
129
        if (column_id == LocalColumnId(ROW_POSITION_COLUMN_ID) &&
746
129
            _find_column_definition(_data_reader.file_schema, column_id) == nullptr) {
747
60
            _data_reader.file_schema.push_back(row_position_column_definition());
748
60
        }
749
129
    }
750
751
    // 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.
752
1.02k
    Status _append_delete_predicate(FileScanRequest* request) {
753
1.02k
        DORIS_CHECK(request != nullptr);
754
1.02k
        if ((_delete_rows == nullptr || _delete_rows->empty()) &&
755
1.02k
            (_deletion_vector == nullptr || _deletion_vector->isEmpty())) {
756
1.00k
            return Status::OK();
757
1.00k
        }
758
17
        const auto row_position_column_id = LocalColumnId(ROW_POSITION_COLUMN_ID);
759
17
        _append_file_scan_column(request, row_position_column_id, &request->predicate_columns);
760
761
17
        const auto block_position = request->local_positions.at(row_position_column_id);
762
17
        auto append_predicate = [&](auto& deleted_rows) {
763
17
            auto delete_predicate = std::make_shared<DeletePredicate>(deleted_rows);
764
17
            delete_predicate->add_child(VSlotRef::create_shared(
765
17
                    cast_set<int>(block_position.value()), cast_set<int>(block_position.value()),
766
17
                    -1, std::make_shared<DataTypeInt64>(), ROW_POSITION_COLUMN_NAME));
767
17
            request->delete_conjuncts.push_back(
768
17
                    VExprContext::create_shared(std::move(delete_predicate)));
769
17
        };
_ZZN5doris6format11TableReader24_append_delete_predicateEPNS0_15FileScanRequestEENKUlRT_E_clISt6vectorIlSaIlEEEEDaS5_
Line
Count
Source
762
13
        auto append_predicate = [&](auto& deleted_rows) {
763
13
            auto delete_predicate = std::make_shared<DeletePredicate>(deleted_rows);
764
13
            delete_predicate->add_child(VSlotRef::create_shared(
765
13
                    cast_set<int>(block_position.value()), cast_set<int>(block_position.value()),
766
13
                    -1, std::make_shared<DataTypeInt64>(), ROW_POSITION_COLUMN_NAME));
767
13
            request->delete_conjuncts.push_back(
768
13
                    VExprContext::create_shared(std::move(delete_predicate)));
769
13
        };
_ZZN5doris6format11TableReader24_append_delete_predicateEPNS0_15FileScanRequestEENKUlRT_E_clIN7roaring12Roaring64MapEEEDaS5_
Line
Count
Source
762
4
        auto append_predicate = [&](auto& deleted_rows) {
763
4
            auto delete_predicate = std::make_shared<DeletePredicate>(deleted_rows);
764
4
            delete_predicate->add_child(VSlotRef::create_shared(
765
4
                    cast_set<int>(block_position.value()), cast_set<int>(block_position.value()),
766
4
                    -1, std::make_shared<DataTypeInt64>(), ROW_POSITION_COLUMN_NAME));
767
4
            request->delete_conjuncts.push_back(
768
4
                    VExprContext::create_shared(std::move(delete_predicate)));
769
4
        };
770
17
        if (_delete_rows != nullptr && !_delete_rows->empty()) {
771
13
            append_predicate(*_delete_rows);
772
13
        }
773
17
        if (_deletion_vector != nullptr && !_deletion_vector->isEmpty()) {
774
4
            append_predicate(*_deletion_vector);
775
4
        }
776
17
        return Status::OK();
777
1.02k
    }
778
779
    // Close the current concrete reader. This hook is called by both create_next_reader() and
780
    // close(), so it should remain idempotent.
781
3.61k
    virtual Status close_current_reader() {
782
3.61k
        _finalize_reader_condition_cache();
783
3.61k
        {
784
3.61k
            SCOPED_TIMER(_profile.file_reader_total_timer);
785
3.61k
            SCOPED_TIMER(_profile.file_reader_close_timer);
786
3.61k
            RETURN_IF_ERROR(_data_reader.reader->close());
787
3.61k
        }
788
3.61k
        _data_reader.reader.reset();
789
3.61k
        if (_data_reader.column_mapper != nullptr) {
790
3.61k
            _data_reader.column_mapper->clear();
791
3.61k
            _data_reader.column_mapper.reset();
792
3.61k
        }
793
3.61k
        _table_filters.clear();
794
3.61k
        _constant_pruning_safe_filter_count = 0;
795
3.61k
        _data_reader.file_schema.clear();
796
3.61k
        _data_reader.file_block_layout.clear();
797
3.61k
        _data_reader.block_template.clear();
798
3.61k
        _file_scan_request.reset();
799
3.61k
        _current_task.reset();
800
3.61k
        _current_file_description.reset();
801
3.61k
        _current_reader_reached_eof = false;
802
3.61k
        return Status::OK();
803
3.61k
    }
804
805
43
    void _record_scan_rows(size_t rows) {
806
43
        if (_io_ctx != nullptr && _io_ctx->file_reader_stats != nullptr) {
807
4
            _io_ctx->file_reader_stats->read_rows += rows;
808
4
        }
809
43
    }
810
811
    // Materialize the projected schema. Subclasses may then transform its columns and rows
812
    // into their output schema; the active reader remains alive until this hook returns.
813
7.03k
    virtual Status finalize_chunk(Block* block, const size_t rows) {
814
7.03k
        SCOPED_TIMER(_profile.finalize_timer);
815
7.03k
        DORIS_CHECK(block->columns() == _projected_columns.size());
816
7.03k
        DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
817
7.03k
        size_t idx = 0;
818
7.03k
        const auto& mappings = _data_reader.column_mapper->mappings();
819
70.8k
        for (const auto& mapping : mappings) {
820
70.8k
            ColumnPtr column;
821
70.8k
            RETURN_IF_ERROR(_materialize_mapping_column(mapping, &_data_reader.block_template, rows,
822
70.8k
                                                        &column, idx + 1 == mappings.size()));
823
70.8k
            block->replace_by_position(idx, IColumn::mutate(std::move(column)));
824
70.8k
            idx++;
825
70.8k
        }
826
7.03k
        RETURN_IF_ERROR(materialize_virtual_columns(block));
827
        // Enforce CHAR/VARCHAR length declared by the table schema after all file-to-table
828
        // materialization has finished.
829
7.03k
        RETURN_IF_ERROR(_truncate_char_or_varchar_columns(block));
830
7.03k
#ifndef NDEBUG
831
        // Validate the materialized schema before a subclass transforms rows or output columns.
832
7.03k
        RETURN_IF_ERROR(_check_table_block_columns("after table materialization", block, rows));
833
7.03k
#endif
834
7.03k
        return Status::OK();
835
7.03k
    }
836
837
    // Materialize virtual columns in the table block, such as Iceberg _row_id and
838
    // _last_updated_sequence_number. This runs after normal column materialization so finalize
839
    // expressions can reference those virtual columns.
840
6.87k
    virtual Status materialize_virtual_columns(Block* table_block) { return Status::OK(); }
841
842
#ifndef NDEBUG
843
7.03k
    Status _check_file_block_columns(std::string_view stage, size_t rows) {
844
7.03k
        DORIS_CHECK(_data_reader.block_template.columns() == _data_reader.file_block_layout.size());
845
77.8k
        for (size_t idx = 0; idx < _data_reader.block_template.columns(); ++idx) {
846
70.8k
            const auto& file_block_column = _data_reader.file_block_layout[idx];
847
70.8k
            const auto& column_with_type = _data_reader.block_template.get_by_position(idx);
848
70.8k
            const auto* column = column_with_type.column.get();
849
70.8k
            try {
850
70.8k
                if (column == nullptr) {
851
0
                    auto st = Status::InternalError(
852
0
                            "Invalid file block column {} at {}: file_column_id={}, name='{}', "
853
0
                            "type={}, column=null, expected_rows={}, reader={}",
854
0
                            idx, stage, file_block_column.file_column_id.value(),
855
0
                            file_block_column.name,
856
0
                            file_block_column.type == nullptr ? "null"
857
0
                                                              : file_block_column.type->get_name(),
858
0
                            rows, debug_string());
859
0
                    LOG(WARNING) << st;
860
0
                    return st;
861
0
                }
862
70.8k
                column->sanity_check();
863
70.8k
                auto st = column_with_type.check_type_and_column_match();
864
70.8k
                if (!st.ok()) {
865
0
                    auto contextual_status = Status::InternalError(
866
0
                            "Invalid file block column {} at {}: file_column_id={}, name='{}', "
867
0
                            "type={}, column={}, column_size={}, expected_rows={}, error={}, "
868
0
                            "reader={}",
869
0
                            idx, stage, file_block_column.file_column_id.value(),
870
0
                            file_block_column.name,
871
0
                            file_block_column.type == nullptr ? "null"
872
0
                                                              : file_block_column.type->get_name(),
873
0
                            column->get_name(), column->size(), rows, st.to_string(),
874
0
                            debug_string());
875
0
                    LOG(WARNING) << contextual_status;
876
0
                    return contextual_status;
877
0
                }
878
70.8k
            } catch (const Exception& e) {
879
0
                auto st = Status::InternalError(
880
0
                        "Invalid file block column {} at {}: file_column_id={}, name='{}', "
881
0
                        "type={}, column={}, column_size={}, expected_rows={}, error={}, "
882
0
                        "reader={}",
883
0
                        idx, stage, file_block_column.file_column_id.value(),
884
0
                        file_block_column.name,
885
0
                        file_block_column.type == nullptr ? "null"
886
0
                                                          : file_block_column.type->get_name(),
887
0
                        column == nullptr ? "null" : column->get_name(),
888
0
                        column == nullptr ? 0 : column->size(), rows, e.to_string(),
889
0
                        debug_string());
890
0
                LOG(WARNING) << st;
891
0
                return st;
892
0
            } catch (const std::exception& e) {
893
0
                auto st = Status::InternalError(
894
0
                        "Invalid file block column {} at {}: file_column_id={}, name='{}', "
895
0
                        "type={}, column={}, column_size={}, expected_rows={}, error={}, "
896
0
                        "reader={}",
897
0
                        idx, stage, file_block_column.file_column_id.value(),
898
0
                        file_block_column.name,
899
0
                        file_block_column.type == nullptr ? "null"
900
0
                                                          : file_block_column.type->get_name(),
901
0
                        column == nullptr ? "null" : column->get_name(),
902
0
                        column == nullptr ? 0 : column->size(), rows, e.what(), debug_string());
903
0
                LOG(WARNING) << st;
904
0
                return st;
905
0
            }
906
70.8k
        }
907
7.03k
        return Status::OK();
908
7.03k
    }
909
910
7.03k
    Status _check_table_block_columns(std::string_view stage, const Block* block, size_t rows) {
911
7.03k
        DORIS_CHECK(block != nullptr);
912
7.03k
        DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
913
77.8k
        for (size_t idx = 0; idx < block->columns(); ++idx) {
914
70.8k
            const auto& mapping = _data_reader.column_mapper->mappings()[idx];
915
70.8k
            const auto& column_with_type = block->get_by_position(idx);
916
70.8k
            const auto* column = column_with_type.column.get();
917
70.8k
            try {
918
70.8k
                if (column == nullptr) {
919
0
                    auto st = Status::InternalError(
920
0
                            "Invalid table block column {} at {}: table_column='{}', "
921
0
                            "global_index={}, type={}, column=null, expected_rows={}, mapping={}",
922
0
                            idx, stage, mapping.table_column_name, mapping.global_index.value(),
923
0
                            mapping.table_type == nullptr ? "null" : mapping.table_type->get_name(),
924
0
                            rows, mapping.debug_string());
925
0
                    LOG(WARNING) << st;
926
0
                    return st;
927
0
                }
928
70.8k
                column->sanity_check();
929
70.8k
                auto st = column_with_type.check_type_and_column_match();
930
70.8k
                if (!st.ok()) {
931
0
                    auto contextual_status = Status::InternalError(
932
0
                            "Invalid table block column {} at {}: table_column='{}', "
933
0
                            "global_index={}, type={}, column={}, column_size={}, "
934
0
                            "expected_rows={}, error={}, mapping={}",
935
0
                            idx, stage, mapping.table_column_name, mapping.global_index.value(),
936
0
                            mapping.table_type == nullptr ? "null" : mapping.table_type->get_name(),
937
0
                            column->get_name(), column->size(), rows, st.to_string(),
938
0
                            mapping.debug_string());
939
0
                    LOG(WARNING) << contextual_status;
940
0
                    return contextual_status;
941
0
                }
942
70.8k
            } catch (const Exception& e) {
943
0
                auto st = Status::InternalError(
944
0
                        "Invalid table block column {} at {}: table_column='{}', global_index={}, "
945
0
                        "type={}, column={}, column_size={}, expected_rows={}, error={}, "
946
0
                        "mapping={}",
947
0
                        idx, stage, mapping.table_column_name, mapping.global_index.value(),
948
0
                        mapping.table_type == nullptr ? "null" : mapping.table_type->get_name(),
949
0
                        column == nullptr ? "null" : column->get_name(),
950
0
                        column == nullptr ? 0 : column->size(), rows, e.to_string(),
951
0
                        mapping.debug_string());
952
0
                LOG(WARNING) << st;
953
0
                return st;
954
0
            } catch (const std::exception& e) {
955
0
                auto st = Status::InternalError(
956
0
                        "Invalid table block column {} at {}: table_column='{}', global_index={}, "
957
0
                        "type={}, column={}, column_size={}, expected_rows={}, error={}, "
958
0
                        "mapping={}",
959
0
                        idx, stage, mapping.table_column_name, mapping.global_index.value(),
960
0
                        mapping.table_type == nullptr ? "null" : mapping.table_type->get_name(),
961
0
                        column == nullptr ? "null" : column->get_name(),
962
0
                        column == nullptr ? 0 : column->size(), rows, e.what(),
963
0
                        mapping.debug_string());
964
0
                LOG(WARNING) << st;
965
0
                return st;
966
0
            }
967
70.8k
        }
968
7.03k
        return Status::OK();
969
7.03k
    }
970
#endif
971
972
7.02k
    Status _truncate_char_or_varchar_columns(Block* block) {
973
7.02k
        DORIS_CHECK(block != nullptr);
974
7.02k
        if (_runtime_state == nullptr ||
975
7.03k
            !_runtime_state->query_options().truncate_char_or_varchar_columns) {
976
7.03k
            return Status::OK();
977
7.03k
        }
978
18.4E
        DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
979
18.4E
        for (size_t idx = 0; idx < _data_reader.column_mapper->mappings().size(); ++idx) {
980
0
            const auto& mapping = _data_reader.column_mapper->mappings()[idx];
981
0
            if (!_should_truncate_char_or_varchar_column(mapping)) {
982
0
                continue;
983
0
            }
984
0
            const auto target_len =
985
0
                    assert_cast<const DataTypeString*>(remove_nullable(mapping.table_type).get())
986
0
                            ->len();
987
0
            _truncate_char_or_varchar_column(block, idx, target_len);
988
0
        }
989
18.4E
        return Status::OK();
990
7.02k
    }
991
992
    // Return true when the table schema has a bounded CHAR/VARCHAR length that is stricter than
993
    // the file-side type. Examples:
994
    // - table VARCHAR(10), file VARCHAR(20): truncate to 10;
995
    // - table VARCHAR(10), file STRING: truncate to 10 because STRING has no declared bound;
996
    // - table STRING, any file type: no truncation because the target has no bound.
997
5
    static bool _should_truncate_char_or_varchar_column(const ColumnMapping& mapping) {
998
5
        if (mapping.table_type == nullptr) {
999
0
            return false;
1000
0
        }
1001
5
        const auto table_type = remove_nullable(mapping.table_type);
1002
5
        const auto primitive_type = table_type->get_primitive_type();
1003
5
        if (primitive_type != TYPE_VARCHAR && primitive_type != TYPE_CHAR) {
1004
1
            return false;
1005
1
        }
1006
4
        const auto target_len = assert_cast<const DataTypeString*>(table_type.get())->len();
1007
4
        if (target_len <= 0) {
1008
0
            return false;
1009
0
        }
1010
4
        if (mapping.file_type == nullptr) {
1011
0
            return true;
1012
0
        }
1013
4
        const auto file_type = remove_nullable(mapping.file_type);
1014
4
        DORIS_CHECK(file_type != nullptr);
1015
4
        int file_len = -1;
1016
4
        if (file_type->get_primitive_type() == TYPE_VARCHAR ||
1017
4
            file_type->get_primitive_type() == TYPE_CHAR ||
1018
4
            file_type->get_primitive_type() == TYPE_STRING) {
1019
3
            file_len = assert_cast<const DataTypeString*>(file_type.get())->len();
1020
3
        }
1021
1022
4
        return file_len < 0 || target_len < file_len;
1023
4
    }
1024
1025
    // Truncate a materialized CHAR/VARCHAR column in place by reusing the vectorized substring
1026
    // implementation: substring(column, 1, len). Nullable columns are unwrapped before substring
1027
    // execution and wrapped back with the original null map afterward, because substring operates
1028
    // on the nested string payload only.
1029
10
    static void _truncate_char_or_varchar_column(Block* block, size_t idx, int len) {
1030
10
        DORIS_CHECK(block != nullptr);
1031
10
        auto int_type = std::make_shared<DataTypeInt32>();
1032
10
        const auto num_columns_without_result = cast_set<uint32_t>(block->columns());
1033
10
        auto& target = block->get_by_position(idx);
1034
10
        const bool is_nullable = target.type->is_nullable();
1035
10
        ColumnPtr input_column = target.column;
1036
10
        ColumnPtr null_map_column;
1037
10
        if (is_nullable) {
1038
10
            const auto* nullable_column = assert_cast<const ColumnNullable*>(target.column.get());
1039
10
            input_column = nullable_column->get_nested_column_ptr();
1040
10
            null_map_column = nullable_column->get_null_map_column_ptr();
1041
10
        }
1042
10
        block->replace_by_position(idx, std::move(input_column));
1043
10
        block->insert({int_type->create_column_const(block->rows(), to_field<TYPE_INT>(1)),
1044
10
                       int_type, "const 1"});
1045
10
        block->insert({int_type->create_column_const(block->rows(), to_field<TYPE_INT>(len)),
1046
10
                       int_type, "const len"});
1047
10
        block->insert({nullptr, std::make_shared<DataTypeString>(), "result"});
1048
1049
10
        ColumnNumbers temp_arguments(3);
1050
10
        temp_arguments[0] = cast_set<uint32_t>(idx);
1051
10
        temp_arguments[1] = num_columns_without_result;
1052
10
        temp_arguments[2] = num_columns_without_result + 1;
1053
10
        const uint32_t result_column_id = num_columns_without_result + 2;
1054
10
        SubstringUtil::substring_execute(*block, temp_arguments, result_column_id, block->rows());
1055
1056
10
        ColumnPtr result_column = block->get_by_position(result_column_id).column;
1057
10
        if (is_nullable) {
1058
10
            result_column = ColumnNullable::create(std::move(result_column), null_map_column);
1059
10
        }
1060
10
        block->replace_by_position(idx, std::move(result_column));
1061
10
        block->erase_tail(num_columns_without_result);
1062
10
    }
1063
1064
3.60k
    Status _try_materialize_aggregate_pushdown_rows(Block* block, bool* pushed_down) {
1065
3.60k
        DORIS_CHECK(block != nullptr);
1066
3.60k
        DORIS_CHECK(pushed_down != nullptr);
1067
3.60k
        *pushed_down = false;
1068
3.60k
        _aggregate_pushdown_tried = true;
1069
3.60k
        if (_row_ids.has_value() || !_supports_aggregate_pushdown(_push_down_agg_type)) {
1070
3.58k
            return Status::OK();
1071
3.58k
        }
1072
15
        block->clear_column_data(_projected_columns.size());
1073
1074
15
        FileAggregateRequest file_request;
1075
15
        RETURN_IF_ERROR(_build_file_aggregate_request(_push_down_agg_type, &file_request));
1076
15
        FileAggregateResult file_result;
1077
15
        Status status;
1078
15
        {
1079
15
            SCOPED_TIMER(_profile.file_reader_total_timer);
1080
15
            SCOPED_TIMER(_profile.file_reader_aggregate_timer);
1081
15
            status = _data_reader.reader->get_aggregate_result(file_request, &file_result);
1082
15
        }
1083
15
        if (status.is<ErrorCode::NOT_IMPLEMENTED_ERROR>()) {
1084
1
            return Status::OK();
1085
1
        }
1086
14
        RETURN_IF_ERROR(status);
1087
13
        if (_push_down_agg_type == TPushAggOp::type::COUNT) {
1088
12
            DORIS_CHECK(file_result.count >= 0);
1089
            // The upper aggregate consumes synthetic input rows, but emitting the whole metadata
1090
            // count in one block bypasses the runtime batch contract and can allocate by file size.
1091
            // Keep the remaining cardinality as split state and expose at most one batch per call.
1092
12
            _remaining_file_level_count = file_result.count;
1093
12
            _current_split_uses_metadata_count = true;
1094
12
            if (_remaining_file_level_count > 0) {
1095
12
                RETURN_IF_ERROR(_materialize_next_count_batch(&_remaining_file_level_count, block));
1096
12
            }
1097
12
        } else {
1098
1
            RETURN_IF_ERROR(
1099
1
                    _materialize_aggregate_pushdown_rows(_push_down_agg_type, file_result, block));
1100
1
        }
1101
13
        *pushed_down = true;
1102
13
        RETURN_IF_ERROR(close_current_reader());
1103
13
        return Status::OK();
1104
13
    }
1105
1106
3.60k
    virtual bool _supports_aggregate_pushdown(TPushAggOp::type agg_type) const {
1107
        // Only COUNT and MIN/MAX can be push down.
1108
3.60k
        if (agg_type != TPushAggOp::type::COUNT && agg_type != TPushAggOp::type::MINMAX) {
1109
3.55k
            return false;
1110
3.55k
        }
1111
        // Aggregate pushdown returns reduced synthetic rows and may close the physical reader
1112
        // before the next scheduler turn. If a runtime filter is still pending, those rows could
1113
        // escape before the filter arrives and cannot later be reconstructed from real file rows.
1114
        // This is the same irreversibility constraint as table-level metadata COUNT, and applies
1115
        // to COUNT and MIN/MAX for Parquet/ORC as well as COUNT for text readers.
1116
55
        if (!_all_runtime_filters_applied_for_split) {
1117
3
            return false;
1118
3
        }
1119
        // Scanner owns the original conjunct list and evaluates it after TableReader finalizes
1120
        // rows. Even a slotless conjunct that cannot become a TableFilter must see every source
1121
        // row before an aggregate reduces the stream to synthetic COUNT/MINMAX rows.
1122
52
        if (!_conjuncts.empty()) {
1123
5
            return false;
1124
5
        }
1125
        // Only support aggregate pushdown when there is no delete or filter, so
1126
        // the reduced rows consumed by the upper aggregate remain semantically equivalent to a
1127
        // normal scan.
1128
47
        if ((_delete_rows != nullptr && !_delete_rows->empty()) ||
1129
47
            (_deletion_vector != nullptr && !_deletion_vector->isEmpty())) {
1130
5
            return false;
1131
5
        }
1132
42
        if (!_table_filters.empty()) {
1133
0
            return false;
1134
0
        }
1135
42
        if (agg_type == TPushAggOp::type::COUNT) {
1136
            // Old FEs do not serialize push_down_count_slot_ids. During the supported BE-first
1137
            // rolling upgrade, nullopt therefore means "COUNT semantics are unknown", not
1138
            // COUNT(*). Fall back to reading rows until the FE explicitly sends either an empty
1139
            // list for COUNT(*) or one slot for COUNT(col).
1140
32
            if (!_push_down_count_columns.has_value()) {
1141
3
                return false;
1142
3
            }
1143
            // COUNT(*) needs no column metadata. COUNT(col) currently supports one direct file
1144
            // column; multiple COUNT arguments fall back to the normal scan so every upper
1145
            // aggregate receives the original rows.
1146
29
            if (_push_down_count_columns->empty()) {
1147
18
                return true;
1148
18
            }
1149
11
            if (_push_down_count_columns->size() != 1) {
1150
1
                return false;
1151
1
            }
1152
10
            const auto& mapping = _push_down_count_mapping();
1153
            // Metadata COUNT skips TableReader's normal materialization path. Only a trivial
1154
            // mapping is safe: for example, a nullable Parquet INT mapped to a NOT NULL table
1155
            // BIGINT normally needs both an INT->BIGINT cast and nullability validation. Counting
1156
            // footer values directly would bypass both operations and could hide invalid data.
1157
10
            return mapping.file_local_id.has_value() && mapping.file_type != nullptr &&
1158
10
                   mapping.table_type != nullptr && mapping.is_trivial &&
1159
10
                   mapping.virtual_column_type == TableVirtualColumnType::INVALID &&
1160
10
                   mapping.default_expr == nullptr;
1161
11
        }
1162
        // For MIN/MAX, only support direct file-to-table column mappings. The two emitted rows
1163
        // must be enough for the upper MIN/MAX aggregate without evaluating default expressions or
1164
        // virtual columns.
1165
13
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
1166
13
            if (!mapping.file_local_id.has_value() ||
1167
13
                mapping.virtual_column_type != TableVirtualColumnType::INVALID ||
1168
13
                mapping.default_expr != nullptr || mapping.file_type == nullptr ||
1169
13
                mapping.table_type == nullptr) {
1170
1
                return false;
1171
1
            }
1172
12
            if (!_can_push_down_minmax_for_mapping(mapping)) {
1173
2
                return false;
1174
2
            }
1175
12
        }
1176
7
        return true;
1177
10
    }
1178
1179
70.8k
    static ColumnPtr _detach_column(ColumnPtr column) {
1180
70.8k
        DORIS_CHECK(column.get() != nullptr);
1181
70.8k
        return IColumn::mutate(std::move(column));
1182
70.8k
    }
1183
1184
7.00k
    static ColumnPtr _take_and_detach_block_column(Block* block, int position) {
1185
7.00k
        DORIS_CHECK(block != nullptr);
1186
7.00k
        DORIS_CHECK(position >= 0 && position < static_cast<int>(block->columns()));
1187
7.00k
        auto& source = block->get_by_position(position);
1188
7.00k
        ColumnPtr column = source.column;
1189
        // The final mapping no longer needs the file block. Release its COW owner before mutate(),
1190
        // otherwise nested MAP/STRING columns are deep-copied and a multi-GB payload can OOM.
1191
7.00k
        block->replace_by_position(position, source.type->create_column());
1192
7.00k
        return _detach_column(std::move(column));
1193
7.00k
    }
1194
1195
    static Status _align_column_nullability(ColumnPtr* column, const DataTypePtr& table_type,
1196
322
                                            const NullMap* nullable_parent_null_map = nullptr) {
1197
322
        DORIS_CHECK(column != nullptr);
1198
322
        DORIS_CHECK(column->get() != nullptr);
1199
322
        DORIS_CHECK(table_type != nullptr);
1200
        // Must return non-const column
1201
322
        *column = (*column)->convert_to_full_column_if_const();
1202
322
        if (table_type->is_nullable()) {
1203
130
            const auto& nested_type =
1204
130
                    assert_cast<const DataTypeNullable&>(*table_type).get_nested_type();
1205
130
            if (!(*column)->is_nullable()) {
1206
2
                RETURN_IF_ERROR(
1207
2
                        _align_column_nullability(column, nested_type, nullable_parent_null_map));
1208
2
                *column = make_nullable(*column);
1209
2
                return Status::OK();
1210
2
            }
1211
128
            const auto& nullable_column = assert_cast<const ColumnNullable&>(**column);
1212
128
            ColumnPtr nested_column = nullable_column.get_nested_column_ptr();
1213
128
            NullMap combined_null_map;
1214
128
            const NullMap* nested_parent_null_map = &nullable_column.get_null_map_data();
1215
128
            if (nullable_parent_null_map != nullptr) {
1216
60
                const auto& own_null_map = nullable_column.get_null_map_data();
1217
60
                DORIS_CHECK(nullable_parent_null_map->size() == own_null_map.size());
1218
                // Required descendants are hidden when either this nullable container or any
1219
                // inherited nullable ancestor masks the row, so preserve the union recursively.
1220
60
                combined_null_map.resize(own_null_map.size());
1221
202
                for (size_t i = 0; i < own_null_map.size(); ++i) {
1222
142
                    combined_null_map[i] = own_null_map[i] || (*nullable_parent_null_map)[i];
1223
142
                }
1224
60
                nested_parent_null_map = &combined_null_map;
1225
60
            }
1226
128
            RETURN_IF_ERROR(
1227
128
                    _align_column_nullability(&nested_column, nested_type, nested_parent_null_map));
1228
128
            *column = ColumnNullable::create(nested_column,
1229
128
                                             nullable_column.get_null_map_column_ptr());
1230
128
            return Status::OK();
1231
128
        }
1232
192
        if ((*column)->is_nullable()) {
1233
24
            const auto& nullable_column = assert_cast<const ColumnNullable&>(**column);
1234
24
            if (nullable_column.has_null()) {
1235
11
                const auto& null_map = nullable_column.get_null_map_data();
1236
11
                if (nullable_parent_null_map == nullptr ||
1237
11
                    nullable_parent_null_map->size() != null_map.size()) {
1238
1
                    return Status::InternalError(
1239
1
                            "Default expression produced NULL for non-nullable table column");
1240
1
                }
1241
23
                for (size_t i = 0; i < null_map.size(); ++i) {
1242
                    // A required child may contain a physical NULL placeholder only when its
1243
                    // nullable parent masks that row from the logical value.
1244
17
                    if (null_map[i] && !(*nullable_parent_null_map)[i]) {
1245
4
                        return Status::InternalError(
1246
4
                                "Default expression produced NULL for non-nullable table column");
1247
4
                    }
1248
17
                }
1249
10
            }
1250
19
            ColumnPtr nested_column = nullable_column.get_nested_column_ptr();
1251
19
            RETURN_IF_ERROR(_align_column_nullability(&nested_column, table_type,
1252
19
                                                      nullable_parent_null_map));
1253
19
            *column = nested_column;
1254
19
            return Status::OK();
1255
19
        }
1256
168
        if (const auto* array_type = typeid_cast<const DataTypeArray*>(table_type.get())) {
1257
2
            const auto& array_column = assert_cast<const ColumnArray&>(**column);
1258
2
            ColumnPtr nested_column = array_column.get_data_ptr();
1259
2
            NullMap descendant_parent_null_map;
1260
            // Collection entries use offset coordinates, so inherited row masks must be projected
1261
            // only when a required descendant can consume them. This avoids scratch proportional
1262
            // to all array entries for the common all-required schema.
1263
2
            const NullMap* descendant_parent_null_map_ptr = nullptr;
1264
2
            if (_requires_collection_parent_null_map(
1265
2
                        nullable_parent_null_map, nested_column, array_type->get_nested_type(),
1266
2
                        array_column.size(), array_column.get_offsets())) {
1267
1
                descendant_parent_null_map_ptr =
1268
1
                        _project_collection_parent_null_map_for_hidden_entries(
1269
1
                                nullptr, nullable_parent_null_map, array_column.size(),
1270
1
                                array_column.get_offsets(), nested_column->size(),
1271
1
                                &descendant_parent_null_map);
1272
1
            }
1273
2
            RETURN_IF_ERROR(_align_column_nullability(&nested_column, array_type->get_nested_type(),
1274
2
                                                      descendant_parent_null_map_ptr));
1275
2
            *column = ColumnArray::create(nested_column, array_column.get_offsets_ptr());
1276
2
            return Status::OK();
1277
2
        }
1278
166
        if (const auto* map_type = typeid_cast<const DataTypeMap*>(table_type.get())) {
1279
1
            const auto& map_column = assert_cast<const ColumnMap&>(**column);
1280
1
            ColumnPtr key_column = map_column.get_keys_ptr();
1281
1
            ColumnPtr value_column = map_column.get_values_ptr();
1282
1
            NullMap descendant_parent_null_map;
1283
1
            const NullMap* descendant_parent_null_map_ptr = nullptr;
1284
1
            if (_requires_collection_parent_null_map(nullable_parent_null_map, key_column,
1285
1
                                                     map_type->get_key_type(), map_column.size(),
1286
1
                                                     map_column.get_offsets()) ||
1287
1
                _requires_collection_parent_null_map(nullable_parent_null_map, value_column,
1288
1
                                                     map_type->get_value_type(), map_column.size(),
1289
1
                                                     map_column.get_offsets())) {
1290
                // Keys and values share offsets, so one projected mask safely covers both streams.
1291
0
                descendant_parent_null_map_ptr =
1292
0
                        _project_collection_parent_null_map_for_hidden_entries(
1293
0
                                nullptr, nullable_parent_null_map, map_column.size(),
1294
0
                                map_column.get_offsets(), key_column->size(),
1295
0
                                &descendant_parent_null_map);
1296
0
            }
1297
1
            RETURN_IF_ERROR(_align_column_nullability(&key_column, map_type->get_key_type(),
1298
1
                                                      descendant_parent_null_map_ptr));
1299
1
            RETURN_IF_ERROR(_align_column_nullability(&value_column, map_type->get_value_type(),
1300
1
                                                      descendant_parent_null_map_ptr));
1301
1
            *column = ColumnMap::create(key_column, value_column, map_column.get_offsets_ptr());
1302
1
            return Status::OK();
1303
1
        }
1304
165
        if (const auto* struct_type = typeid_cast<const DataTypeStruct*>(table_type.get())) {
1305
15
            const auto& struct_column = assert_cast<const ColumnStruct&>(**column);
1306
15
            Columns columns = struct_column.get_columns_copy();
1307
15
            DORIS_CHECK(columns.size() == struct_type->get_elements().size());
1308
42
            for (size_t i = 0; i < columns.size(); ++i) {
1309
27
                RETURN_IF_ERROR(_align_column_nullability(&columns[i], struct_type->get_element(i),
1310
27
                                                          nullable_parent_null_map));
1311
27
            }
1312
15
            *column = ColumnStruct::create(columns);
1313
15
            return Status::OK();
1314
15
        }
1315
150
        return Status::OK();
1316
165
    }
1317
1318
    static Status _execute_default_expr_without_root_type_check(
1319
            const VExprContextSPtr& default_expr, const Block* block,
1320
28
            ColumnWithTypeAndName* result_data) {
1321
28
        DORIS_CHECK(default_expr != nullptr);
1322
28
        DORIS_CHECK(block != nullptr);
1323
28
        DORIS_CHECK(result_data != nullptr);
1324
28
        ColumnPtr result_column;
1325
28
        Status st;
1326
28
        RETURN_IF_CATCH_EXCEPTION({
1327
28
            st = default_expr->root()->execute_column_impl(default_expr.get(), block, nullptr,
1328
28
                                                           block->rows(), result_column);
1329
28
        });
1330
28
        RETURN_IF_ERROR(st);
1331
28
        DORIS_CHECK(result_column.get() != nullptr);
1332
28
        if (result_column->size() != block->rows()) {
1333
0
            return Status::InternalError(
1334
0
                    "Default expr {} return column size {} not equal to expected size {}",
1335
0
                    default_expr->expr_name(), result_column->size(), block->rows());
1336
0
        }
1337
28
        result_data->column = result_column;
1338
28
        result_data->type = default_expr->execute_type(block);
1339
28
        result_data->name = default_expr->expr_name();
1340
28
        return Status::OK();
1341
28
    }
1342
1343
    Status _cast_column_to_type(ColumnPtr* column, const DataTypePtr& file_type,
1344
                                const DataTypePtr& table_type, const std::string& column_name,
1345
33
                                bool truncate_datetimev2_precision = false) const {
1346
33
        DORIS_CHECK(column != nullptr);
1347
33
        DORIS_CHECK(column->get() != nullptr);
1348
33
        DORIS_CHECK(file_type != nullptr);
1349
33
        DORIS_CHECK(table_type != nullptr);
1350
33
        if (file_type->equals(*table_type) ||
1351
33
            remove_nullable(file_type)->equals(*remove_nullable(table_type))) {
1352
15
            return Status::OK();
1353
15
        }
1354
1355
18
        DataTypePtr input_type = file_type;
1356
        // Cast wrappers unwrap nullable inputs according to the declared input type, so keep the
1357
        // root nullability of the declared input aligned with the actual column shape. When the
1358
        // runtime column is nullable, also keep the cast target nullable; the caller applies the
1359
        // table's final nullability after value conversion. Casting a nullable runtime column
1360
        // directly to a non-nullable target would pass ColumnNullable to CastToImpl.
1361
18
        if ((*column)->is_nullable() && !input_type->is_nullable()) {
1362
0
            input_type = make_nullable(input_type);
1363
18
        } else if (!(*column)->is_nullable() && input_type->is_nullable()) {
1364
1
            input_type = remove_nullable(input_type);
1365
1
        }
1366
18
        DataTypePtr cast_type = table_type;
1367
18
        if ((*column)->is_nullable() && !cast_type->is_nullable()) {
1368
8
            cast_type = make_nullable(cast_type);
1369
8
        }
1370
18
        Block cast_block;
1371
18
        cast_block.insert({*column, input_type, column_name});
1372
18
        auto slot_ref = VSlotRef::create_shared(0, 0, -1, input_type, column_name);
1373
18
        auto cast_expr = EvolutionCast::create_shared(cast_type, truncate_datetimev2_precision);
1374
18
        cast_expr->add_child(std::move(slot_ref));
1375
18
        auto cast_ctx = VExprContext::create_shared(std::move(cast_expr));
1376
18
        RowDescriptor row_desc;
1377
18
        RETURN_IF_ERROR(cast_ctx->prepare(_runtime_state, row_desc));
1378
18
        RETURN_IF_ERROR(cast_ctx->open(_runtime_state));
1379
18
        ColumnPtr cast_column;
1380
18
        RETURN_IF_ERROR(cast_ctx->execute(&cast_block, cast_column));
1381
16
        *column = std::move(cast_column);
1382
16
        return Status::OK();
1383
18
    }
1384
1385
    Status _try_materialize_scalar_cast_with_runtime_nullability(const ColumnMapping& mapping,
1386
                                                                 const Block* current_block,
1387
                                                                 ColumnPtr* column,
1388
70.8k
                                                                 bool* handled) const {
1389
70.8k
        DORIS_CHECK(column != nullptr);
1390
70.8k
        DORIS_CHECK(handled != nullptr);
1391
70.8k
        *handled = false;
1392
70.8k
        if (mapping.projection == nullptr || !mapping.file_local_id.has_value() ||
1393
70.8k
            !mapping.child_mappings.empty()) {
1394
1.53k
            return Status::OK();
1395
1.53k
        }
1396
1397
69.3k
        const auto& root = mapping.projection->root();
1398
69.3k
        if (root == nullptr || root->node_type() != TExprNodeType::CAST_EXPR) {
1399
68.5k
            return Status::OK();
1400
68.5k
        }
1401
726
        DORIS_CHECK(root->get_num_children() == 1);
1402
726
        const auto* slot = dynamic_cast<const VSlotRef*>(root->get_child(0).get());
1403
726
        DORIS_CHECK(slot != nullptr);
1404
726
        DORIS_CHECK(current_block != nullptr);
1405
726
        DORIS_CHECK(slot->column_id() >= 0);
1406
726
        DORIS_CHECK(cast_set<size_t>(slot->column_id()) < current_block->columns());
1407
726
        const auto& source = current_block->get_by_position(slot->column_id());
1408
726
        DORIS_CHECK(source.column.get() != nullptr);
1409
726
        DORIS_CHECK(slot->data_type() != nullptr);
1410
726
        DORIS_CHECK(mapping.table_type != nullptr);
1411
726
        const bool runtime_input_mismatch =
1412
726
                source.column->is_nullable() != slot->data_type()->is_nullable();
1413
726
        const bool nullable_input_to_required_table =
1414
726
                source.column->is_nullable() && !mapping.table_type->is_nullable();
1415
726
        if (!runtime_input_mismatch && !nullable_input_to_required_table) {
1416
714
            return Status::OK();
1417
714
        }
1418
1419
        // File readers can return a nullable runtime column even when the physical schema marks the
1420
        // leaf required. A pre-built Cast binds to the declared file type and can therefore pass a
1421
        // ColumnNullable to a non-nullable CastToImpl. Rebuild only when that runtime shape differs
1422
        // from the declared input, or when a declared nullable file field maps to a required table
1423
        // field. Keep the cast target nullable while converting values, then let
1424
        // _align_column_nullability() reject an actual NULL before removing the wrapper.
1425
12
        ColumnPtr result_column = source.column;
1426
12
        RETURN_IF_ERROR(_cast_column_to_type(&result_column, slot->data_type(), mapping.table_type,
1427
12
                                             mapping.file_column_name,
1428
12
                                             mapping.truncate_datetimev2_precision));
1429
12
        RETURN_IF_ERROR(_align_column_nullability(&result_column, mapping.table_type));
1430
11
        *column = _detach_column(std::move(result_column));
1431
11
        *handled = true;
1432
11
        return Status::OK();
1433
12
    }
1434
1435
    Status _materialize_present_child_mapping_column(
1436
            const ColumnMapping& mapping, const ColumnPtr& file_column, const size_t rows,
1437
66
            ColumnPtr* column, const NullMap* nullable_parent_null_map = nullptr) {
1438
66
        DORIS_CHECK(column != nullptr);
1439
66
        DORIS_CHECK(mapping.file_type != nullptr);
1440
66
        DORIS_CHECK(mapping.table_type != nullptr);
1441
66
        *column = file_column;
1442
66
        if (!mapping.is_trivial) {
1443
35
            if (!mapping.child_mappings.empty()) {
1444
16
                RETURN_IF_ERROR(_materialize_complex_mapping_column(mapping, *column, rows, column,
1445
16
                                                                    nullable_parent_null_map));
1446
19
            } else {
1447
19
                RETURN_IF_ERROR(_cast_column_to_type(column, mapping.file_type, mapping.table_type,
1448
19
                                                     mapping.file_column_name,
1449
19
                                                     mapping.truncate_datetimev2_precision));
1450
19
            }
1451
35
        }
1452
61
        RETURN_IF_ERROR(
1453
61
                _align_column_nullability(column, mapping.table_type, nullable_parent_null_map));
1454
57
        return Status::OK();
1455
61
    }
1456
1457
    Status _materialize_default_or_missing_column(
1458
            const ColumnMapping& mapping, const Block* current_block, const size_t rows,
1459
66
            ColumnPtr* column, const NullMap* nullable_parent_null_map = nullptr) {
1460
66
        DORIS_CHECK(mapping.table_type != nullptr);
1461
66
        DORIS_CHECK(column != nullptr);
1462
66
        if (mapping.default_expr != nullptr) {
1463
28
            Block synthetic_block;
1464
28
            const Block* eval_block = current_block;
1465
28
            if (eval_block == nullptr || eval_block->rows() != rows) {
1466
                // Nested ARRAY/MAP children use element/entry cardinality rather than the root
1467
                // block's row count. Iceberg initial defaults are typed literals, so a synthetic
1468
                // block with the desired row count is sufficient and avoids a top-level
1469
                // ConstantMap dependency for nested mappings.
1470
14
                synthetic_block.insert(
1471
14
                        {mapping.table_type->create_column_const_with_default_value(rows),
1472
14
                         mapping.table_type, "__table_reader_nested_default_rows"});
1473
14
                eval_block = &synthetic_block;
1474
14
            }
1475
28
            ColumnWithTypeAndName result;
1476
28
            RETURN_IF_ERROR(_execute_default_expr_without_root_type_check(mapping.default_expr,
1477
28
                                                                          eval_block, &result));
1478
28
            ColumnPtr result_column = result.column;
1479
28
            RETURN_IF_ERROR(_align_column_nullability(&result_column, mapping.table_type,
1480
28
                                                      nullable_parent_null_map));
1481
28
            *column = _detach_column(std::move(result_column));
1482
28
            return Status::OK();
1483
28
        }
1484
38
        ColumnPtr result_column = mapping.table_type->create_column_const_with_default_value(rows);
1485
38
        RETURN_IF_ERROR(_align_column_nullability(&result_column, mapping.table_type,
1486
38
                                                  nullable_parent_null_map));
1487
38
        *column = _detach_column(std::move(result_column));
1488
38
        return Status::OK();
1489
38
    }
1490
1491
    Status _materialize_mapping_column(const ColumnMapping& mapping, Block* current_block,
1492
                                       const size_t rows, ColumnPtr* column,
1493
70.8k
                                       bool take_projection_result = false) {
1494
70.8k
        if (!mapping.is_trivial && mapping.file_local_id.has_value() &&
1495
70.8k
            !mapping.child_mappings.empty()) {
1496
28
            DCHECK(mapping.projection != nullptr);
1497
28
            int res_id;
1498
28
            auto st = mapping.projection->execute(current_block, &res_id);
1499
28
            if (!st.ok()) {
1500
0
                return Status::InternalError(
1501
0
                        "Failed to execute complex mapping projection for table column '{}' "
1502
0
                        "(global_index={}, file_local_id={}, rows={}): {}, mapping={}",
1503
0
                        mapping.table_column_name, mapping.global_index.value(),
1504
0
                        *mapping.file_local_id, rows, st.to_string(), mapping.debug_string());
1505
0
            }
1506
28
            ColumnPtr result_column = take_projection_result
1507
28
                                              ? _take_and_detach_block_column(current_block, res_id)
1508
28
                                              : current_block->get_by_position(res_id).column;
1509
28
            RETURN_IF_ERROR(
1510
28
                    _materialize_complex_mapping_column(mapping, result_column, rows, column));
1511
23
            return Status::OK();
1512
28
        }
1513
70.8k
        bool runtime_nullability_cast_handled = false;
1514
70.8k
        RETURN_IF_ERROR(_try_materialize_scalar_cast_with_runtime_nullability(
1515
70.8k
                mapping, current_block, column, &runtime_nullability_cast_handled));
1516
70.8k
        if (runtime_nullability_cast_handled) {
1517
11
            return Status::OK();
1518
11
        }
1519
70.8k
        if (mapping.projection != nullptr) {
1520
70.7k
            int res_id;
1521
70.7k
            auto st = mapping.projection->execute(current_block, &res_id);
1522
70.7k
            if (!st.ok()) {
1523
2
                std::string file_local_id = "null";
1524
2
                if (mapping.file_local_id.has_value()) {
1525
2
                    file_local_id = std::to_string(*mapping.file_local_id);
1526
2
                }
1527
2
                return Status::InternalError(
1528
2
                        "Failed to execute mapping projection for table column '{}' "
1529
2
                        "(global_index={}, file_local_id={}, rows={}): {}, mapping={}",
1530
2
                        mapping.table_column_name, mapping.global_index.value(), file_local_id,
1531
2
                        rows, st.to_string(), mapping.debug_string());
1532
2
            }
1533
70.7k
            if (take_projection_result) {
1534
6.97k
                *column = _take_and_detach_block_column(current_block, res_id);
1535
63.7k
            } else {
1536
63.7k
                ColumnPtr result_column = current_block->get_by_position(res_id).column;
1537
63.7k
                *column = _detach_column(std::move(result_column));
1538
63.7k
            }
1539
70.7k
            return Status::OK();
1540
70.7k
        }
1541
49
        return _materialize_default_or_missing_column(mapping, current_block, rows, column);
1542
70.8k
    }
1543
1544
    Status _materialize_complex_mapping_column(const ColumnMapping& mapping,
1545
                                               const ColumnPtr& file_column, const size_t rows,
1546
                                               ColumnPtr* column,
1547
44
                                               const NullMap* nullable_parent_null_map = nullptr) {
1548
44
        DORIS_CHECK(mapping.table_type != nullptr);
1549
44
        DORIS_CHECK(file_column.get() != nullptr);
1550
44
        const auto table_type = remove_nullable(mapping.table_type);
1551
44
        switch (table_type->get_primitive_type()) {
1552
31
        case TYPE_STRUCT:
1553
31
            RETURN_IF_ERROR(_materialize_struct_mapping_column(mapping, file_column, rows, column,
1554
31
                                                               nullable_parent_null_map));
1555
26
            break;
1556
26
        case TYPE_ARRAY:
1557
8
            RETURN_IF_ERROR(_materialize_array_mapping_column(mapping, file_column, rows, column,
1558
8
                                                              nullable_parent_null_map));
1559
5
            break;
1560
5
        case TYPE_MAP:
1561
5
            RETURN_IF_ERROR(_materialize_map_mapping_column(mapping, file_column, rows, column,
1562
5
                                                            nullable_parent_null_map));
1563
5
            break;
1564
5
        default:
1565
0
            *column = _detach_column(file_column);
1566
0
            break;
1567
44
        }
1568
36
        return Status::OK();
1569
44
    }
1570
1571
    static std::vector<const ColumnMapping*> _present_child_mappings_in_file_order(
1572
36
            const std::vector<ColumnMapping>& child_mappings) {
1573
36
        std::vector<const ColumnMapping*> result;
1574
36
        result.reserve(child_mappings.size());
1575
55
        for (const auto& child_mapping : child_mappings) {
1576
55
            if (child_mapping.file_local_id.has_value()) {
1577
41
                result.push_back(&child_mapping);
1578
41
            }
1579
55
        }
1580
36
        std::ranges::sort(result, [](const ColumnMapping* lhs, const ColumnMapping* rhs) {
1581
14
            DORIS_CHECK(lhs->file_local_id.has_value());
1582
14
            DORIS_CHECK(rhs->file_local_id.has_value());
1583
14
            return *lhs->file_local_id < *rhs->file_local_id;
1584
14
        });
1585
36
        return result;
1586
36
    }
1587
1588
    static size_t _file_child_ordinal_for_mapping(
1589
            const ColumnMapping& mapping, const ColumnMapping& child_mapping,
1590
40
            const std::vector<const ColumnMapping*>& file_ordered_children) {
1591
40
        DORIS_CHECK(child_mapping.file_local_id.has_value());
1592
40
        if (!mapping.projected_file_children.empty()) {
1593
29
            const auto child_it = std::ranges::find_if(
1594
38
                    mapping.projected_file_children, [&](const ColumnDefinition& file_child) {
1595
38
                        return file_child.file_local_id() == *child_mapping.file_local_id;
1596
38
                    });
1597
29
            DORIS_CHECK(child_it != mapping.projected_file_children.end());
1598
29
            return static_cast<size_t>(
1599
29
                    std::distance(mapping.projected_file_children.begin(), child_it));
1600
29
        }
1601
11
        const auto child_it = std::ranges::find(file_ordered_children, &child_mapping);
1602
11
        DORIS_CHECK(child_it != file_ordered_children.end());
1603
11
        return static_cast<size_t>(std::distance(file_ordered_children.begin(), child_it));
1604
40
    }
1605
1606
    static std::vector<const ColumnMapping*> _child_mappings_in_table_type_order(
1607
36
            const ColumnMapping& mapping, const DataTypeStruct& table_type) {
1608
36
        std::vector<const ColumnMapping*> result;
1609
36
        result.reserve(mapping.child_mappings.size());
1610
91
        for (size_t child_idx = 0; child_idx < table_type.get_elements().size(); ++child_idx) {
1611
55
            const auto& child_name = table_type.get_element_name(child_idx);
1612
55
            const auto child_it = std::ranges::find_if(
1613
77
                    mapping.child_mappings, [&](const ColumnMapping& child_mapping) {
1614
77
                        return child_mapping.table_column_name == child_name;
1615
77
                    });
1616
55
            DORIS_CHECK(child_it != mapping.child_mappings.end())
1617
0
                    << mapping.debug_string() << ", table_child_name=" << child_name;
1618
55
            result.push_back(&*child_it);
1619
55
        }
1620
36
        return result;
1621
36
    }
1622
1623
    static const IColumn* _nested_column_if_nullable(const ColumnPtr& column,
1624
57
                                                     const NullMap** null_map) {
1625
57
        DORIS_CHECK(column.get() != nullptr);
1626
57
        if (const auto* nullable_column = check_and_get_column<ColumnNullable>(*column)) {
1627
53
            if (null_map != nullptr) {
1628
53
                *null_map = &nullable_column->get_null_map_data();
1629
53
            }
1630
53
            return &nullable_column->get_nested_column();
1631
53
        }
1632
4
        return column.get();
1633
57
    }
1634
1635
    static bool _requires_parent_null_map_for_alignment(const ColumnPtr& column,
1636
0
                                                        const DataTypePtr& table_type) {
1637
0
        DORIS_CHECK(column.get() != nullptr);
1638
0
        DORIS_CHECK(table_type != nullptr);
1639
0
        if (table_type->is_nullable()) {
1640
0
            const auto& nested_type =
1641
0
                    assert_cast<const DataTypeNullable&>(*table_type).get_nested_type();
1642
0
            if (const auto* nullable_column = check_and_get_column<ColumnNullable>(*column)) {
1643
0
                return _requires_parent_null_map_for_alignment(
1644
0
                        nullable_column->get_nested_column_ptr(), nested_type);
1645
0
            }
1646
0
            return _requires_parent_null_map_for_alignment(column, nested_type);
1647
0
        }
1648
0
        if (const auto* nullable_column = check_and_get_column<ColumnNullable>(*column)) {
1649
0
            if (nullable_column->has_null()) {
1650
0
                return true;
1651
0
            }
1652
0
            return _requires_parent_null_map_for_alignment(nullable_column->get_nested_column_ptr(),
1653
0
                                                           table_type);
1654
0
        }
1655
0
        if (const auto* array_type = typeid_cast<const DataTypeArray*>(table_type.get())) {
1656
0
            const auto& array_column = assert_cast<const ColumnArray&>(*column);
1657
0
            return _requires_parent_null_map_for_alignment(array_column.get_data_ptr(),
1658
0
                                                           array_type->get_nested_type());
1659
0
        }
1660
0
        if (const auto* map_type = typeid_cast<const DataTypeMap*>(table_type.get())) {
1661
0
            const auto& map_column = assert_cast<const ColumnMap&>(*column);
1662
0
            return _requires_parent_null_map_for_alignment(map_column.get_keys_ptr(),
1663
0
                                                           map_type->get_key_type()) ||
1664
0
                   _requires_parent_null_map_for_alignment(map_column.get_values_ptr(),
1665
0
                                                           map_type->get_value_type());
1666
0
        }
1667
0
        if (const auto* struct_type = typeid_cast<const DataTypeStruct*>(table_type.get())) {
1668
0
            const auto& struct_column = assert_cast<const ColumnStruct&>(*column);
1669
0
            DORIS_CHECK(struct_column.tuple_size() == struct_type->get_elements().size());
1670
0
            for (size_t i = 0; i < struct_column.tuple_size(); ++i) {
1671
0
                if (_requires_parent_null_map_for_alignment(struct_column.get_column_ptr(i),
1672
0
                                                            struct_type->get_element(i))) {
1673
0
                    return true;
1674
0
                }
1675
0
            }
1676
0
        }
1677
0
        return false;
1678
0
    }
1679
1680
    static bool _requires_parent_null_map_for_alignment_at(const ColumnPtr& column,
1681
                                                           const DataTypePtr& table_type,
1682
17
                                                           const size_t row) {
1683
17
        DORIS_CHECK(column.get() != nullptr);
1684
17
        DORIS_CHECK(table_type != nullptr);
1685
17
        DORIS_CHECK(row < column->size());
1686
17
        if (table_type->is_nullable()) {
1687
5
            const auto& nested_type =
1688
5
                    assert_cast<const DataTypeNullable&>(*table_type).get_nested_type();
1689
5
            if (const auto* nullable_column = check_and_get_column<ColumnNullable>(*column)) {
1690
                // A nearer nullable wrapper already protects its descendants at this entry, so an
1691
                // inherited collection mask cannot be needed there.
1692
5
                if (nullable_column->is_null_at(row)) {
1693
0
                    return false;
1694
0
                }
1695
5
                return _requires_parent_null_map_for_alignment_at(
1696
5
                        nullable_column->get_nested_column_ptr(), nested_type, row);
1697
5
            }
1698
0
            return _requires_parent_null_map_for_alignment_at(column, nested_type, row);
1699
5
        }
1700
12
        if (const auto* nullable_column = check_and_get_column<ColumnNullable>(*column)) {
1701
4
            if (nullable_column->is_null_at(row)) {
1702
2
                return true;
1703
2
            }
1704
2
            return _requires_parent_null_map_for_alignment_at(
1705
2
                    nullable_column->get_nested_column_ptr(), table_type, row);
1706
4
        }
1707
8
        if (const auto* array_type = typeid_cast<const DataTypeArray*>(table_type.get())) {
1708
0
            const auto& array_column = assert_cast<const ColumnArray&>(*column);
1709
0
            const auto& offsets = array_column.get_offsets();
1710
0
            const size_t begin = row == 0 ? 0 : offsets[row - 1];
1711
0
            const size_t end = offsets[row];
1712
0
            for (size_t child_row = begin; child_row < end; ++child_row) {
1713
0
                if (_requires_parent_null_map_for_alignment_at(array_column.get_data_ptr(),
1714
0
                                                               array_type->get_nested_type(),
1715
0
                                                               child_row)) {
1716
0
                    return true;
1717
0
                }
1718
0
            }
1719
0
            return false;
1720
0
        }
1721
8
        if (const auto* map_type = typeid_cast<const DataTypeMap*>(table_type.get())) {
1722
0
            const auto& map_column = assert_cast<const ColumnMap&>(*column);
1723
0
            const auto& offsets = map_column.get_offsets();
1724
0
            const size_t begin = row == 0 ? 0 : offsets[row - 1];
1725
0
            const size_t end = offsets[row];
1726
0
            for (size_t child_row = begin; child_row < end; ++child_row) {
1727
0
                if (_requires_parent_null_map_for_alignment_at(
1728
0
                            map_column.get_keys_ptr(), map_type->get_key_type(), child_row) ||
1729
0
                    _requires_parent_null_map_for_alignment_at(
1730
0
                            map_column.get_values_ptr(), map_type->get_value_type(), child_row)) {
1731
0
                    return true;
1732
0
                }
1733
0
            }
1734
0
            return false;
1735
0
        }
1736
8
        if (const auto* struct_type = typeid_cast<const DataTypeStruct*>(table_type.get())) {
1737
4
            const auto& struct_column = assert_cast<const ColumnStruct&>(*column);
1738
4
            DORIS_CHECK(struct_column.tuple_size() == struct_type->get_elements().size());
1739
6
            for (size_t i = 0; i < struct_column.tuple_size(); ++i) {
1740
4
                if (_requires_parent_null_map_for_alignment_at(struct_column.get_column_ptr(i),
1741
4
                                                               struct_type->get_element(i), row)) {
1742
2
                    return true;
1743
2
                }
1744
4
            }
1745
4
        }
1746
6
        return false;
1747
8
    }
1748
1749
    static bool _requires_collection_parent_null_map(const NullMap* parent_null_map,
1750
                                                     const ColumnPtr& column,
1751
                                                     const DataTypePtr& table_type) {
1752
        // Descendant null maps can be entry-sized. Scan them only when an inherited mask can
1753
        // actually hide a row; absent/all-clear masks cannot authorize any physical child NULL.
1754
        if (parent_null_map == nullptr ||
1755
            std::ranges::none_of(*parent_null_map, [](const auto value) { return value != 0; })) {
1756
            return false;
1757
        }
1758
        return _requires_parent_null_map_for_alignment(column, table_type);
1759
    }
1760
1761
    template <typename Offsets>
1762
    static bool _parent_null_map_hides_collection_entries(const NullMap* container_null_map,
1763
                                                          const NullMap* ancestor_null_map,
1764
                                                          const size_t rows,
1765
23
                                                          const Offsets& offsets) {
1766
23
        if (container_null_map == nullptr && ancestor_null_map == nullptr) {
1767
2
            return false;
1768
2
        }
1769
21
        DORIS_CHECK(container_null_map == nullptr || container_null_map->size() == rows);
1770
21
        DORIS_CHECK(ancestor_null_map == nullptr || ancestor_null_map->size() == rows);
1771
21
        DORIS_CHECK(offsets.size() == rows);
1772
21
        size_t begin = 0;
1773
72
        for (size_t row = 0; row < rows; ++row) {
1774
56
            const size_t end = offsets[row];
1775
56
            const bool hidden = (container_null_map != nullptr && (*container_null_map)[row]) ||
1776
56
                                (ancestor_null_map != nullptr && (*ancestor_null_map)[row]);
1777
            // A hidden collection row protects descendants only when its offset span is nonempty.
1778
56
            if (hidden && end > begin) {
1779
5
                return true;
1780
5
            }
1781
51
            begin = end;
1782
51
        }
1783
16
        return false;
1784
21
    }
1785
1786
    template <typename Offsets>
1787
    static bool _requires_collection_parent_null_map(const NullMap* parent_null_map,
1788
                                                     const ColumnPtr& column,
1789
                                                     const DataTypePtr& table_type,
1790
9
                                                     const size_t rows, const Offsets& offsets) {
1791
9
        if (parent_null_map == nullptr) {
1792
0
            return false;
1793
0
        }
1794
9
        DORIS_CHECK(parent_null_map->size() == rows);
1795
9
        DORIS_CHECK(offsets.size() == rows);
1796
9
        DORIS_CHECK(offsets.empty() || offsets.back() == column->size());
1797
9
        size_t begin = 0;
1798
22
        for (size_t row = 0; row < rows; ++row) {
1799
15
            const size_t end = offsets[row];
1800
15
            if ((*parent_null_map)[row]) {
1801
                // Only ancestor-hidden entries can consume this projection. Restricting the probe
1802
                // to their spans avoids scanning visible payload covered by nearer nullable masks.
1803
12
                for (size_t child_row = begin; child_row < end; ++child_row) {
1804
6
                    if (_requires_parent_null_map_for_alignment_at(column, table_type, child_row)) {
1805
2
                        return true;
1806
2
                    }
1807
6
                }
1808
8
            }
1809
13
            begin = end;
1810
13
        }
1811
7
        return false;
1812
9
    }
1813
1814
    template <typename Offsets>
1815
    static const NullMap* _project_collection_parent_null_map_for_hidden_entries(
1816
            const NullMap* container_null_map, const NullMap* ancestor_null_map, const size_t rows,
1817
23
            const Offsets& offsets, const size_t child_rows, NullMap* const projected_null_map) {
1818
23
        if (!_parent_null_map_hides_collection_entries(container_null_map, ancestor_null_map, rows,
1819
23
                                                       offsets)) {
1820
            // Nullable collection wrappers expose a null-map even when every row is present; avoid
1821
            // allocating entry-coordinate scratch unless a hidden row owns physical entries.
1822
18
            return nullptr;
1823
18
        }
1824
5
        projected_null_map->resize(child_rows);
1825
5
        std::fill(projected_null_map->begin(), projected_null_map->end(), 0);
1826
5
        size_t begin = 0;
1827
15
        for (size_t row = 0; row < rows; ++row) {
1828
10
            const size_t end = offsets[row];
1829
10
            const bool hidden = (container_null_map != nullptr && (*container_null_map)[row]) ||
1830
10
                                (ancestor_null_map != nullptr && (*ancestor_null_map)[row]);
1831
10
            if (hidden) {
1832
                // Collection masks use row coordinates; descendants need the same invariant
1833
                // projected through offsets so hidden physical payload cannot fail validation.
1834
5
                std::fill(projected_null_map->begin() + begin, projected_null_map->begin() + end,
1835
5
                          1);
1836
5
            }
1837
10
            begin = end;
1838
10
        }
1839
5
        DORIS_CHECK(begin == child_rows);
1840
5
        return projected_null_map;
1841
23
    }
1842
1843
    Status _materialize_struct_mapping_column(const ColumnMapping& mapping,
1844
                                              const ColumnPtr& file_column, const size_t rows,
1845
                                              ColumnPtr* column,
1846
37
                                              const NullMap* nullable_parent_null_map = nullptr) {
1847
37
        DORIS_CHECK(mapping.table_type != nullptr);
1848
37
        const auto* table_type =
1849
37
                assert_cast<const DataTypeStruct*>(remove_nullable(mapping.table_type).get());
1850
37
        const auto full_file_column = file_column->convert_to_full_column_if_const();
1851
37
        const NullMap* parent_null_map = nullptr;
1852
37
        const auto* nested_file_column =
1853
37
                _nested_column_if_nullable(full_file_column, &parent_null_map);
1854
37
        const auto* file_struct = assert_cast<const ColumnStruct*>(nested_file_column);
1855
37
        DORIS_CHECK(table_type->get_elements().size() == mapping.child_mappings.size());
1856
1857
37
        NullMap combined_parent_null_map;
1858
37
        const NullMap* descendant_parent_null_map = nullable_parent_null_map;
1859
37
        if (parent_null_map != nullptr) {
1860
35
            DORIS_CHECK(parent_null_map->size() == rows);
1861
35
            if (nullable_parent_null_map != nullptr) {
1862
3
                DORIS_CHECK(nullable_parent_null_map->size() == rows);
1863
3
            }
1864
35
            if (!mapping.table_type->is_nullable()) {
1865
7
                for (size_t i = 0; i < rows; ++i) {
1866
                    // A required nested container may drop its own NULL only when an ancestor
1867
                    // already hides that row; otherwise physical defaults become visible values.
1868
5
                    if ((*parent_null_map)[i] &&
1869
5
                        (nullable_parent_null_map == nullptr || !(*nullable_parent_null_map)[i])) {
1870
1
                        return Status::InternalError(
1871
1
                                "Source struct contains NULL for non-nullable table column");
1872
1
                    }
1873
5
                }
1874
3
            }
1875
34
            combined_parent_null_map.resize(rows);
1876
107
            for (size_t i = 0; i < rows; ++i) {
1877
73
                combined_parent_null_map[i] =
1878
73
                        (*parent_null_map)[i] ||
1879
73
                        (nullable_parent_null_map != nullptr && (*nullable_parent_null_map)[i]);
1880
73
            }
1881
34
            descendant_parent_null_map = &combined_parent_null_map;
1882
34
        }
1883
1884
36
        Columns child_columns;
1885
36
        child_columns.reserve(mapping.child_mappings.size());
1886
36
        const auto file_ordered_children =
1887
36
                _present_child_mappings_in_file_order(mapping.child_mappings);
1888
36
        const auto table_ordered_children =
1889
36
                _child_mappings_in_table_type_order(mapping, *table_type);
1890
54
        for (const auto* child_mapping : table_ordered_children) {
1891
54
            DORIS_CHECK(child_mapping != nullptr);
1892
54
            if (!child_mapping->file_local_id.has_value()) {
1893
14
                ColumnPtr child_column;
1894
14
                RETURN_IF_ERROR(_materialize_default_or_missing_column(
1895
14
                        *child_mapping, nullptr, rows, &child_column, descendant_parent_null_map));
1896
14
                child_column = child_column->convert_to_full_column_if_const();
1897
14
                child_columns.push_back(std::move(child_column));
1898
14
                continue;
1899
14
            }
1900
40
            const auto file_child_idx =
1901
40
                    _file_child_ordinal_for_mapping(mapping, *child_mapping, file_ordered_children);
1902
40
            DORIS_CHECK(file_child_idx < file_struct->get_columns().size());
1903
40
            ColumnPtr child_column = file_struct->get_column_ptr(file_child_idx);
1904
40
            RETURN_IF_ERROR(_materialize_present_child_mapping_column(
1905
40
                    *child_mapping, child_column, rows, &child_column, descendant_parent_null_map));
1906
34
            child_columns.push_back(std::move(child_column));
1907
34
        }
1908
30
        MutableColumns mutable_child_columns;
1909
30
        mutable_child_columns.reserve(child_columns.size());
1910
46
        for (auto& child_column : child_columns) {
1911
46
            mutable_child_columns.push_back(IColumn::mutate(std::move(child_column)));
1912
46
        }
1913
30
        auto result = ColumnStruct::create(std::move(mutable_child_columns));
1914
30
        if (mapping.table_type->is_nullable()) {
1915
26
            auto null_map = ColumnUInt8::create();
1916
26
            auto& null_map_data = null_map->get_data();
1917
26
            null_map_data.resize(rows);
1918
26
            if (parent_null_map != nullptr) {
1919
26
                DORIS_CHECK(parent_null_map->size() == rows);
1920
26
                null_map_data.assign(parent_null_map->begin(), parent_null_map->end());
1921
26
            } else {
1922
0
                std::fill(null_map_data.begin(), null_map_data.end(), 0);
1923
0
            }
1924
26
            *column = ColumnNullable::create(std::move(result), std::move(null_map));
1925
26
        } else {
1926
4
            *column = std::move(result);
1927
4
        }
1928
30
        return Status::OK();
1929
36
    }
1930
1931
    Status _materialize_array_mapping_column(const ColumnMapping& mapping,
1932
                                             const ColumnPtr& file_column, const size_t rows,
1933
                                             ColumnPtr* column,
1934
12
                                             const NullMap* nullable_parent_null_map = nullptr) {
1935
12
        DORIS_CHECK(mapping.child_mappings.size() == 1);
1936
12
        const auto full_file_column = file_column->convert_to_full_column_if_const();
1937
12
        const NullMap* parent_null_map = nullptr;
1938
12
        const auto* nested_file_column =
1939
12
                _nested_column_if_nullable(full_file_column, &parent_null_map);
1940
12
        if (parent_null_map != nullptr && !mapping.table_type->is_nullable()) {
1941
2
            DORIS_CHECK(parent_null_map->size() == rows);
1942
2
            if (nullable_parent_null_map != nullptr) {
1943
1
                DORIS_CHECK(nullable_parent_null_map->size() == rows);
1944
1
            }
1945
4
            for (size_t i = 0; i < rows; ++i) {
1946
                // ARRAY row masks cannot be forwarded to elements because they use different
1947
                // coordinates, so validate the container before dropping its nullable wrapper.
1948
3
                if ((*parent_null_map)[i] &&
1949
3
                    (nullable_parent_null_map == nullptr || !(*nullable_parent_null_map)[i])) {
1950
1
                    return Status::InternalError(
1951
1
                            "Source array contains NULL for non-nullable table column");
1952
1
                }
1953
3
            }
1954
2
        }
1955
11
        const auto* file_array = assert_cast<const ColumnArray*>(nested_file_column);
1956
11
        ColumnPtr nested_column = file_array->get_data_ptr();
1957
11
        auto element_mapping = mapping.child_mappings[0];
1958
        // Keep the descriptor type for schema matching. ARRAY's nullable element wrapper is a
1959
        // storage invariant, so add it only at the materialization boundary.
1960
11
        element_mapping.table_type = make_nullable(element_mapping.table_type);
1961
11
        NullMap descendant_parent_null_map;
1962
11
        const NullMap* descendant_parent_null_map_ptr =
1963
11
                _project_collection_parent_null_map_for_hidden_entries(
1964
11
                        parent_null_map, nullable_parent_null_map, rows, file_array->get_offsets(),
1965
11
                        nested_column->size(), &descendant_parent_null_map);
1966
11
        RETURN_IF_ERROR(_materialize_present_child_mapping_column(
1967
11
                element_mapping, nested_column, nested_column->size(), &nested_column,
1968
11
                descendant_parent_null_map_ptr));
1969
8
        auto offsets_column = file_array->get_offsets_ptr()->convert_to_full_column_if_const();
1970
8
        auto result = ColumnArray::create(IColumn::mutate(std::move(nested_column)),
1971
8
                                          IColumn::mutate(std::move(offsets_column)));
1972
8
        if (mapping.table_type->is_nullable()) {
1973
7
            auto null_map = ColumnUInt8::create();
1974
7
            auto& null_map_data = null_map->get_data();
1975
7
            null_map_data.resize(rows);
1976
7
            if (parent_null_map != nullptr) {
1977
7
                DORIS_CHECK(parent_null_map->size() == rows);
1978
7
                null_map_data.assign(parent_null_map->begin(), parent_null_map->end());
1979
7
            } else {
1980
0
                std::fill(null_map_data.begin(), null_map_data.end(), 0);
1981
0
            }
1982
7
            *column = ColumnNullable::create(std::move(result), std::move(null_map));
1983
7
        } else {
1984
1
            *column = std::move(result);
1985
1
        }
1986
8
        return Status::OK();
1987
11
    }
1988
1989
    Status _materialize_map_mapping_column(const ColumnMapping& mapping,
1990
                                           const ColumnPtr& file_column, const size_t rows,
1991
                                           ColumnPtr* column,
1992
8
                                           const NullMap* nullable_parent_null_map = nullptr) {
1993
8
        const auto full_file_column = file_column->convert_to_full_column_if_const();
1994
8
        const NullMap* parent_null_map = nullptr;
1995
8
        const auto* nested_file_column =
1996
8
                _nested_column_if_nullable(full_file_column, &parent_null_map);
1997
8
        if (parent_null_map != nullptr && !mapping.table_type->is_nullable()) {
1998
0
            DORIS_CHECK(parent_null_map->size() == rows);
1999
0
            if (nullable_parent_null_map != nullptr) {
2000
0
                DORIS_CHECK(nullable_parent_null_map->size() == rows);
2001
0
            }
2002
0
            for (size_t i = 0; i < rows; ++i) {
2003
                // MAP row masks cannot be forwarded to entries because they use different
2004
                // coordinates, so validate the container before dropping its nullable wrapper.
2005
0
                if ((*parent_null_map)[i] &&
2006
0
                    (nullable_parent_null_map == nullptr || !(*nullable_parent_null_map)[i])) {
2007
0
                    return Status::InternalError(
2008
0
                            "Source map contains NULL for non-nullable table column");
2009
0
                }
2010
0
            }
2011
0
        }
2012
8
        const auto* file_map = assert_cast<const ColumnMap*>(nested_file_column);
2013
8
        ColumnPtr key_column = file_map->get_keys_ptr();
2014
8
        ColumnPtr value_column = file_map->get_values_ptr();
2015
8
        DORIS_CHECK(key_column->size() == value_column->size());
2016
8
        NullMap descendant_parent_null_map;
2017
8
        const NullMap* descendant_parent_null_map_ptr =
2018
8
                _project_collection_parent_null_map_for_hidden_entries(
2019
8
                        parent_null_map, nullable_parent_null_map, rows, file_map->get_offsets(),
2020
8
                        key_column->size(), &descendant_parent_null_map);
2021
2022
8
        const ColumnMapping* key_mapping = nullptr;
2023
8
        const ColumnMapping* value_mapping = nullptr;
2024
13
        for (const auto& child_mapping : mapping.child_mappings) {
2025
13
            if (!child_mapping.file_local_id.has_value()) {
2026
0
                continue;
2027
0
            }
2028
13
            if (*child_mapping.file_local_id == 0) {
2029
5
                key_mapping = &child_mapping;
2030
8
            } else if (*child_mapping.file_local_id == 1) {
2031
8
                value_mapping = &child_mapping;
2032
8
            }
2033
13
        }
2034
2035
8
        if (key_mapping != nullptr) {
2036
5
            RETURN_IF_ERROR(_materialize_present_child_mapping_column(
2037
5
                    *key_mapping, key_column, key_column->size(), &key_column,
2038
5
                    descendant_parent_null_map_ptr));
2039
5
        } else {
2040
3
            const auto* table_map =
2041
3
                    assert_cast<const DataTypeMap*>(remove_nullable(mapping.table_type).get());
2042
            // Value-only projection retains the physical key stream to preserve entry offsets;
2043
            // align it under the entry mask so NULL placeholders from hidden Map rows stay hidden.
2044
3
            RETURN_IF_ERROR(_align_column_nullability(&key_column, table_map->get_key_type(),
2045
3
                                                      descendant_parent_null_map_ptr));
2046
3
        }
2047
8
        if (value_mapping != nullptr) {
2048
8
            RETURN_IF_ERROR(_materialize_present_child_mapping_column(
2049
8
                    *value_mapping, value_column, value_column->size(), &value_column,
2050
8
                    descendant_parent_null_map_ptr));
2051
8
        } else {
2052
0
            const auto* table_map =
2053
0
                    assert_cast<const DataTypeMap*>(remove_nullable(mapping.table_type).get());
2054
            // A retained structural value stream follows the same hidden-entry invariant as keys.
2055
0
            RETURN_IF_ERROR(_align_column_nullability(&value_column, table_map->get_value_type(),
2056
0
                                                      descendant_parent_null_map_ptr));
2057
0
        }
2058
8
        auto offsets_column = file_map->get_offsets_ptr()->convert_to_full_column_if_const();
2059
8
        auto result = ColumnMap::create(IColumn::mutate(std::move(key_column)),
2060
8
                                        IColumn::mutate(std::move(value_column)),
2061
8
                                        IColumn::mutate(std::move(offsets_column)));
2062
8
        if (mapping.table_type->is_nullable()) {
2063
6
            auto null_map = ColumnUInt8::create();
2064
6
            auto& null_map_data = null_map->get_data();
2065
6
            null_map_data.resize(rows);
2066
6
            if (parent_null_map != nullptr) {
2067
6
                DORIS_CHECK(parent_null_map->size() == rows);
2068
6
                null_map_data.assign(parent_null_map->begin(), parent_null_map->end());
2069
6
            } else {
2070
0
                std::fill(null_map_data.begin(), null_map_data.end(), 0);
2071
0
            }
2072
6
            *column = ColumnNullable::create(std::move(result), std::move(null_map));
2073
6
        } else {
2074
2
            *column = std::move(result);
2075
2
        }
2076
8
        return Status::OK();
2077
8
    }
2078
2079
25.3k
    Status _open_mapping_expr_tree(const ColumnMapping& mapping, const RowDescriptor& row_desc) {
2080
25.3k
        if (mapping.projection != nullptr) {
2081
23.0k
            RETURN_IF_ERROR(mapping.projection->prepare(_runtime_state, row_desc));
2082
23.0k
            RETURN_IF_ERROR(mapping.projection->open(_runtime_state));
2083
23.0k
        }
2084
25.3k
        if (mapping.default_expr != nullptr) {
2085
29
            RETURN_IF_ERROR(mapping.default_expr->prepare(_runtime_state, row_desc));
2086
29
            RETURN_IF_ERROR(mapping.default_expr->open(_runtime_state));
2087
29
        }
2088
25.3k
        for (const auto& child_mapping : mapping.child_mappings) {
2089
2.22k
            RETURN_IF_ERROR(_open_mapping_expr_tree(child_mapping, row_desc));
2090
2.22k
        }
2091
25.3k
        return Status::OK();
2092
25.3k
    }
2093
2094
3.60k
    Status _open_mapping_exprs() {
2095
3.60k
        RowDescriptor row_desc;
2096
23.1k
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
2097
23.1k
            RETURN_IF_ERROR(_open_mapping_expr_tree(mapping, row_desc));
2098
23.1k
        }
2099
3.60k
        return Status::OK();
2100
3.60k
    }
2101
2102
    Status _build_file_aggregate_request(TPushAggOp::type agg_type,
2103
17
                                         FileAggregateRequest* request) const {
2104
17
        DORIS_CHECK(request != nullptr);
2105
17
        DORIS_CHECK(_supports_aggregate_pushdown(agg_type));
2106
17
        request->agg_type = agg_type;
2107
17
        request->columns.clear();
2108
17
        if (agg_type == TPushAggOp::type::COUNT) {
2109
13
            DORIS_CHECK(_push_down_count_columns.has_value());
2110
            // An empty explicit list is the semantic signal for COUNT(*). Do not inspect the
2111
            // mapping count: `SELECT COUNT(*) FROM t` may still project one nullable column because
2112
            // the planner keeps a placeholder slot. In a 10,000-row file where that arbitrary slot
2113
            // has 9,015 non-null values, passing the slot would ask Parquet/ORC metadata for
2114
            // COUNT(slot)=9,015 instead of the required row count 10,000.
2115
13
            if (!_push_down_count_columns->empty()) {
2116
4
                const auto& mapping = _push_down_count_mapping();
2117
4
                DORIS_CHECK(mapping.file_local_id.has_value());
2118
4
                FileAggregateRequest::Column column;
2119
4
                column.projection =
2120
4
                        LocalColumnIndex::top_level(LocalColumnId(*mapping.file_local_id));
2121
4
                request->columns.push_back(std::move(column));
2122
4
            }
2123
13
            return Status::OK();
2124
13
        }
2125
4
        request->columns.reserve(_data_reader.column_mapper->mappings().size());
2126
5
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
2127
5
            DORIS_CHECK(mapping.file_local_id.has_value());
2128
5
            FileAggregateRequest::Column column;
2129
5
            column.projection = LocalColumnIndex::top_level(LocalColumnId(*mapping.file_local_id));
2130
5
            if (!mapping.child_mappings.empty()) {
2131
1
                RETURN_IF_ERROR(build_aggregate_projection(mapping, &column.projection));
2132
1
            }
2133
5
            request->columns.push_back(std::move(column));
2134
5
        }
2135
4
        return Status::OK();
2136
4
    }
2137
2138
14
    const ColumnMapping& _push_down_count_mapping() const {
2139
14
        DORIS_CHECK(_push_down_count_columns.has_value());
2140
14
        DORIS_CHECK(_push_down_count_columns->size() == 1);
2141
14
        const auto mapping_it =
2142
14
                std::ranges::find(_data_reader.column_mapper->mappings(),
2143
14
                                  _push_down_count_columns->front(), &ColumnMapping::global_index);
2144
        // FileScannerV2 translates FE SlotIds through the same projected-column list used to build
2145
        // the mapper, so a missing mapping is an FE/BE contract violation rather than a fallback.
2146
14
        DORIS_CHECK(mapping_it != _data_reader.column_mapper->mappings().end());
2147
14
        return *mapping_it;
2148
14
    }
2149
2150
    Status _materialize_aggregate_pushdown_rows(TPushAggOp::type agg_type,
2151
                                                const FileAggregateResult& file_result,
2152
3
                                                Block* block) {
2153
3
        DORIS_CHECK(agg_type == TPushAggOp::type::MINMAX);
2154
        // MIN/MAX pushdown emits two rows, min first and max second, for each projected column.
2155
        // The upper MIN/MAX aggregate consumes those two rows to produce the final aggregate value.
2156
3
        DORIS_CHECK(file_result.columns.size() == _data_reader.column_mapper->mappings().size());
2157
3
        DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
2158
3
        Block file_block;
2159
3
        file_block.reserve(_data_reader.file_block_layout.size());
2160
4
        for (const auto& column : _data_reader.file_block_layout) {
2161
4
            file_block.insert({column.type->create_column(), column.type, column.name});
2162
4
        }
2163
7
        for (size_t column_idx = 0; column_idx < file_result.columns.size(); ++column_idx) {
2164
4
            const auto& result_column = file_result.columns[column_idx];
2165
4
            if (!result_column.has_min || !result_column.has_max) {
2166
0
                return Status::NotSupported("Missing min/max aggregate result for column {}",
2167
0
                                            _projected_columns[column_idx].name);
2168
0
            }
2169
4
            bool found_file_column = false;
2170
5
            for (size_t block_position = 0; block_position < _data_reader.file_block_layout.size();
2171
5
                 ++block_position) {
2172
5
                if (_data_reader.file_block_layout[block_position].file_column_id ==
2173
5
                    file_result.columns[column_idx].projection.column_id()) {
2174
4
                    found_file_column = true;
2175
4
                    auto column = file_block.get_by_position(block_position)
2176
4
                                          .type->create_column()
2177
4
                                          ->assert_mutable();
2178
4
                    RETURN_IF_ERROR(_insert_aggregate_projection_value(
2179
4
                            file_result.columns[column_idx].projection, result_column.min_value,
2180
4
                            column.get()));
2181
4
                    RETURN_IF_ERROR(_insert_aggregate_projection_value(
2182
4
                            file_result.columns[column_idx].projection, result_column.max_value,
2183
4
                            column.get()));
2184
4
                    file_block.replace_by_position(block_position, std::move(column));
2185
4
                    break;
2186
4
                }
2187
5
            }
2188
4
            DORIS_CHECK(found_file_column);
2189
4
        }
2190
7
        for (size_t column_idx = 0; column_idx < _data_reader.column_mapper->mappings().size();
2191
4
             ++column_idx) {
2192
4
            ColumnPtr table_column;
2193
4
            RETURN_IF_ERROR(_materialize_mapping_column(
2194
4
                    _data_reader.column_mapper->mappings()[column_idx], &file_block, 2,
2195
4
                    &table_column,
2196
4
                    column_idx + 1 == _data_reader.column_mapper->mappings().size()));
2197
4
            block->replace_by_position(column_idx, std::move(table_column));
2198
4
        }
2199
3
        return Status::OK();
2200
3
    }
2201
2202
    struct FileBlockColumn {
2203
        LocalColumnId file_column_id = LocalColumnId::invalid();
2204
        std::string name;
2205
        DataTypePtr type;
2206
    };
2207
2208
    struct DataReader {
2209
        std::unique_ptr<FileReader> reader;
2210
        std::unique_ptr<TableColumnMapper> column_mapper;
2211
        // Schema of the data file, also including virtual column (row position).
2212
        std::vector<ColumnDefinition> file_schema;
2213
        // Layout of the block returned by file reader, determined by column mapping and file
2214
        // schema. It is used for file reader to materialize columns into correct type and position.
2215
        std::vector<FileBlockColumn> file_block_layout;
2216
        Block block_template;
2217
    };
2218
    DataReader _data_reader;
2219
    // Latest immutable request queued to the physical reader. The file-block layout remains fixed
2220
    // for the split even while predicates are refreshed at a reader-defined granule boundary.
2221
    std::shared_ptr<FileScanRequest> _file_scan_request;
2222
    std::vector<ColumnDefinition> _projected_columns;
2223
    std::unique_ptr<ScanTask> _current_task;
2224
    std::optional<io::FileDescription> _current_file_description;
2225
    // Range-level compression has higher priority than scan-param compression. TVF/load can keep
2226
    // the logical format as CSV/TEXT while carrying the concrete compression such as GZ or LZO on
2227
    // each TFileRangeDesc, matching the old FileScanner reader contract.
2228
    TFileCompressType::type _current_range_compress_type = TFileCompressType::UNKNOWN;
2229
    std::optional<TUniqueId> _current_range_load_id;
2230
    TFileRangeDesc _current_file_range_desc;
2231
    std::shared_ptr<io::FileSystemProperties> _system_properties;
2232
    // partition key -> value
2233
    std::map<std::string, Field> _partition_values;
2234
    // Predicates built from scan conjuncts before file-level localization.
2235
    std::vector<TableFilter> _table_filters;
2236
    // Number of localized filters before the first unsafe conjunct in the original row-level
2237
    // order. This differs from scanning `_table_filters` for safety because slotless predicates are
2238
    // intentionally absent from that vector but must still act as ordering barriers.
2239
    size_t _constant_pruning_safe_filter_count = 0;
2240
    VExprContextSPtrs _conjuncts;
2241
    ReadProfile _profile;
2242
    // Parsed from row-position based delete files, including position delete and deletion vector.
2243
    DeleteRows* _delete_rows = nullptr;
2244
    DeletionVector* _deletion_vector = nullptr;
2245
    TFileScanRangeParams* _scan_params;
2246
    std::shared_ptr<io::IOContext> _io_ctx;
2247
    RuntimeState* _runtime_state;
2248
    RuntimeProfile* _scanner_profile;
2249
    parquet::ParquetProfileCache* _parquet_profile_cache = nullptr;
2250
    const std::vector<SlotDescriptor*>* _file_slot_descs = nullptr;
2251
    FileFormat _format;
2252
    TPushAggOp::type _push_down_agg_type = TPushAggOp::type::NONE;
2253
    std::optional<std::vector<GlobalIndex>> _push_down_count_columns;
2254
    size_t _batch_size = 0;
2255
    uint64_t _initial_condition_cache_digest = 0;
2256
    uint64_t _condition_cache_digest = 0;
2257
    // True only when prepare_split() received a digest for the exact conjunct snapshot used by
2258
    // this split. Standalone callers that only supplied TableReadOptions::condition_cache_digest
2259
    // keep the conservative runtime-filter guard.
2260
    bool _condition_cache_digest_covers_current_split = false;
2261
    segment_v2::ConditionCache::ExternalCacheKey _condition_cache_key;
2262
    std::shared_ptr<std::vector<bool>> _condition_cache;
2263
    std::shared_ptr<ConditionCacheContext> _condition_cache_ctx;
2264
    int64_t _condition_cache_hit_count = 0;
2265
    bool _current_reader_reached_eof = false;
2266
    int64_t _remaining_table_level_count = -1;
2267
    int64_t _remaining_file_level_count = -1;
2268
    // True only after the active split selects a table-level row-count shortcut or successfully
2269
    // materializes COUNT rows from file metadata. FileScannerV2 uses this result, rather than the
2270
    // raw aggregate opcode, to keep adaptive batching enabled for normal row-scan fallbacks.
2271
    bool _current_split_uses_metadata_count = false;
2272
    // Snapshot supplied by FileScannerV2 for the active split. It gates every shortcut that emits
2273
    // irreversible aggregate rows, not only the table-level row-count shortcut in prepare_split().
2274
    bool _all_runtime_filters_applied_for_split = true;
2275
    std::optional<GlobalRowIdContext> _global_rowid_context;
2276
    std::optional<std::vector<int64_t>> _row_ids;
2277
    bool _aggregate_pushdown_tried = false;
2278
    bool _current_split_pruned = false;
2279
    TableColumnMapperOptions _mapper_options;
2280
2281
private:
2282
    static std::optional<std::string> _get_int96_timezone_override(
2283
            const TFileScanRangeParams* params);
2284
2285
    static const ColumnDefinition* _find_column_definition(
2286
23.2k
            const std::vector<ColumnDefinition>& schema, LocalColumnId column_id) {
2287
210k
        for (const auto& field : schema) {
2288
210k
            if (field.file_local_id() == column_id.value()) {
2289
23.1k
                return &field;
2290
23.1k
            }
2291
210k
        }
2292
61
        return nullptr;
2293
23.2k
    }
2294
2295
14
    static bool _can_push_down_minmax_for_mapping(const ColumnMapping& mapping) {
2296
14
        if (mapping.child_mappings.empty()) {
2297
            // Direct mappings use a slot-ref projection to materialize the file column. The
2298
            // projection does not transform ordering; casts and other conversions are already
2299
            // represented by a non-trivial mapping and must fall back to row scanning.
2300
11
            return mapping.is_trivial;
2301
11
        }
2302
3
        const auto primitive_type = remove_nullable(mapping.file_type)->get_primitive_type();
2303
3
        if (primitive_type != TYPE_STRUCT) {
2304
1
            return false;
2305
1
        }
2306
2
        size_t mapped_children = 0;
2307
2
        const ColumnMapping* mapped_child = nullptr;
2308
2
        for (const auto& child_mapping : mapping.child_mappings) {
2309
2
            if (!child_mapping.file_local_id.has_value()) {
2310
0
                continue;
2311
0
            }
2312
2
            ++mapped_children;
2313
2
            mapped_child = &child_mapping;
2314
2
        }
2315
2
        return mapped_children == 1 && mapped_child != nullptr &&
2316
2
               _can_push_down_minmax_for_mapping(*mapped_child);
2317
3
    }
2318
2319
    static Status build_aggregate_projection(const ColumnMapping& mapping,
2320
2
                                             LocalColumnIndex* projection) {
2321
2
        DORIS_CHECK(projection != nullptr);
2322
2
        DORIS_CHECK(mapping.file_local_id.has_value());
2323
2
        *projection = LocalColumnIndex::local(*mapping.file_local_id);
2324
2
        projection->children.clear();
2325
2
        projection->project_all_children = true;
2326
2
        if (mapping.child_mappings.empty()) {
2327
1
            return Status::OK();
2328
1
        }
2329
1
        projection->project_all_children = false;
2330
1
        for (const auto& child_mapping : mapping.child_mappings) {
2331
1
            if (!child_mapping.file_local_id.has_value()) {
2332
0
                continue;
2333
0
            }
2334
1
            LocalColumnIndex child_projection;
2335
1
            RETURN_IF_ERROR(build_aggregate_projection(child_mapping, &child_projection));
2336
1
            projection->children.push_back(std::move(child_projection));
2337
1
        }
2338
1
        DORIS_CHECK(projection->children.size() == 1);
2339
1
        return Status::OK();
2340
1
    }
2341
2342
    static Status _insert_aggregate_projection_value(const LocalColumnIndex& projection,
2343
20
                                                     const Field& value, IColumn* column) {
2344
20
        DORIS_CHECK(column != nullptr);
2345
20
        if (auto* nullable_column = check_and_get_column<ColumnNullable>(*column)) {
2346
10
            RETURN_IF_ERROR(_insert_aggregate_projection_value(
2347
10
                    projection, value, &nullable_column->get_nested_column()));
2348
10
            nullable_column->get_null_map_data().push_back(0);
2349
10
            return Status::OK();
2350
10
        }
2351
10
        if (projection.project_all_children || projection.children.empty()) {
2352
8
            column->insert(value);
2353
8
            return Status::OK();
2354
8
        }
2355
2
        auto* struct_column = assert_cast<ColumnStruct*>(column);
2356
2
        DORIS_CHECK(projection.children.size() == 1);
2357
2
        const auto& child_projection = projection.children[0];
2358
2
        DORIS_CHECK(struct_column->get_columns().size() == 1);
2359
2
        RETURN_IF_ERROR(_insert_aggregate_projection_value(child_projection, value,
2360
2
                                                           &struct_column->get_column(0)));
2361
2
        return Status::OK();
2362
2
    }
2363
2364
    // Parse a DV into its compressed bitmap. Position delete files continue to use _delete_rows.
2365
    Status _parse_delete_predicates(const SplitReadOptions& options);
2366
};
2367
2368
} // namespace doris::format