Coverage Report

Created: 2026-07-10 20:48

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format_v2/table_reader.h
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#pragma once
19
20
#include <bvar/status.h>
21
22
#include <algorithm>
23
#include <exception>
24
#include <map>
25
#include <memory>
26
#include <optional>
27
#include <string>
28
#include <string_view>
29
#include <utility>
30
#include <vector>
31
32
#include "common/cast_set.h"
33
#include "common/exception.h"
34
#include "common/logging.h"
35
#include "common/status.h"
36
#include "core/assert_cast.h"
37
#include "core/block/block.h"
38
#include "core/column/column_array.h"
39
#include "core/column/column_const.h"
40
#include "core/column/column_map.h"
41
#include "core/column/column_nullable.h"
42
#include "core/column/column_struct.h"
43
#include "core/column/column_vector.h"
44
#include "core/data_type/data_type.h"
45
#include "core/data_type/data_type_array.h"
46
#include "core/data_type/data_type_map.h"
47
#include "core/data_type/data_type_nullable.h"
48
#include "core/data_type/data_type_number.h"
49
#include "core/data_type/data_type_string.h"
50
#include "core/data_type/data_type_struct.h"
51
#include "core/field.h"
52
#include "exec/common/stringop_substring.h"
53
#include "exprs/vexpr.h"
54
#include "exprs/vexpr_context.h"
55
#include "exprs/vexpr_fwd.h"
56
#include "exprs/vslot_ref.h"
57
#include "format/table/deletion_vector.h"
58
#include "format_v2/column_data.h"
59
#include "format_v2/column_mapper.h"
60
#include "format_v2/expr/cast.h"
61
#include "format_v2/expr/delete_predicate.h"
62
#include "format_v2/file_reader.h"
63
#include "format_v2/parquet/reader/column_reader.h"
64
#include "format_v2/schema_projection.h"
65
#include "gen_cpp/PlanNodes_types.h"
66
#include "io/io_common.h"
67
#include "runtime/descriptors.h"
68
#include "storage/segment/condition_cache.h"
69
70
namespace doris {
71
class Block;
72
struct DeleteFileDesc;
73
class RuntimeState;
74
} // namespace doris
75
76
namespace doris::format {
77
78
using DeleteRows = std::vector<int64_t>;
79
80
// Row-level predicates on table/global schema. They are rewritten to file-local expressions when
81
// possible, and remain the source of row-level filtering after localization.
82
struct TableFilter {
83
    VExprContextSPtr conjunct;
84
    std::vector<GlobalIndex> global_indices;
85
};
86
87
struct ScanTask {
88
86
    virtual ~ScanTask() = default;
89
90
    std::unique_ptr<io::FileDescription> data_file;
91
};
92
93
struct ProjectedColumnBuildContext {
94
    const TFileScanRangeParams* scan_params = nullptr;
95
    const TFileRangeDesc* range = nullptr;
96
    RuntimeState* runtime_state = nullptr;
97
    std::optional<ColumnDefinition> schema_column = std::nullopt;
98
    size_t next_file_column_idx = 0;
99
};
100
101
struct ReadProfile {
102
    RuntimeProfile::Counter* num_delete_files = nullptr;
103
    RuntimeProfile::Counter* num_delete_rows = nullptr;
104
    RuntimeProfile::Counter* parse_delete_file_time = nullptr;
105
    RuntimeProfile::Counter* decoded_dv_cache_hit_count = nullptr;
106
    RuntimeProfile::Counter* decoded_dv_cache_miss_count = nullptr;
107
    RuntimeProfile::Counter* dv_file_cache_hit_count = nullptr;
108
    RuntimeProfile::Counter* dv_file_cache_miss_count = nullptr;
109
    RuntimeProfile::Counter* dv_file_cache_peer_read_count = nullptr;
110
    RuntimeProfile::Counter* exec_timer = nullptr;
111
    RuntimeProfile::Counter* prepare_split_timer = nullptr;
112
    RuntimeProfile::Counter* finalize_timer = nullptr;
113
    RuntimeProfile::Counter* create_reader_timer = nullptr;
114
    RuntimeProfile::Counter* pushdown_agg_timer = nullptr;
115
    RuntimeProfile::Counter* open_reader_timer = nullptr;
116
};
117
118
struct TableReadOptions {
119
    // Columns need to be read from file and output by table reader. They are all in table/global
120
    // schema semantics.
121
    const std::vector<ColumnDefinition> projected_columns;
122
    // All complex conjuncts from scan operator
123
    const VExprContextSPtrs conjuncts;
124
    // File format of the underlying data files, needed for reader initialization and reader-level
125
    // filter pushdown.
126
    const FileFormat format;
127
    TFileScanRangeParams* scan_params;
128
    std::shared_ptr<io::IOContext> io_ctx;
129
    RuntimeState* runtime_state;
130
    RuntimeProfile* scanner_profile;
131
    // File formats without complete self-describing metadata, such as CSV, Text, and JSON, need
132
    // the FE-planned physical file slots to build their file-local schema and deserialize values.
133
    const std::vector<SlotDescriptor*>* file_slot_descs = nullptr;
134
    // Push-down aggregate type.
135
    const TPushAggOp::type push_down_agg_type = TPushAggOp::type::NONE;
136
    // Digest of stable pushed-down predicates. A zero digest disables condition cache.
137
    uint64_t condition_cache_digest = 0;
138
};
139
140
struct SplitReadOptions {
141
    // 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.
142
    std::map<std::string, Field> partition_values;
143
    ShardedKVCache* cache;
144
    TFileRangeDesc current_range;
145
    FileFormat current_split_format = FileFormat::PARQUET;
146
    std::optional<GlobalRowIdContext> global_rowid_context;
147
};
148
149
// Base class for table-level readers.
150
// This layer owns common table-level orchestration, such as split iteration, dynamic partition
151
// pruning, delete handling and conversion from file-local blocks to table-schema blocks. Concrete
152
// table-format readers only need to provide format-specific hooks for opening readers and parsing
153
// split metadata.
154
class TableReader {
155
public:
156
121
    virtual ~TableReader() = default;
157
158
    // Initialize common runtime options for the table reader. Subclasses may call this from their
159
    // own init(options); table-format schema and split metadata are provided later per split.
160
    virtual Status init(TableReadOptions&& options);
161
162
    // FileScannerV2 adjusts this before each get_block() using an adaptive bytes-per-row estimate.
163
    // Store it here as well as forwarding to the current reader so newly opened split readers start
164
    // with the latest predicted batch size.
165
0
    void set_batch_size(size_t batch_size) {
166
0
        _batch_size = std::max<size_t>(1, batch_size);
167
0
        if (_data_reader.reader != nullptr) {
168
0
            _data_reader.reader->set_batch_size(_batch_size);
169
0
        }
170
0
    }
171
172
    // Prepare for reading a new split/task.
173
    // 1. Pass a new split/task to reader, which will be used in subsequent open_reader() to initialize the underlying file reader.
174
    // 2. Parse delete predicates from split/task information, which will be used for later dynamic filtering and delete handling.
175
    virtual Status prepare_split(const SplitReadOptions& options);
176
177
    // Public entry point for reading a table-schema block. The base class opens the current reader,
178
    // advances across EOF, and closes exhausted readers. Subclasses provide protected hooks for
179
    // table-format-specific behavior.
180
85
    virtual Status get_block(Block* block, bool* eos) {
181
85
        SCOPED_TIMER(_profile.exec_timer);
182
85
        DORIS_CHECK(block->columns() == _projected_columns.size());
183
85
        block->clear_column_data(_projected_columns.size());
184
185
96
        while (true) {
186
96
            if (*eos) {
187
0
                return Status::OK();
188
0
            }
189
96
            if (_io_ctx != nullptr && _io_ctx->should_stop) {
190
0
                *eos = true;
191
0
                return Status::OK();
192
0
            }
193
96
            if (!_data_reader.reader) {
194
86
                if (_is_table_level_count_active()) {
195
6
                    RETURN_IF_ERROR(_read_table_level_count(block, eos));
196
6
                    return Status::OK();
197
6
                }
198
80
                RETURN_IF_ERROR(create_next_reader(eos));
199
80
                if (!_data_reader.reader) {
200
13
                    DCHECK(*eos);
201
13
                    return Status::OK();
202
13
                }
203
80
            }
204
205
            // Materialize a reduced row set for upper aggregate operators when aggregate
206
            // pushdown can be applied. This is not the final aggregate result: COUNT emits
207
            // `count` default rows for the upper COUNT(*), and MIN/MAX emits two rows containing
208
            // file-level min/max values for the upper MIN/MAX.
209
77
            if (!_aggregate_pushdown_tried) {
210
67
                SCOPED_TIMER(_profile.pushdown_agg_timer);
211
67
                bool pushed_down = false;
212
67
                const auto status = _try_materialize_aggregate_pushdown_rows(block, &pushed_down);
213
67
                if (!status.ok()) {
214
1
                    if (_io_ctx != nullptr && _io_ctx->should_stop &&
215
1
                        status.is<ErrorCode::END_OF_FILE>()) {
216
1
                        *eos = true;
217
1
                        return Status::OK();
218
1
                    }
219
0
                    return status;
220
1
                }
221
66
                if (pushed_down) {
222
7
                    return Status::OK();
223
7
                }
224
66
            }
225
226
69
            bool current_eof = false;
227
69
            _data_reader.block_template.clear_column_data(
228
69
                    cast_set<int64_t>(_data_reader.file_block_layout.size()));
229
69
            size_t current_rows = 0;
230
69
            RETURN_IF_ERROR(_data_reader.reader->get_block(&_data_reader.block_template,
231
69
                                                           &current_rows, &current_eof));
232
69
            if (current_rows == 0) {
233
11
                if (current_eof) {
234
11
                    _current_reader_reached_eof = true;
235
11
                    RETURN_IF_ERROR(close_current_reader());
236
11
                }
237
11
                continue;
238
11
            }
239
69
            DCHECK_EQ(_data_reader.block_template.columns(), _data_reader.file_block_layout.size())
240
0
                    << _data_reader.block_template.dump_structure();
241
58
#ifndef NDEBUG
242
58
            RETURN_IF_ERROR(_check_file_block_columns("after file reader get_block", current_rows));
243
58
#endif
244
58
            DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
245
58
            RETURN_IF_ERROR(finalize_chunk(block, current_rows));
246
58
#ifndef NDEBUG
247
58
            RETURN_IF_ERROR(
248
58
                    _check_table_block_columns("after finalize_chunk", block, current_rows));
249
58
#endif
250
58
            if (current_eof) {
251
6
                _current_reader_reached_eof = true;
252
6
                RETURN_IF_ERROR(close_current_reader());
253
6
            }
254
58
            return Status::OK();
255
58
        }
256
85
    }
257
258
    // Close the table reader and the currently active file reader. Subclasses that hold additional
259
    // table-format resources should override this and call TableReader::close() first.
260
73
    virtual Status close() {
261
73
        if (_data_reader.reader) {
262
42
            RETURN_IF_ERROR(close_current_reader());
263
42
        }
264
73
        _current_task.reset();
265
73
        _current_file_description.reset();
266
73
        _remaining_table_level_count = -1;
267
73
        return Status::OK();
268
73
    }
269
270
3
    int64_t condition_cache_hit_count() const { return _condition_cache_hit_count; }
271
272
    virtual std::string debug_string() const;
273
274
    virtual Status annotate_projected_column(const TFileScanSlotInfo& slot_info,
275
                                             ProjectedColumnBuildContext* context,
276
                                             ColumnDefinition* column) const;
277
278
0
    virtual Status validate_projected_columns(const ProjectedColumnBuildContext& context) const {
279
0
        (void)context;
280
0
        return Status::OK();
281
0
    }
282
283
protected:
284
    // Parse deletion vector information from table format specific file description.
285
    virtual Status _parse_deletion_vector_file(const TTableFormatFileDesc& t_desc,
286
52
                                               DeleteFileDesc* desc, bool* has_delete_file) {
287
52
        *has_delete_file = false;
288
52
        return Status::OK();
289
52
    }
290
291
    // Advance to the next reader. This closes the current reader first and then opens the next
292
    // concrete reader. Subclasses should not duplicate this loop.
293
    Status create_next_reader(bool* eos);
294
    virtual Status create_file_reader(std::unique_ptr<FileReader>* reader);
295
47
    virtual TableColumnMappingMode mapping_mode() const { return TableColumnMappingMode::BY_NAME; }
296
67
    virtual Status annotate_file_schema(std::vector<ColumnDefinition>* file_schema) {
297
67
        DORIS_CHECK(file_schema != nullptr);
298
67
        return Status::OK();
299
67
    }
300
301
    // Open the concrete reader for the current split/task and build the file-local scan request.
302
68
    virtual Status open_reader() {
303
68
        SCOPED_TIMER(_profile.open_reader_timer);
304
        // 1. Get file schema and create column mapping.
305
68
        std::vector<ColumnDefinition> file_schema;
306
68
        RETURN_IF_ERROR(_data_reader.reader->get_schema(&file_schema));
307
        // For Paimon/Hudi, FE can provide field ids through `history_schema_info`. Annotate the
308
        // file schema before column mapping when the table format maps columns by field id.
309
68
        RETURN_IF_ERROR(annotate_file_schema(&file_schema));
310
68
        _data_reader.file_schema = file_schema;
311
68
        _mapper_options.mode = mapping_mode();
312
313
68
        _data_reader.column_mapper = _data_reader.reader->create_column_mapper(_mapper_options);
314
68
        DORIS_CHECK(_data_reader.column_mapper != nullptr);
315
68
        RETURN_IF_ERROR(_data_reader.column_mapper->create_mapping(_projected_columns,
316
68
                                                                   _partition_values, file_schema));
317
68
        DORIS_CHECK(_data_reader.column_mapper->mappings().size() == _projected_columns.size());
318
319
        // 2. Build table filters based on conjuncts and column predicates.
320
68
        RETURN_IF_ERROR(_build_table_filters_from_conjuncts());
321
322
        // 3. Create file scan request based on column mapping and table filters, then open file
323
        // reader with the request. File scan request carries row-level expression filters and
324
        // file-level pruning hints. Only expression filters decide returned rows.
325
68
        auto file_request = std::make_shared<FileScanRequest>();
326
68
        RETURN_IF_ERROR(_data_reader.column_mapper->create_scan_request(
327
68
                _table_filters, _projected_columns, file_request.get(), _runtime_state));
328
68
        bool constant_filter_pruned_split = false;
329
68
        RETURN_IF_ERROR(_evaluate_constant_filters(&constant_filter_pruned_split));
330
68
        if (constant_filter_pruned_split) {
331
1
            RETURN_IF_ERROR(close_current_reader());
332
1
            return Status::OK();
333
1
        }
334
67
        RETURN_IF_ERROR(customize_file_scan_request(file_request.get()));
335
67
        RETURN_IF_ERROR(_open_local_filter_exprs(*file_request));
336
67
        _data_reader.file_block_layout.clear();
337
67
        _data_reader.block_template.clear();
338
67
        _data_reader.file_block_layout.resize(file_request->local_positions.size());
339
340
        // 4. Build file block layout from file schema and column mapping. The layout describes
341
        // the block returned by file reader before table-column materialization.
342
97
        for (const auto& [file_column_id, block_position] : file_request->local_positions) {
343
97
            DORIS_CHECK(block_position.value() < _data_reader.file_block_layout.size());
344
97
            const auto* field = _find_column_definition(_data_reader.file_schema, file_column_id);
345
97
            DORIS_CHECK(field != nullptr);
346
347
97
            ColumnDefinition projected_field;
348
97
            {
349
97
                auto it = std::find_if(
350
97
                        file_request->non_predicate_columns.begin(),
351
97
                        file_request->non_predicate_columns.end(),
352
112
                        [&](const LocalColumnIndex& p) { return p.column_id() == file_column_id; });
353
97
                if (it != file_request->non_predicate_columns.end()) {
354
64
                    RETURN_IF_ERROR(project_column_definition(*field, *it, &projected_field));
355
64
                }
356
97
            }
357
97
            {
358
97
                auto it = std::find_if(
359
97
                        file_request->predicate_columns.begin(),
360
97
                        file_request->predicate_columns.end(),
361
97
                        [&](const LocalColumnIndex& p) { return p.column_id() == file_column_id; });
362
97
                if (it != file_request->predicate_columns.end()) {
363
33
                    RETURN_IF_ERROR(project_column_definition(*field, *it, &projected_field));
364
33
                }
365
97
            }
366
97
            _data_reader.file_block_layout[block_position.value()] = {
367
97
                    .file_column_id = file_column_id,
368
97
                    .name = projected_field.name,
369
97
                    .type = projected_field.type,
370
97
            };
371
97
            DORIS_CHECK(_data_reader.file_block_layout[block_position.value()].type != nullptr);
372
97
        }
373
374
        // 5. Prepare block template from file block layout. The block template stores the block
375
        // returned by file reader before table-column materialization.
376
67
        _data_reader.block_template.reserve(_data_reader.file_block_layout.size());
377
97
        for (const auto& column : _data_reader.file_block_layout) {
378
97
            _data_reader.block_template.insert(
379
97
                    {column.type->create_column(), column.type, column.name});
380
97
        }
381
67
        if (VLOG_DEBUG_IS_ON) {
382
0
            VLOG_DEBUG << "TableReader debug: " << debug_string();
383
0
        }
384
67
        RETURN_IF_ERROR(_open_mapping_exprs());
385
67
        RETURN_IF_ERROR(_data_reader.reader->open(file_request));
386
67
        RETURN_IF_ERROR(_init_reader_condition_cache(*file_request));
387
67
        return Status::OK();
388
67
    }
389
390
    Status _build_table_filters_from_conjuncts();
391
    Status _open_local_filter_exprs(const FileScanRequest& file_request);
392
    Status _init_reader_condition_cache(const FileScanRequest& file_request);
393
    void _finalize_reader_condition_cache();
394
    bool _should_enable_condition_cache(const FileScanRequest& file_request) const;
395
396
68
    Status _evaluate_constant_filters(bool* can_filter_all) {
397
68
        DORIS_CHECK(can_filter_all != nullptr);
398
68
        *can_filter_all = false;
399
68
        for (const auto& table_filter : _table_filters) {
400
25
            if (table_filter.conjunct == nullptr ||
401
                // RuntimeFilterExpr does not implement execute_column_impl(); it is evaluated by
402
                // the row-level filter path through execute_filter(). Constant split pruning uses
403
                // VExprContext::execute() on a one-row synthetic block, so runtime filters must not
404
                // be pre-executed here even when their referenced slot maps to a constant value.
405
25
                table_filter.conjunct->root()->is_rf_wrapper() ||
406
25
                !_table_filter_has_only_constant_entries(table_filter)) {
407
23
                continue;
408
23
            }
409
2
            Block eval_block;
410
2
            RETURN_IF_ERROR(_build_constant_filter_block(table_filter, &eval_block));
411
2
            RowDescriptor row_desc;
412
2
            RETURN_IF_ERROR(table_filter.conjunct->prepare(_runtime_state, row_desc));
413
2
            RETURN_IF_ERROR(table_filter.conjunct->open(_runtime_state));
414
2
            int result_column_id = -1;
415
2
            RETURN_IF_ERROR(table_filter.conjunct->execute(&eval_block, &result_column_id));
416
2
            DORIS_CHECK(result_column_id >= 0);
417
2
            if (_filter_result_filters_all(eval_block.get_by_position(result_column_id).column)) {
418
1
                *can_filter_all = true;
419
1
                return Status::OK();
420
1
            }
421
2
        }
422
67
        return Status::OK();
423
68
    }
424
425
23
    bool _table_filter_has_only_constant_entries(const TableFilter& table_filter) const {
426
23
        const auto& filter_entries = _data_reader.column_mapper->filter_entries();
427
23
        for (const auto global_index : table_filter.global_indices) {
428
23
            const auto entry_it = filter_entries.find(global_index);
429
23
            if (entry_it == filter_entries.end() || !entry_it->second.is_constant()) {
430
21
                return false;
431
21
            }
432
23
        }
433
2
        return !table_filter.global_indices.empty();
434
23
    }
435
436
2
    Status _build_constant_filter_block(const TableFilter& table_filter, Block* eval_block) {
437
2
        DORIS_CHECK(eval_block != nullptr);
438
2
        eval_block->clear();
439
2
        const auto& mappings = _data_reader.column_mapper->mappings();
440
2
        const auto& filter_entries = _data_reader.column_mapper->filter_entries();
441
2
        DORIS_CHECK(mappings.size() == _projected_columns.size());
442
4
        for (size_t column_idx = 0; column_idx < mappings.size(); ++column_idx) {
443
2
            const auto global_index = GlobalIndex(column_idx);
444
2
            const auto& mapping = mappings[column_idx];
445
2
            const auto entry_it = filter_entries.find(global_index);
446
2
            const bool referenced_by_filter =
447
2
                    std::find(table_filter.global_indices.begin(),
448
2
                              table_filter.global_indices.end(),
449
2
                              global_index) != table_filter.global_indices.end();
450
2
            if (referenced_by_filter && entry_it != filter_entries.end() &&
451
2
                entry_it->second.is_constant()) {
452
2
                ColumnPtr constant_column;
453
2
                RETURN_IF_ERROR(_materialize_constant_filter_column(
454
2
                        entry_it->second.constant_index(), &constant_column));
455
2
                eval_block->insert({std::move(constant_column), mapping.table_type,
456
2
                                    mapping.table_column_name});
457
2
            } else {
458
0
                eval_block->insert({mapping.table_type->create_column_const_with_default_value(1),
459
0
                                    mapping.table_type, mapping.table_column_name});
460
0
            }
461
2
        }
462
2
        return Status::OK();
463
2
    }
464
465
2
    Status _materialize_constant_filter_column(ConstantIndex constant_index, ColumnPtr* column) {
466
2
        DORIS_CHECK(column != nullptr);
467
2
        const auto& constant_entry = _data_reader.column_mapper->constant_map().get(constant_index);
468
2
        DORIS_CHECK(constant_entry.expr != nullptr);
469
2
        DORIS_CHECK(constant_entry.type != nullptr);
470
2
        RowDescriptor row_desc;
471
2
        RETURN_IF_ERROR(constant_entry.expr->prepare(_runtime_state, row_desc));
472
2
        RETURN_IF_ERROR(constant_entry.expr->open(_runtime_state));
473
2
        Block eval_block;
474
2
        eval_block.insert({constant_entry.type->create_column_const_with_default_value(1),
475
2
                           constant_entry.type, "__table_reader_constant_filter"});
476
2
        int result_column_id = -1;
477
2
        RETURN_IF_ERROR(constant_entry.expr->execute(&eval_block, &result_column_id));
478
2
        DORIS_CHECK(result_column_id >= 0);
479
2
        *column = eval_block.get_by_position(result_column_id).column;
480
2
        DORIS_CHECK((*column)->size() == 1);
481
2
        return Status::OK();
482
2
    }
483
484
2
    static bool _filter_result_filters_all(const ColumnPtr& filter_column) {
485
2
        DORIS_CHECK(filter_column.get() != nullptr);
486
2
        DORIS_CHECK(filter_column->size() == 1);
487
2
        return !filter_column->get_bool(0);
488
2
    }
489
490
68
    virtual Status customize_file_scan_request(FileScanRequest* file_request) {
491
68
        return _append_delete_predicate(file_request);
492
68
    }
493
494
196
    bool _is_table_level_count_active() const { return _remaining_table_level_count >= 0; }
495
496
7
    Status _materialize_count_rows(size_t rows, Block* block) const {
497
7
        DORIS_CHECK(block != nullptr);
498
7
        DORIS_CHECK(block->columns() > 0 || rows == 0);
499
14
        for (size_t column_idx = 0; column_idx < block->columns(); ++column_idx) {
500
7
            auto column = block->get_by_position(column_idx).type->create_column();
501
7
            column->resize(rows);
502
7
            block->replace_by_position(column_idx, std::move(column));
503
7
        }
504
7
        return Status::OK();
505
7
    }
506
507
6
    Status _read_table_level_count(Block* block, bool* eos) {
508
6
        DORIS_CHECK(block != nullptr);
509
6
        DORIS_CHECK(eos != nullptr);
510
6
        DORIS_CHECK(_push_down_agg_type == TPushAggOp::type::COUNT);
511
6
        DORIS_CHECK(_remaining_table_level_count >= 0);
512
6
        if (_remaining_table_level_count == 0) {
513
2
            _remaining_table_level_count = -1;
514
2
            _current_task.reset();
515
2
            *eos = true;
516
2
            return Status::OK();
517
2
        }
518
519
4
        const int64_t batch_size = _runtime_state == nullptr
520
4
                                           ? _remaining_table_level_count
521
4
                                           : static_cast<int64_t>(_runtime_state->batch_size());
522
4
        const auto rows = std::min(_remaining_table_level_count, batch_size);
523
4
        RETURN_IF_ERROR(_materialize_count_rows(cast_set<size_t>(rows), block));
524
4
        _remaining_table_level_count -= rows;
525
4
        *eos = false;
526
4
        return Status::OK();
527
4
    }
528
529
    void _append_file_scan_column(FileScanRequest* request, LocalColumnId column_id,
530
25
                                  std::vector<LocalColumnIndex>* scan_columns) {
531
25
        DORIS_CHECK(request != nullptr);
532
25
        DORIS_CHECK(scan_columns != nullptr);
533
25
        FileScanRequestBuilder builder(request);
534
25
        Status status;
535
25
        if (scan_columns == &request->predicate_columns) {
536
14
            status = builder.add_predicate_column(column_id);
537
14
        } else {
538
11
            DORIS_CHECK(scan_columns == &request->non_predicate_columns);
539
11
            status = builder.add_non_predicate_column(column_id);
540
11
        }
541
25
        DORIS_CHECK(status.ok()) << status.to_string();
542
25
        if (column_id == LocalColumnId(ROW_POSITION_COLUMN_ID) &&
543
25
            _find_column_definition(_data_reader.file_schema, column_id) == nullptr) {
544
18
            _data_reader.file_schema.push_back(row_position_column_definition());
545
18
        }
546
25
    }
547
548
    // 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.
549
68
    Status _append_delete_predicate(FileScanRequest* request) {
550
68
        DORIS_CHECK(request != nullptr);
551
68
        if ((_delete_rows == nullptr || _delete_rows->empty()) &&
552
68
            (_deletion_vector == nullptr || _deletion_vector->isEmpty())) {
553
57
            return Status::OK();
554
57
        }
555
11
        const auto row_position_column_id = LocalColumnId(ROW_POSITION_COLUMN_ID);
556
11
        _append_file_scan_column(request, row_position_column_id, &request->predicate_columns);
557
558
11
        const auto block_position = request->local_positions.at(row_position_column_id);
559
11
        auto append_predicate = [&](auto& deleted_rows) {
560
11
            auto delete_predicate = std::make_shared<DeletePredicate>(deleted_rows);
561
11
            delete_predicate->add_child(VSlotRef::create_shared(
562
11
                    cast_set<int>(block_position.value()), cast_set<int>(block_position.value()),
563
11
                    -1, std::make_shared<DataTypeInt64>(), ROW_POSITION_COLUMN_NAME));
564
11
            request->delete_conjuncts.push_back(
565
11
                    VExprContext::create_shared(std::move(delete_predicate)));
566
11
        };
_ZZN5doris6format11TableReader24_append_delete_predicateEPNS0_15FileScanRequestEENKUlRT_E_clISt6vectorIlSaIlEEEEDaS5_
Line
Count
Source
559
7
        auto append_predicate = [&](auto& deleted_rows) {
560
7
            auto delete_predicate = std::make_shared<DeletePredicate>(deleted_rows);
561
7
            delete_predicate->add_child(VSlotRef::create_shared(
562
7
                    cast_set<int>(block_position.value()), cast_set<int>(block_position.value()),
563
7
                    -1, std::make_shared<DataTypeInt64>(), ROW_POSITION_COLUMN_NAME));
564
7
            request->delete_conjuncts.push_back(
565
7
                    VExprContext::create_shared(std::move(delete_predicate)));
566
7
        };
_ZZN5doris6format11TableReader24_append_delete_predicateEPNS0_15FileScanRequestEENKUlRT_E_clIN7roaring12Roaring64MapEEEDaS5_
Line
Count
Source
559
4
        auto append_predicate = [&](auto& deleted_rows) {
560
4
            auto delete_predicate = std::make_shared<DeletePredicate>(deleted_rows);
561
4
            delete_predicate->add_child(VSlotRef::create_shared(
562
4
                    cast_set<int>(block_position.value()), cast_set<int>(block_position.value()),
563
4
                    -1, std::make_shared<DataTypeInt64>(), ROW_POSITION_COLUMN_NAME));
564
4
            request->delete_conjuncts.push_back(
565
4
                    VExprContext::create_shared(std::move(delete_predicate)));
566
4
        };
567
11
        if (_delete_rows != nullptr && !_delete_rows->empty()) {
568
7
            append_predicate(*_delete_rows);
569
7
        }
570
11
        if (_deletion_vector != nullptr && !_deletion_vector->isEmpty()) {
571
4
            append_predicate(*_deletion_vector);
572
4
        }
573
11
        return Status::OK();
574
68
    }
575
576
    // Close the current concrete reader. This hook is called by both create_next_reader() and
577
    // close(), so it should remain idempotent.
578
67
    virtual Status close_current_reader() {
579
67
        _finalize_reader_condition_cache();
580
67
        RETURN_IF_ERROR(_data_reader.reader->close());
581
67
        _data_reader.reader.reset();
582
67
        if (_data_reader.column_mapper != nullptr) {
583
67
            _data_reader.column_mapper->clear();
584
67
            _data_reader.column_mapper.reset();
585
67
        }
586
67
        _table_filters.clear();
587
67
        _data_reader.file_schema.clear();
588
67
        _data_reader.file_block_layout.clear();
589
67
        _data_reader.block_template.clear();
590
67
        _current_task.reset();
591
67
        _current_file_description.reset();
592
67
        _current_reader_reached_eof = false;
593
67
        return Status::OK();
594
67
    }
595
596
0
    void _record_scan_rows(size_t rows) {
597
0
        if (_io_ctx != nullptr && _io_ctx->file_reader_stats != nullptr) {
598
0
            _io_ctx->file_reader_stats->read_rows += rows;
599
0
        }
600
0
    }
601
602
    // Finalize file-local block to table/global schema block.
603
58
    Status finalize_chunk(Block* block, const size_t rows) {
604
58
        SCOPED_TIMER(_profile.finalize_timer);
605
58
        size_t idx = 0;
606
87
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
607
87
            ColumnPtr column;
608
87
            RETURN_IF_ERROR(_materialize_mapping_column(mapping, &_data_reader.block_template, rows,
609
87
                                                        &column));
610
87
            block->replace_by_position(idx, IColumn::mutate(std::move(column)));
611
87
            idx++;
612
87
        }
613
58
        RETURN_IF_ERROR(materialize_virtual_columns(block));
614
        // Enforce CHAR/VARCHAR length declared by the table schema after all file-to-table
615
        // materialization has finished.
616
58
        RETURN_IF_ERROR(_truncate_char_or_varchar_columns(block));
617
58
        return Status::OK();
618
58
    }
619
620
    // Materialize virtual columns in the table block, such as Iceberg _row_id and
621
    // _last_updated_sequence_number. This runs after normal column materialization so finalize
622
    // expressions can reference those virtual columns.
623
38
    virtual Status materialize_virtual_columns(Block* table_block) { return Status::OK(); }
624
625
#ifndef NDEBUG
626
58
    Status _check_file_block_columns(std::string_view stage, size_t rows) {
627
58
        DORIS_CHECK(_data_reader.block_template.columns() == _data_reader.file_block_layout.size());
628
144
        for (size_t idx = 0; idx < _data_reader.block_template.columns(); ++idx) {
629
86
            const auto& file_block_column = _data_reader.file_block_layout[idx];
630
86
            const auto& column_with_type = _data_reader.block_template.get_by_position(idx);
631
86
            const auto* column = column_with_type.column.get();
632
86
            try {
633
86
                if (column == nullptr) {
634
0
                    auto st = Status::InternalError(
635
0
                            "Invalid file block column {} at {}: file_column_id={}, name='{}', "
636
0
                            "type={}, column=null, expected_rows={}, reader={}",
637
0
                            idx, stage, file_block_column.file_column_id.value(),
638
0
                            file_block_column.name,
639
0
                            file_block_column.type == nullptr ? "null"
640
0
                                                              : file_block_column.type->get_name(),
641
0
                            rows, debug_string());
642
0
                    LOG(WARNING) << st;
643
0
                    return st;
644
0
                }
645
86
                column->sanity_check();
646
86
                auto st = column_with_type.check_type_and_column_match();
647
86
                if (!st.ok()) {
648
0
                    auto contextual_status = Status::InternalError(
649
0
                            "Invalid file block column {} at {}: file_column_id={}, name='{}', "
650
0
                            "type={}, column={}, column_size={}, expected_rows={}, error={}, "
651
0
                            "reader={}",
652
0
                            idx, stage, file_block_column.file_column_id.value(),
653
0
                            file_block_column.name,
654
0
                            file_block_column.type == nullptr ? "null"
655
0
                                                              : file_block_column.type->get_name(),
656
0
                            column->get_name(), column->size(), rows, st.to_string(),
657
0
                            debug_string());
658
0
                    LOG(WARNING) << contextual_status;
659
0
                    return contextual_status;
660
0
                }
661
86
            } catch (const Exception& e) {
662
0
                auto st = Status::InternalError(
663
0
                        "Invalid file block column {} at {}: file_column_id={}, name='{}', "
664
0
                        "type={}, column={}, column_size={}, expected_rows={}, error={}, "
665
0
                        "reader={}",
666
0
                        idx, stage, file_block_column.file_column_id.value(),
667
0
                        file_block_column.name,
668
0
                        file_block_column.type == nullptr ? "null"
669
0
                                                          : file_block_column.type->get_name(),
670
0
                        column == nullptr ? "null" : column->get_name(),
671
0
                        column == nullptr ? 0 : column->size(), rows, e.to_string(),
672
0
                        debug_string());
673
0
                LOG(WARNING) << st;
674
0
                return st;
675
0
            } catch (const std::exception& e) {
676
0
                auto st = Status::InternalError(
677
0
                        "Invalid file block column {} at {}: file_column_id={}, name='{}', "
678
0
                        "type={}, column={}, column_size={}, expected_rows={}, error={}, "
679
0
                        "reader={}",
680
0
                        idx, stage, file_block_column.file_column_id.value(),
681
0
                        file_block_column.name,
682
0
                        file_block_column.type == nullptr ? "null"
683
0
                                                          : file_block_column.type->get_name(),
684
0
                        column == nullptr ? "null" : column->get_name(),
685
0
                        column == nullptr ? 0 : column->size(), rows, e.what(), debug_string());
686
0
                LOG(WARNING) << st;
687
0
                return st;
688
0
            }
689
86
        }
690
58
        return Status::OK();
691
58
    }
692
693
58
    Status _check_table_block_columns(std::string_view stage, const Block* block, size_t rows) {
694
58
        DORIS_CHECK(block != nullptr);
695
58
        DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
696
145
        for (size_t idx = 0; idx < block->columns(); ++idx) {
697
87
            const auto& mapping = _data_reader.column_mapper->mappings()[idx];
698
87
            const auto& column_with_type = block->get_by_position(idx);
699
87
            const auto* column = column_with_type.column.get();
700
87
            try {
701
87
                if (column == nullptr) {
702
0
                    auto st = Status::InternalError(
703
0
                            "Invalid table block column {} at {}: table_column='{}', "
704
0
                            "global_index={}, type={}, column=null, expected_rows={}, mapping={}",
705
0
                            idx, stage, mapping.table_column_name, mapping.global_index.value(),
706
0
                            mapping.table_type == nullptr ? "null" : mapping.table_type->get_name(),
707
0
                            rows, mapping.debug_string());
708
0
                    LOG(WARNING) << st;
709
0
                    return st;
710
0
                }
711
87
                column->sanity_check();
712
87
                auto st = column_with_type.check_type_and_column_match();
713
87
                if (!st.ok()) {
714
0
                    auto contextual_status = Status::InternalError(
715
0
                            "Invalid table block column {} at {}: table_column='{}', "
716
0
                            "global_index={}, type={}, column={}, column_size={}, "
717
0
                            "expected_rows={}, error={}, mapping={}",
718
0
                            idx, stage, mapping.table_column_name, mapping.global_index.value(),
719
0
                            mapping.table_type == nullptr ? "null" : mapping.table_type->get_name(),
720
0
                            column->get_name(), column->size(), rows, st.to_string(),
721
0
                            mapping.debug_string());
722
0
                    LOG(WARNING) << contextual_status;
723
0
                    return contextual_status;
724
0
                }
725
87
            } catch (const Exception& e) {
726
0
                auto st = Status::InternalError(
727
0
                        "Invalid table block column {} at {}: table_column='{}', global_index={}, "
728
0
                        "type={}, column={}, column_size={}, expected_rows={}, error={}, "
729
0
                        "mapping={}",
730
0
                        idx, stage, mapping.table_column_name, mapping.global_index.value(),
731
0
                        mapping.table_type == nullptr ? "null" : mapping.table_type->get_name(),
732
0
                        column == nullptr ? "null" : column->get_name(),
733
0
                        column == nullptr ? 0 : column->size(), rows, e.to_string(),
734
0
                        mapping.debug_string());
735
0
                LOG(WARNING) << st;
736
0
                return st;
737
0
            } catch (const std::exception& e) {
738
0
                auto st = Status::InternalError(
739
0
                        "Invalid table block column {} at {}: table_column='{}', global_index={}, "
740
0
                        "type={}, column={}, column_size={}, expected_rows={}, error={}, "
741
0
                        "mapping={}",
742
0
                        idx, stage, mapping.table_column_name, mapping.global_index.value(),
743
0
                        mapping.table_type == nullptr ? "null" : mapping.table_type->get_name(),
744
0
                        column == nullptr ? "null" : column->get_name(),
745
0
                        column == nullptr ? 0 : column->size(), rows, e.what(),
746
0
                        mapping.debug_string());
747
0
                LOG(WARNING) << st;
748
0
                return st;
749
0
            }
750
87
        }
751
58
        return Status::OK();
752
58
    }
753
#endif
754
755
58
    Status _truncate_char_or_varchar_columns(Block* block) {
756
58
        DORIS_CHECK(block != nullptr);
757
58
        if (_runtime_state == nullptr ||
758
58
            !_runtime_state->query_options().truncate_char_or_varchar_columns) {
759
58
            return Status::OK();
760
58
        }
761
0
        DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
762
0
        for (size_t idx = 0; idx < _data_reader.column_mapper->mappings().size(); ++idx) {
763
0
            const auto& mapping = _data_reader.column_mapper->mappings()[idx];
764
0
            if (!_should_truncate_char_or_varchar_column(mapping)) {
765
0
                continue;
766
0
            }
767
0
            const auto target_len =
768
0
                    assert_cast<const DataTypeString*>(remove_nullable(mapping.table_type).get())
769
0
                            ->len();
770
0
            _truncate_char_or_varchar_column(block, idx, target_len);
771
0
        }
772
0
        return Status::OK();
773
58
    }
774
775
    // Return true when the table schema has a bounded CHAR/VARCHAR length that is stricter than
776
    // the file-side type. Examples:
777
    // - table VARCHAR(10), file VARCHAR(20): truncate to 10;
778
    // - table VARCHAR(10), file STRING: truncate to 10 because STRING has no declared bound;
779
    // - table STRING, any file type: no truncation because the target has no bound.
780
5
    static bool _should_truncate_char_or_varchar_column(const ColumnMapping& mapping) {
781
5
        if (mapping.table_type == nullptr) {
782
0
            return false;
783
0
        }
784
5
        const auto table_type = remove_nullable(mapping.table_type);
785
5
        const auto primitive_type = table_type->get_primitive_type();
786
5
        if (primitive_type != TYPE_VARCHAR && primitive_type != TYPE_CHAR) {
787
1
            return false;
788
1
        }
789
4
        const auto target_len = assert_cast<const DataTypeString*>(table_type.get())->len();
790
4
        if (target_len <= 0) {
791
0
            return false;
792
0
        }
793
4
        if (mapping.file_type == nullptr) {
794
0
            return true;
795
0
        }
796
4
        const auto file_type = remove_nullable(mapping.file_type);
797
4
        DORIS_CHECK(file_type != nullptr);
798
4
        int file_len = -1;
799
4
        if (file_type->get_primitive_type() == TYPE_VARCHAR ||
800
4
            file_type->get_primitive_type() == TYPE_CHAR ||
801
4
            file_type->get_primitive_type() == TYPE_STRING) {
802
3
            file_len = assert_cast<const DataTypeString*>(file_type.get())->len();
803
3
        }
804
805
4
        return file_len < 0 || target_len < file_len;
806
4
    }
807
808
    // Truncate a materialized CHAR/VARCHAR column in place by reusing the vectorized substring
809
    // implementation: substring(column, 1, len). Nullable columns are unwrapped before substring
810
    // execution and wrapped back with the original null map afterward, because substring operates
811
    // on the nested string payload only.
812
1
    static void _truncate_char_or_varchar_column(Block* block, size_t idx, int len) {
813
1
        DORIS_CHECK(block != nullptr);
814
1
        auto int_type = std::make_shared<DataTypeInt32>();
815
1
        const auto num_columns_without_result = cast_set<uint32_t>(block->columns());
816
1
        auto& target = block->get_by_position(idx);
817
1
        const bool is_nullable = target.type->is_nullable();
818
1
        ColumnPtr input_column = target.column;
819
1
        ColumnPtr null_map_column;
820
1
        if (is_nullable) {
821
1
            const auto* nullable_column = assert_cast<const ColumnNullable*>(target.column.get());
822
1
            input_column = nullable_column->get_nested_column_ptr();
823
1
            null_map_column = nullable_column->get_null_map_column_ptr();
824
1
        }
825
1
        block->replace_by_position(idx, std::move(input_column));
826
1
        block->insert({int_type->create_column_const(block->rows(), to_field<TYPE_INT>(1)),
827
1
                       int_type, "const 1"});
828
1
        block->insert({int_type->create_column_const(block->rows(), to_field<TYPE_INT>(len)),
829
1
                       int_type, "const len"});
830
1
        block->insert({nullptr, std::make_shared<DataTypeString>(), "result"});
831
832
1
        ColumnNumbers temp_arguments(3);
833
1
        temp_arguments[0] = cast_set<uint32_t>(idx);
834
1
        temp_arguments[1] = num_columns_without_result;
835
1
        temp_arguments[2] = num_columns_without_result + 1;
836
1
        const uint32_t result_column_id = num_columns_without_result + 2;
837
1
        SubstringUtil::substring_execute(*block, temp_arguments, result_column_id, block->rows());
838
839
1
        ColumnPtr result_column = block->get_by_position(result_column_id).column;
840
1
        if (is_nullable) {
841
1
            result_column = ColumnNullable::create(std::move(result_column), null_map_column);
842
1
        }
843
1
        block->replace_by_position(idx, std::move(result_column));
844
1
        block->erase_tail(num_columns_without_result);
845
1
    }
846
847
67
    Status _try_materialize_aggregate_pushdown_rows(Block* block, bool* pushed_down) {
848
67
        DORIS_CHECK(block != nullptr);
849
67
        DORIS_CHECK(pushed_down != nullptr);
850
67
        *pushed_down = false;
851
67
        block->clear_column_data(_projected_columns.size());
852
67
        _aggregate_pushdown_tried = true;
853
67
        if (!_supports_aggregate_pushdown(_push_down_agg_type)) {
854
58
            return Status::OK();
855
58
        }
856
857
9
        FileAggregateRequest file_request;
858
9
        RETURN_IF_ERROR(_build_file_aggregate_request(_push_down_agg_type, &file_request));
859
9
        FileAggregateResult file_result;
860
9
        const auto status = _data_reader.reader->get_aggregate_result(file_request, &file_result);
861
9
        if (status.is<ErrorCode::NOT_IMPLEMENTED_ERROR>()) {
862
1
            return Status::OK();
863
1
        }
864
8
        RETURN_IF_ERROR(status);
865
7
        RETURN_IF_ERROR(
866
7
                _materialize_aggregate_pushdown_rows(_push_down_agg_type, file_result, block));
867
7
        *pushed_down = true;
868
7
        RETURN_IF_ERROR(close_current_reader());
869
7
        return Status::OK();
870
7
    }
871
872
76
    virtual bool _supports_aggregate_pushdown(TPushAggOp::type agg_type) const {
873
        // Only COUNT and MIN/MAX can be push down.
874
76
        if (agg_type != TPushAggOp::type::COUNT && agg_type != TPushAggOp::type::MINMAX) {
875
49
            return false;
876
49
        }
877
        // Only support aggregate pushdown when there is no delete or filter, so
878
        // the reduced rows consumed by the upper aggregate remain semantically equivalent to a
879
        // normal scan.
880
27
        if ((_delete_rows != nullptr && !_delete_rows->empty()) ||
881
27
            (_deletion_vector != nullptr && !_deletion_vector->isEmpty())) {
882
3
            return false;
883
3
        }
884
24
        if (!_table_filters.empty()) {
885
3
            return false;
886
3
        }
887
21
        if (agg_type == TPushAggOp::type::COUNT) {
888
9
            return true;
889
9
        }
890
        // For MIN/MAX, only support direct file-to-table column mappings. The two emitted rows
891
        // must be enough for the upper MIN/MAX aggregate without evaluating default expressions or
892
        // virtual columns.
893
14
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
894
14
            if (!mapping.file_local_id.has_value() ||
895
14
                mapping.virtual_column_type != TableVirtualColumnType::INVALID ||
896
14
                mapping.default_expr != nullptr || mapping.file_type == nullptr ||
897
14
                mapping.table_type == nullptr) {
898
1
                return false;
899
1
            }
900
13
            if (!_can_push_down_minmax_for_mapping(mapping)) {
901
1
                return false;
902
1
            }
903
13
        }
904
10
        return true;
905
12
    }
906
907
87
    static ColumnPtr _detach_column(ColumnPtr column) {
908
87
        DORIS_CHECK(column.get() != nullptr);
909
87
        return IColumn::mutate(std::move(column));
910
87
    }
911
912
58
    static Status _align_column_nullability(ColumnPtr* column, const DataTypePtr& table_type) {
913
58
        DORIS_CHECK(column != nullptr);
914
58
        DORIS_CHECK(column->get() != nullptr);
915
58
        DORIS_CHECK(table_type != nullptr);
916
        // Must return non-const column
917
58
        *column = (*column)->convert_to_full_column_if_const();
918
58
        if (table_type->is_nullable()) {
919
23
            const auto& nested_type =
920
23
                    assert_cast<const DataTypeNullable&>(*table_type).get_nested_type();
921
23
            if (!(*column)->is_nullable()) {
922
2
                RETURN_IF_ERROR(_align_column_nullability(column, nested_type));
923
2
                *column = make_nullable(*column);
924
2
                return Status::OK();
925
2
            }
926
21
            const auto& nullable_column = assert_cast<const ColumnNullable&>(**column);
927
21
            ColumnPtr nested_column = nullable_column.get_nested_column_ptr();
928
21
            RETURN_IF_ERROR(_align_column_nullability(&nested_column, nested_type));
929
21
            *column = ColumnNullable::create(nested_column,
930
21
                                             nullable_column.get_null_map_column_ptr());
931
21
            return Status::OK();
932
21
        }
933
35
        if ((*column)->is_nullable()) {
934
0
            const auto& nullable_column = assert_cast<const ColumnNullable&>(**column);
935
0
            if (nullable_column.has_null()) {
936
0
                return Status::InternalError(
937
0
                        "Default expression produced NULL for non-nullable table column");
938
0
            }
939
0
            ColumnPtr nested_column = nullable_column.get_nested_column_ptr();
940
0
            RETURN_IF_ERROR(_align_column_nullability(&nested_column, table_type));
941
0
            *column = nested_column;
942
0
            return Status::OK();
943
0
        }
944
35
        if (const auto* array_type = typeid_cast<const DataTypeArray*>(table_type.get())) {
945
1
            const auto& array_column = assert_cast<const ColumnArray&>(**column);
946
1
            ColumnPtr nested_column = array_column.get_data_ptr();
947
1
            RETURN_IF_ERROR(
948
1
                    _align_column_nullability(&nested_column, array_type->get_nested_type()));
949
1
            *column = ColumnArray::create(nested_column, array_column.get_offsets_ptr());
950
1
            return Status::OK();
951
1
        }
952
34
        if (const auto* map_type = typeid_cast<const DataTypeMap*>(table_type.get())) {
953
0
            const auto& map_column = assert_cast<const ColumnMap&>(**column);
954
0
            ColumnPtr key_column = map_column.get_keys_ptr();
955
0
            ColumnPtr value_column = map_column.get_values_ptr();
956
0
            RETURN_IF_ERROR(_align_column_nullability(&key_column, map_type->get_key_type()));
957
0
            RETURN_IF_ERROR(_align_column_nullability(&value_column, map_type->get_value_type()));
958
0
            *column = ColumnMap::create(key_column, value_column, map_column.get_offsets_ptr());
959
0
            return Status::OK();
960
0
        }
961
34
        if (const auto* struct_type = typeid_cast<const DataTypeStruct*>(table_type.get())) {
962
5
            const auto& struct_column = assert_cast<const ColumnStruct&>(**column);
963
5
            Columns columns = struct_column.get_columns_copy();
964
5
            DORIS_CHECK(columns.size() == struct_type->get_elements().size());
965
16
            for (size_t i = 0; i < columns.size(); ++i) {
966
11
                RETURN_IF_ERROR(
967
11
                        _align_column_nullability(&columns[i], struct_type->get_element(i)));
968
11
            }
969
5
            *column = ColumnStruct::create(columns);
970
5
            return Status::OK();
971
5
        }
972
29
        return Status::OK();
973
34
    }
974
975
    static Status _execute_default_expr_without_root_type_check(
976
            const VExprContextSPtr& default_expr, const Block* block,
977
5
            ColumnWithTypeAndName* result_data) {
978
5
        DORIS_CHECK(default_expr != nullptr);
979
5
        DORIS_CHECK(block != nullptr);
980
5
        DORIS_CHECK(result_data != nullptr);
981
5
        ColumnPtr result_column;
982
5
        Status st;
983
5
        RETURN_IF_CATCH_EXCEPTION({
984
5
            st = default_expr->root()->execute_column_impl(default_expr.get(), block, nullptr,
985
5
                                                           block->rows(), result_column);
986
5
        });
987
5
        RETURN_IF_ERROR(st);
988
5
        DORIS_CHECK(result_column.get() != nullptr);
989
5
        if (result_column->size() != block->rows()) {
990
0
            return Status::InternalError(
991
0
                    "Default expr {} return column size {} not equal to expected size {}",
992
0
                    default_expr->expr_name(), result_column->size(), block->rows());
993
0
        }
994
5
        result_data->column = result_column;
995
5
        result_data->type = default_expr->execute_type(block);
996
5
        result_data->name = default_expr->expr_name();
997
5
        return Status::OK();
998
5
    }
999
1000
    Status _cast_column_to_type(ColumnPtr* column, const DataTypePtr& file_type,
1001
                                const DataTypePtr& table_type,
1002
1
                                const std::string& column_name) const {
1003
1
        DORIS_CHECK(column != nullptr);
1004
1
        DORIS_CHECK(column->get() != nullptr);
1005
1
        DORIS_CHECK(file_type != nullptr);
1006
1
        DORIS_CHECK(table_type != nullptr);
1007
1
        if (file_type->equals(*table_type)) {
1008
0
            return Status::OK();
1009
0
        }
1010
1011
1
        DataTypePtr input_type = file_type;
1012
        // Cast wrappers unwrap nullable inputs according to the declared input type, so keep the
1013
        // root nullability of the declared type aligned with the actual column shape.
1014
1
        if ((*column)->is_nullable() && !input_type->is_nullable()) {
1015
0
            input_type = make_nullable(input_type);
1016
1
        } else if (!(*column)->is_nullable() && input_type->is_nullable()) {
1017
1
            input_type = remove_nullable(input_type);
1018
1
        }
1019
1
        Block cast_block;
1020
1
        cast_block.insert({*column, input_type, column_name});
1021
1
        auto slot_ref = VSlotRef::create_shared(0, 0, -1, input_type, column_name);
1022
1
        auto cast_expr = Cast::create_shared(table_type);
1023
1
        cast_expr->add_child(std::move(slot_ref));
1024
1
        auto cast_ctx = VExprContext::create_shared(std::move(cast_expr));
1025
1
        RowDescriptor row_desc;
1026
1
        RETURN_IF_ERROR(cast_ctx->prepare(_runtime_state, row_desc));
1027
1
        RETURN_IF_ERROR(cast_ctx->open(_runtime_state));
1028
1
        ColumnPtr cast_column;
1029
1
        RETURN_IF_ERROR(cast_ctx->execute(&cast_block, cast_column));
1030
1
        *column = std::move(cast_column);
1031
1
        return Status::OK();
1032
1
    }
1033
1034
    Status _materialize_present_child_mapping_column(const ColumnMapping& mapping,
1035
                                                     const ColumnPtr& file_column,
1036
18
                                                     const size_t rows, ColumnPtr* column) {
1037
18
        DORIS_CHECK(column != nullptr);
1038
18
        DORIS_CHECK(mapping.file_type != nullptr);
1039
18
        DORIS_CHECK(mapping.table_type != nullptr);
1040
18
        *column = file_column;
1041
18
        if (!mapping.is_trivial) {
1042
5
            if (!mapping.child_mappings.empty()) {
1043
5
                RETURN_IF_ERROR(
1044
5
                        _materialize_complex_mapping_column(mapping, *column, rows, column));
1045
5
            } else {
1046
0
                RETURN_IF_ERROR(_cast_column_to_type(column, mapping.file_type, mapping.table_type,
1047
0
                                                     mapping.file_column_name));
1048
0
            }
1049
5
        }
1050
18
        RETURN_IF_ERROR(_align_column_nullability(column, mapping.table_type));
1051
18
        return Status::OK();
1052
18
    }
1053
1054
    Status _materialize_mapping_column(const ColumnMapping& mapping, Block* current_block,
1055
92
                                       const size_t rows, ColumnPtr* column) {
1056
92
        if (!mapping.is_trivial && mapping.file_local_id.has_value() &&
1057
92
            !mapping.child_mappings.empty()) {
1058
5
            DCHECK(mapping.projection != nullptr);
1059
5
            int res_id;
1060
5
            auto st = mapping.projection->execute(current_block, &res_id);
1061
5
            if (!st.ok()) {
1062
0
                return Status::InternalError(
1063
0
                        "Failed to execute complex mapping projection for table column '{}' "
1064
0
                        "(global_index={}, file_local_id={}, rows={}): {}, mapping={}",
1065
0
                        mapping.table_column_name, mapping.global_index.value(),
1066
0
                        *mapping.file_local_id, rows, st.to_string(), mapping.debug_string());
1067
0
            }
1068
5
            ColumnPtr result_column = current_block->get_by_position(res_id).column;
1069
5
            RETURN_IF_ERROR(
1070
5
                    _materialize_complex_mapping_column(mapping, result_column, rows, column));
1071
5
            return Status::OK();
1072
5
        }
1073
87
        if (mapping.projection != nullptr) {
1074
69
            int res_id;
1075
69
            auto st = mapping.projection->execute(current_block, &res_id);
1076
69
            if (!st.ok()) {
1077
0
                std::string file_local_id = "null";
1078
0
                if (mapping.file_local_id.has_value()) {
1079
0
                    file_local_id = std::to_string(*mapping.file_local_id);
1080
0
                }
1081
0
                return Status::InternalError(
1082
0
                        "Failed to execute mapping projection for table column '{}' "
1083
0
                        "(global_index={}, file_local_id={}, rows={}): {}, mapping={}",
1084
0
                        mapping.table_column_name, mapping.global_index.value(), file_local_id,
1085
0
                        rows, st.to_string(), mapping.debug_string());
1086
0
            }
1087
69
            ColumnPtr result_column = current_block->get_by_position(res_id).column;
1088
69
            *column = _detach_column(std::move(result_column));
1089
69
            return Status::OK();
1090
69
        }
1091
18
        if (mapping.default_expr != nullptr) {
1092
5
            if (current_block->rows() == rows) {
1093
0
                ColumnWithTypeAndName result;
1094
0
                RETURN_IF_ERROR(_execute_default_expr_without_root_type_check(
1095
0
                        mapping.default_expr, current_block, &result));
1096
0
                ColumnPtr result_column = result.column;
1097
0
                RETURN_IF_ERROR(_align_column_nullability(&result_column, mapping.table_type));
1098
0
                *column = _detach_column(std::move(result_column));
1099
5
            } else {
1100
5
                DORIS_CHECK(mapping.constant_index.has_value());
1101
5
                Block eval_block;
1102
5
                eval_block.insert({mapping.table_type->create_column_const_with_default_value(rows),
1103
5
                                   mapping.table_type, "__table_reader_const_rows"});
1104
5
                ColumnWithTypeAndName result;
1105
5
                RETURN_IF_ERROR(_execute_default_expr_without_root_type_check(
1106
5
                        mapping.default_expr, &eval_block, &result));
1107
5
                ColumnPtr result_column = result.column;
1108
5
                RETURN_IF_ERROR(_align_column_nullability(&result_column, mapping.table_type));
1109
5
                *column = _detach_column(std::move(result_column));
1110
5
            }
1111
5
            return Status::OK();
1112
5
        }
1113
13
        ColumnPtr result_column = mapping.table_type->create_column_const_with_default_value(rows);
1114
13
        *column = _detach_column(std::move(result_column));
1115
13
        return Status::OK();
1116
18
    }
1117
1118
    Status _materialize_complex_mapping_column(const ColumnMapping& mapping,
1119
                                               const ColumnPtr& file_column, const size_t rows,
1120
10
                                               ColumnPtr* column) {
1121
10
        DORIS_CHECK(mapping.table_type != nullptr);
1122
10
        DORIS_CHECK(file_column.get() != nullptr);
1123
10
        const auto table_type = remove_nullable(mapping.table_type);
1124
10
        switch (table_type->get_primitive_type()) {
1125
7
        case TYPE_STRUCT:
1126
7
            RETURN_IF_ERROR(_materialize_struct_mapping_column(mapping, file_column, rows, column));
1127
7
            break;
1128
7
        case TYPE_ARRAY:
1129
2
            RETURN_IF_ERROR(_materialize_array_mapping_column(mapping, file_column, rows, column));
1130
2
            break;
1131
2
        case TYPE_MAP:
1132
1
            RETURN_IF_ERROR(_materialize_map_mapping_column(mapping, file_column, rows, column));
1133
1
            break;
1134
1
        default:
1135
0
            *column = _detach_column(file_column);
1136
0
            break;
1137
10
        }
1138
10
        return Status::OK();
1139
10
    }
1140
1141
    static std::vector<const ColumnMapping*> _present_child_mappings_in_file_order(
1142
7
            const std::vector<ColumnMapping>& child_mappings) {
1143
7
        std::vector<const ColumnMapping*> result;
1144
7
        result.reserve(child_mappings.size());
1145
16
        for (const auto& child_mapping : child_mappings) {
1146
16
            if (child_mapping.file_local_id.has_value()) {
1147
11
                result.push_back(&child_mapping);
1148
11
            }
1149
16
        }
1150
7
        std::ranges::sort(result, [](const ColumnMapping* lhs, const ColumnMapping* rhs) {
1151
6
            DORIS_CHECK(lhs->file_local_id.has_value());
1152
6
            DORIS_CHECK(rhs->file_local_id.has_value());
1153
6
            return *lhs->file_local_id < *rhs->file_local_id;
1154
6
        });
1155
7
        return result;
1156
7
    }
1157
1158
    static size_t _file_child_ordinal_for_mapping(
1159
            const ColumnMapping& mapping, const ColumnMapping& child_mapping,
1160
11
            const std::vector<const ColumnMapping*>& file_ordered_children) {
1161
11
        DORIS_CHECK(child_mapping.file_local_id.has_value());
1162
11
        if (!mapping.projected_file_children.empty()) {
1163
7
            const auto child_it = std::ranges::find_if(
1164
10
                    mapping.projected_file_children, [&](const ColumnDefinition& file_child) {
1165
10
                        return file_child.file_local_id() == *child_mapping.file_local_id;
1166
10
                    });
1167
7
            DORIS_CHECK(child_it != mapping.projected_file_children.end());
1168
7
            return static_cast<size_t>(
1169
7
                    std::distance(mapping.projected_file_children.begin(), child_it));
1170
7
        }
1171
4
        const auto child_it = std::ranges::find(file_ordered_children, &child_mapping);
1172
4
        DORIS_CHECK(child_it != file_ordered_children.end());
1173
4
        return static_cast<size_t>(std::distance(file_ordered_children.begin(), child_it));
1174
11
    }
1175
1176
    static std::vector<const ColumnMapping*> _child_mappings_in_table_type_order(
1177
7
            const ColumnMapping& mapping, const DataTypeStruct& table_type) {
1178
7
        std::vector<const ColumnMapping*> result;
1179
7
        result.reserve(mapping.child_mappings.size());
1180
23
        for (size_t child_idx = 0; child_idx < table_type.get_elements().size(); ++child_idx) {
1181
16
            const auto& child_name = table_type.get_element_name(child_idx);
1182
16
            const auto child_it = std::ranges::find_if(
1183
28
                    mapping.child_mappings, [&](const ColumnMapping& child_mapping) {
1184
28
                        return child_mapping.table_column_name == child_name;
1185
28
                    });
1186
16
            DORIS_CHECK(child_it != mapping.child_mappings.end())
1187
0
                    << mapping.debug_string() << ", table_child_name=" << child_name;
1188
16
            result.push_back(&*child_it);
1189
16
        }
1190
7
        return result;
1191
7
    }
1192
1193
    static const IColumn* _nested_column_if_nullable(const ColumnPtr& column,
1194
12
                                                     const NullMap** null_map) {
1195
12
        DORIS_CHECK(column.get() != nullptr);
1196
12
        if (const auto* nullable_column = check_and_get_column<ColumnNullable>(*column)) {
1197
8
            if (null_map != nullptr) {
1198
8
                *null_map = &nullable_column->get_null_map_data();
1199
8
            }
1200
8
            return &nullable_column->get_nested_column();
1201
8
        }
1202
4
        return column.get();
1203
12
    }
1204
1205
    Status _materialize_struct_mapping_column(const ColumnMapping& mapping,
1206
                                              const ColumnPtr& file_column, const size_t rows,
1207
7
                                              ColumnPtr* column) {
1208
7
        DORIS_CHECK(mapping.table_type != nullptr);
1209
7
        const auto* table_type =
1210
7
                assert_cast<const DataTypeStruct*>(remove_nullable(mapping.table_type).get());
1211
7
        const auto full_file_column = file_column->convert_to_full_column_if_const();
1212
7
        const NullMap* parent_null_map = nullptr;
1213
7
        const auto* nested_file_column =
1214
7
                _nested_column_if_nullable(full_file_column, &parent_null_map);
1215
7
        const auto* file_struct = assert_cast<const ColumnStruct*>(nested_file_column);
1216
7
        DORIS_CHECK(table_type->get_elements().size() == mapping.child_mappings.size());
1217
1218
7
        Columns child_columns;
1219
7
        child_columns.reserve(mapping.child_mappings.size());
1220
7
        const auto file_ordered_children =
1221
7
                _present_child_mappings_in_file_order(mapping.child_mappings);
1222
7
        const auto table_ordered_children =
1223
7
                _child_mappings_in_table_type_order(mapping, *table_type);
1224
16
        for (const auto* child_mapping : table_ordered_children) {
1225
16
            DORIS_CHECK(child_mapping != nullptr);
1226
16
            if (!child_mapping->file_local_id.has_value()) {
1227
5
                child_columns.push_back(
1228
5
                        child_mapping->table_type->create_column_const_with_default_value(rows)
1229
5
                                ->convert_to_full_column_if_const());
1230
5
                continue;
1231
5
            }
1232
11
            const auto file_child_idx =
1233
11
                    _file_child_ordinal_for_mapping(mapping, *child_mapping, file_ordered_children);
1234
11
            DORIS_CHECK(file_child_idx < file_struct->get_columns().size());
1235
11
            ColumnPtr child_column = file_struct->get_column_ptr(file_child_idx);
1236
11
            RETURN_IF_ERROR(_materialize_present_child_mapping_column(*child_mapping, child_column,
1237
11
                                                                      rows, &child_column));
1238
11
            child_columns.push_back(std::move(child_column));
1239
11
        }
1240
7
        MutableColumns mutable_child_columns;
1241
7
        mutable_child_columns.reserve(child_columns.size());
1242
16
        for (auto& child_column : child_columns) {
1243
16
            mutable_child_columns.push_back(IColumn::mutate(std::move(child_column)));
1244
16
        }
1245
7
        auto result = ColumnStruct::create(std::move(mutable_child_columns));
1246
7
        if (mapping.table_type->is_nullable()) {
1247
5
            auto null_map = ColumnUInt8::create();
1248
5
            auto& null_map_data = null_map->get_data();
1249
5
            null_map_data.resize(rows);
1250
5
            if (parent_null_map != nullptr) {
1251
5
                DORIS_CHECK(parent_null_map->size() == rows);
1252
5
                null_map_data.assign(parent_null_map->begin(), parent_null_map->end());
1253
5
            } else {
1254
0
                std::fill(null_map_data.begin(), null_map_data.end(), 0);
1255
0
            }
1256
5
            *column = ColumnNullable::create(std::move(result), std::move(null_map));
1257
5
        } else {
1258
2
            *column = std::move(result);
1259
2
        }
1260
7
        return Status::OK();
1261
7
    }
1262
1263
    Status _materialize_array_mapping_column(const ColumnMapping& mapping,
1264
                                             const ColumnPtr& file_column, const size_t rows,
1265
2
                                             ColumnPtr* column) {
1266
2
        DORIS_CHECK(mapping.child_mappings.size() == 1);
1267
2
        const auto full_file_column = file_column->convert_to_full_column_if_const();
1268
2
        const NullMap* parent_null_map = nullptr;
1269
2
        const auto* nested_file_column =
1270
2
                _nested_column_if_nullable(full_file_column, &parent_null_map);
1271
2
        const auto* file_array = assert_cast<const ColumnArray*>(nested_file_column);
1272
2
        ColumnPtr nested_column = file_array->get_data_ptr();
1273
2
        const auto& element_mapping = mapping.child_mappings[0];
1274
2
        RETURN_IF_ERROR(_materialize_present_child_mapping_column(
1275
2
                element_mapping, nested_column, nested_column->size(), &nested_column));
1276
2
        auto offsets_column = file_array->get_offsets_ptr()->convert_to_full_column_if_const();
1277
2
        auto result = ColumnArray::create(IColumn::mutate(std::move(nested_column)),
1278
2
                                          IColumn::mutate(std::move(offsets_column)));
1279
2
        if (mapping.table_type->is_nullable()) {
1280
2
            auto null_map = ColumnUInt8::create();
1281
2
            auto& null_map_data = null_map->get_data();
1282
2
            null_map_data.resize(rows);
1283
2
            if (parent_null_map != nullptr) {
1284
2
                DORIS_CHECK(parent_null_map->size() == rows);
1285
2
                null_map_data.assign(parent_null_map->begin(), parent_null_map->end());
1286
2
            } else {
1287
0
                std::fill(null_map_data.begin(), null_map_data.end(), 0);
1288
0
            }
1289
2
            *column = ColumnNullable::create(std::move(result), std::move(null_map));
1290
2
        } else {
1291
0
            *column = std::move(result);
1292
0
        }
1293
2
        return Status::OK();
1294
2
    }
1295
1296
    Status _materialize_map_mapping_column(const ColumnMapping& mapping,
1297
                                           const ColumnPtr& file_column, const size_t rows,
1298
3
                                           ColumnPtr* column) {
1299
3
        const auto full_file_column = file_column->convert_to_full_column_if_const();
1300
3
        const NullMap* parent_null_map = nullptr;
1301
3
        const auto* nested_file_column =
1302
3
                _nested_column_if_nullable(full_file_column, &parent_null_map);
1303
3
        const auto* file_map = assert_cast<const ColumnMap*>(nested_file_column);
1304
3
        ColumnPtr key_column = file_map->get_keys_ptr();
1305
3
        ColumnPtr value_column = file_map->get_values_ptr();
1306
1307
3
        const ColumnMapping* key_mapping = nullptr;
1308
3
        const ColumnMapping* value_mapping = nullptr;
1309
5
        for (const auto& child_mapping : mapping.child_mappings) {
1310
5
            if (!child_mapping.file_local_id.has_value()) {
1311
0
                continue;
1312
0
            }
1313
5
            if (*child_mapping.file_local_id == 0) {
1314
2
                key_mapping = &child_mapping;
1315
3
            } else if (*child_mapping.file_local_id == 1) {
1316
3
                value_mapping = &child_mapping;
1317
3
            }
1318
5
        }
1319
1320
3
        if (key_mapping != nullptr) {
1321
2
            RETURN_IF_ERROR(_materialize_present_child_mapping_column(
1322
2
                    *key_mapping, key_column, key_column->size(), &key_column));
1323
2
        }
1324
3
        if (value_mapping != nullptr) {
1325
3
            RETURN_IF_ERROR(_materialize_present_child_mapping_column(
1326
3
                    *value_mapping, value_column, value_column->size(), &value_column));
1327
3
        }
1328
3
        auto offsets_column = file_map->get_offsets_ptr()->convert_to_full_column_if_const();
1329
3
        auto result = ColumnMap::create(IColumn::mutate(std::move(key_column)),
1330
3
                                        IColumn::mutate(std::move(value_column)),
1331
3
                                        IColumn::mutate(std::move(offsets_column)));
1332
3
        if (mapping.table_type->is_nullable()) {
1333
1
            auto null_map = ColumnUInt8::create();
1334
1
            auto& null_map_data = null_map->get_data();
1335
1
            null_map_data.resize(rows);
1336
1
            if (parent_null_map != nullptr) {
1337
1
                DORIS_CHECK(parent_null_map->size() == rows);
1338
1
                null_map_data.assign(parent_null_map->begin(), parent_null_map->end());
1339
1
            } else {
1340
0
                std::fill(null_map_data.begin(), null_map_data.end(), 0);
1341
0
            }
1342
1
            *column = ColumnNullable::create(std::move(result), std::move(null_map));
1343
2
        } else {
1344
2
            *column = std::move(result);
1345
2
        }
1346
3
        return Status::OK();
1347
3
    }
1348
1349
67
    Status _open_mapping_exprs() {
1350
67
        RowDescriptor row_desc;
1351
98
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
1352
98
            if (mapping.projection != nullptr) {
1353
80
                RETURN_IF_ERROR(mapping.projection->prepare(_runtime_state, row_desc));
1354
80
                RETURN_IF_ERROR(mapping.projection->open(_runtime_state));
1355
80
            }
1356
98
            if (mapping.default_expr != nullptr) {
1357
5
                RETURN_IF_ERROR(mapping.default_expr->prepare(_runtime_state, row_desc));
1358
5
                RETURN_IF_ERROR(mapping.default_expr->open(_runtime_state));
1359
5
            }
1360
98
        }
1361
67
        return Status::OK();
1362
67
    }
1363
1364
    Status _build_file_aggregate_request(TPushAggOp::type agg_type,
1365
9
                                         FileAggregateRequest* request) const {
1366
9
        DORIS_CHECK(request != nullptr);
1367
9
        DORIS_CHECK(_supports_aggregate_pushdown(agg_type));
1368
9
        request->agg_type = agg_type;
1369
9
        request->columns.clear();
1370
9
        if (agg_type == TPushAggOp::type::COUNT) {
1371
            // COUNT pushdown historically meant COUNT(*) and therefore carried no columns. For
1372
            // complex COUNT(col), materializing the full MAP/LIST/STRUCT value only to test the
1373
            // top-level NULL bit can be extremely expensive. When the scan projects exactly one
1374
            // directly-mapped complex column, pass that file column to the reader so formats such
1375
            // as Parquet can count the column shape from metadata/levels without decoding payload
1376
            // values like MAP value strings. Other COUNT cases stay on the existing row-count path
1377
            // to avoid changing count(*) semantics.
1378
4
            if (_data_reader.column_mapper->mappings().size() == 1) {
1379
4
                const auto& mapping = _data_reader.column_mapper->mappings()[0];
1380
4
                if (mapping.file_local_id.has_value() && mapping.file_type != nullptr &&
1381
4
                    is_complex_type(remove_nullable(mapping.file_type)->get_primitive_type()) &&
1382
4
                    mapping.virtual_column_type == TableVirtualColumnType::INVALID &&
1383
4
                    mapping.default_expr == nullptr) {
1384
0
                    FileAggregateRequest::Column column;
1385
0
                    column.projection =
1386
0
                            LocalColumnIndex::top_level(LocalColumnId(*mapping.file_local_id));
1387
0
                    request->columns.push_back(std::move(column));
1388
0
                }
1389
4
            }
1390
4
            return Status::OK();
1391
4
        }
1392
5
        request->columns.reserve(_data_reader.column_mapper->mappings().size());
1393
6
        for (const auto& mapping : _data_reader.column_mapper->mappings()) {
1394
6
            DORIS_CHECK(mapping.file_local_id.has_value());
1395
6
            FileAggregateRequest::Column column;
1396
6
            column.projection = LocalColumnIndex::top_level(LocalColumnId(*mapping.file_local_id));
1397
6
            if (!mapping.child_mappings.empty()) {
1398
1
                RETURN_IF_ERROR(build_aggregate_projection(mapping, &column.projection));
1399
1
            }
1400
6
            request->columns.push_back(std::move(column));
1401
6
        }
1402
5
        return Status::OK();
1403
5
    }
1404
1405
    Status _materialize_aggregate_pushdown_rows(TPushAggOp::type agg_type,
1406
                                                const FileAggregateResult& file_result,
1407
7
                                                Block* block) {
1408
7
        if (agg_type == TPushAggOp::type::COUNT) {
1409
            // COUNT pushdown is not a final count value. It emits `count` default rows so the
1410
            // upper COUNT(*) aggregate can count them and produce the final result, including
1411
            // zero rows when count is 0.
1412
3
            DORIS_CHECK(file_result.count >= 0);
1413
3
            return _materialize_count_rows(cast_set<size_t>(file_result.count), block);
1414
3
        }
1415
        // MIN/MAX pushdown emits two rows, min first and max second, for each projected column.
1416
        // The upper MIN/MAX aggregate consumes those two rows to produce the final aggregate value.
1417
4
        DORIS_CHECK(file_result.columns.size() == _data_reader.column_mapper->mappings().size());
1418
4
        DORIS_CHECK(block->columns() == _data_reader.column_mapper->mappings().size());
1419
4
        Block file_block;
1420
4
        file_block.reserve(_data_reader.file_block_layout.size());
1421
5
        for (const auto& column : _data_reader.file_block_layout) {
1422
5
            file_block.insert({column.type->create_column(), column.type, column.name});
1423
5
        }
1424
9
        for (size_t column_idx = 0; column_idx < file_result.columns.size(); ++column_idx) {
1425
5
            const auto& result_column = file_result.columns[column_idx];
1426
5
            if (!result_column.has_min || !result_column.has_max) {
1427
0
                return Status::NotSupported("Missing min/max aggregate result for column {}",
1428
0
                                            _projected_columns[column_idx].name);
1429
0
            }
1430
5
            bool found_file_column = false;
1431
6
            for (size_t block_position = 0; block_position < _data_reader.file_block_layout.size();
1432
6
                 ++block_position) {
1433
6
                if (_data_reader.file_block_layout[block_position].file_column_id ==
1434
6
                    file_result.columns[column_idx].projection.column_id()) {
1435
5
                    found_file_column = true;
1436
5
                    auto column = file_block.get_by_position(block_position)
1437
5
                                          .type->create_column()
1438
5
                                          ->assert_mutable();
1439
5
                    RETURN_IF_ERROR(_insert_aggregate_projection_value(
1440
5
                            file_result.columns[column_idx].projection, result_column.min_value,
1441
5
                            column.get()));
1442
5
                    RETURN_IF_ERROR(_insert_aggregate_projection_value(
1443
5
                            file_result.columns[column_idx].projection, result_column.max_value,
1444
5
                            column.get()));
1445
5
                    file_block.replace_by_position(block_position, std::move(column));
1446
5
                    break;
1447
5
                }
1448
6
            }
1449
5
            DORIS_CHECK(found_file_column);
1450
5
        }
1451
9
        for (size_t column_idx = 0; column_idx < _data_reader.column_mapper->mappings().size();
1452
5
             ++column_idx) {
1453
5
            ColumnPtr table_column;
1454
5
            RETURN_IF_ERROR(
1455
5
                    _materialize_mapping_column(_data_reader.column_mapper->mappings()[column_idx],
1456
5
                                                &file_block, 2, &table_column));
1457
5
            block->replace_by_position(column_idx, std::move(table_column));
1458
5
        }
1459
4
        return Status::OK();
1460
4
    }
1461
1462
    struct FileBlockColumn {
1463
        LocalColumnId file_column_id = LocalColumnId::invalid();
1464
        std::string name;
1465
        DataTypePtr type;
1466
    };
1467
1468
    struct DataReader {
1469
        std::unique_ptr<FileReader> reader;
1470
        std::unique_ptr<TableColumnMapper> column_mapper;
1471
        // Schema of the data file, also including virtual column (row position).
1472
        std::vector<ColumnDefinition> file_schema;
1473
        // Layout of the block returned by file reader, determined by column mapping and file
1474
        // schema. It is used for file reader to materialize columns into correct type and position.
1475
        std::vector<FileBlockColumn> file_block_layout;
1476
        Block block_template;
1477
    };
1478
    DataReader _data_reader;
1479
    std::vector<ColumnDefinition> _projected_columns;
1480
    std::unique_ptr<ScanTask> _current_task;
1481
    std::optional<io::FileDescription> _current_file_description;
1482
    // Range-level compression has higher priority than scan-param compression. TVF/load can keep
1483
    // the logical format as CSV/TEXT while carrying the concrete compression such as GZ or LZO on
1484
    // each TFileRangeDesc, matching the old FileScanner reader contract.
1485
    TFileCompressType::type _current_range_compress_type = TFileCompressType::UNKNOWN;
1486
    std::optional<TUniqueId> _current_range_load_id;
1487
    TFileRangeDesc _current_file_range_desc;
1488
    std::shared_ptr<io::FileSystemProperties> _system_properties;
1489
    // partition key -> value
1490
    std::map<std::string, Field> _partition_values;
1491
    // Predicates built from scan conjuncts before file-level localization.
1492
    std::vector<TableFilter> _table_filters;
1493
    VExprContextSPtrs _conjuncts;
1494
    ReadProfile _profile;
1495
    // Parsed from row-position based delete files, including position delete and deletion vector.
1496
    DeleteRows* _delete_rows = nullptr;
1497
    DeletionVector* _deletion_vector = nullptr;
1498
    TFileScanRangeParams* _scan_params;
1499
    std::shared_ptr<io::IOContext> _io_ctx;
1500
    RuntimeState* _runtime_state;
1501
    RuntimeProfile* _scanner_profile;
1502
    const std::vector<SlotDescriptor*>* _file_slot_descs = nullptr;
1503
    FileFormat _format;
1504
    TPushAggOp::type _push_down_agg_type = TPushAggOp::type::NONE;
1505
    size_t _batch_size = 0;
1506
    uint64_t _condition_cache_digest = 0;
1507
    segment_v2::ConditionCache::ExternalCacheKey _condition_cache_key;
1508
    std::shared_ptr<std::vector<bool>> _condition_cache;
1509
    std::shared_ptr<ConditionCacheContext> _condition_cache_ctx;
1510
    int64_t _condition_cache_hit_count = 0;
1511
    bool _current_reader_reached_eof = false;
1512
    int64_t _remaining_table_level_count = -1;
1513
    std::optional<GlobalRowIdContext> _global_rowid_context;
1514
    bool _aggregate_pushdown_tried = false;
1515
    TableColumnMapperOptions _mapper_options;
1516
1517
private:
1518
    static const ColumnDefinition* _find_column_definition(
1519
117
            const std::vector<ColumnDefinition>& schema, LocalColumnId column_id) {
1520
232
        for (const auto& field : schema) {
1521
232
            if (field.file_local_id() == column_id.value()) {
1522
99
                return &field;
1523
99
            }
1524
232
        }
1525
18
        return nullptr;
1526
117
    }
1527
1528
15
    static bool _can_push_down_minmax_for_mapping(const ColumnMapping& mapping) {
1529
15
        if (mapping.child_mappings.empty()) {
1530
12
            return true;
1531
12
        }
1532
3
        const auto primitive_type = remove_nullable(mapping.file_type)->get_primitive_type();
1533
3
        if (primitive_type != TYPE_STRUCT) {
1534
1
            return false;
1535
1
        }
1536
2
        size_t mapped_children = 0;
1537
2
        const ColumnMapping* mapped_child = nullptr;
1538
2
        for (const auto& child_mapping : mapping.child_mappings) {
1539
2
            if (!child_mapping.file_local_id.has_value()) {
1540
0
                continue;
1541
0
            }
1542
2
            ++mapped_children;
1543
2
            mapped_child = &child_mapping;
1544
2
        }
1545
2
        return mapped_children == 1 && mapped_child != nullptr &&
1546
2
               _can_push_down_minmax_for_mapping(*mapped_child);
1547
3
    }
1548
1549
    static Status build_aggregate_projection(const ColumnMapping& mapping,
1550
2
                                             LocalColumnIndex* projection) {
1551
2
        DORIS_CHECK(projection != nullptr);
1552
2
        DORIS_CHECK(mapping.file_local_id.has_value());
1553
2
        *projection = LocalColumnIndex::local(*mapping.file_local_id);
1554
2
        projection->children.clear();
1555
2
        projection->project_all_children = true;
1556
2
        if (mapping.child_mappings.empty()) {
1557
1
            return Status::OK();
1558
1
        }
1559
1
        projection->project_all_children = false;
1560
1
        for (const auto& child_mapping : mapping.child_mappings) {
1561
1
            if (!child_mapping.file_local_id.has_value()) {
1562
0
                continue;
1563
0
            }
1564
1
            LocalColumnIndex child_projection;
1565
1
            RETURN_IF_ERROR(build_aggregate_projection(child_mapping, &child_projection));
1566
1
            projection->children.push_back(std::move(child_projection));
1567
1
        }
1568
1
        DORIS_CHECK(projection->children.size() == 1);
1569
1
        return Status::OK();
1570
1
    }
1571
1572
    static Status _insert_aggregate_projection_value(const LocalColumnIndex& projection,
1573
24
                                                     const Field& value, IColumn* column) {
1574
24
        DORIS_CHECK(column != nullptr);
1575
24
        if (auto* nullable_column = check_and_get_column<ColumnNullable>(*column)) {
1576
12
            RETURN_IF_ERROR(_insert_aggregate_projection_value(
1577
12
                    projection, value, &nullable_column->get_nested_column()));
1578
12
            nullable_column->get_null_map_data().push_back(0);
1579
12
            return Status::OK();
1580
12
        }
1581
12
        if (projection.project_all_children || projection.children.empty()) {
1582
10
            column->insert(value);
1583
10
            return Status::OK();
1584
10
        }
1585
2
        auto* struct_column = assert_cast<ColumnStruct*>(column);
1586
2
        DORIS_CHECK(projection.children.size() == 1);
1587
2
        const auto& child_projection = projection.children[0];
1588
2
        DORIS_CHECK(struct_column->get_columns().size() == 1);
1589
2
        RETURN_IF_ERROR(_insert_aggregate_projection_value(child_projection, value,
1590
2
                                                           &struct_column->get_column(0)));
1591
2
        return Status::OK();
1592
2
    }
1593
1594
    // Parse a DV into its compressed bitmap. Position delete files continue to use _delete_rows.
1595
    Status _parse_delete_predicates(const SplitReadOptions& options);
1596
};
1597
1598
} // namespace doris::format