Coverage Report

Created: 2026-07-07 16:13

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