Coverage Report

Created: 2026-08-06 13:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format_v2/column_mapper.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 <cstddef>
21
#include <cstdint>
22
#include <memory>
23
#include <optional>
24
#include <string>
25
#include <utility>
26
#include <vector>
27
28
#include "common/status.h"
29
#include "core/data_type/data_type.h"
30
#include "core/field.h"
31
#include "exprs/vexpr_fwd.h"
32
#include "format_v2/file_reader.h"
33
34
namespace doris {
35
class RuntimeState;
36
} // namespace doris
37
38
namespace doris::format {
39
40
struct ColumnDefinition;
41
struct TableFilter;
42
43
enum class TableColumnMappingMode {
44
    // Match by ColumnDefinition::identifier TYPE_INT as field id.
45
    BY_FIELD_ID,
46
    // Match by ColumnDefinition::identifier TYPE_STRING, or logical name when identifier is null.
47
    BY_NAME,
48
    // Match top-level columns by file position. This mainly serves Hive1 ORC style files whose
49
    // column names are placeholder values such as `_col0` / `_col1`, where position is the only
50
    // reliable way to select the correct column.
51
    BY_INDEX,
52
};
53
54
enum TableVirtualColumnType {
55
    INVALID = 0, // not a virtual column
56
    // Iceberg v3 row lineage metadata column `_row_id`. Physical non-null values
57
    // are preserved; NULL or missing values inherit first_row_id + row_position.
58
    ROW_ID = 1,
59
    // Iceberg v3 row lineage metadata column `_last_updated_sequence_number`.
60
    // Physical non-null values are preserved; NULL or missing values inherit the
61
    // data file's last_updated_sequence_number.
62
    LAST_UPDATED_SEQUENCE_NUMBER = 2,
63
    // Doris internal Iceberg row locator column `__DORIS_ICEBERG_ROWID_COL__`.
64
    // It is a struct used by delete/update/merge, not the Iceberg `_row_id`.
65
    ICEBERG_ROWID = 3,
66
};
67
68
enum class FilterConversionType {
69
    COPY_DIRECTLY, // filter can be copied directly from file layer without any change, e.g. column type and table type are the same and no complex nested projection is involved.
70
    CAST_FILTER, // filter can be converted from file layer by adding a cast, e.g. column type is nullable but table type is not, or file column has a trivial nested projection but table column has a complex nested projection.
71
    READER_EXPRESSION,
72
    FINALIZE_ONLY, // filter cannot be converted to file layer and should be evaluated at table reader finalize phase, e.g. predicates on ICEBERG_ROW_ID column which is generated by IcebergReader.
73
    CONSTANT,
74
};
75
76
// Nested global-to-local child mapping. The root index points either to a request-local slot or to
77
// a child id, depending on the owner. child_mapping keeps the recursive table-child to file-child
78
// relationship explicit instead of encoding it in ColumnMapping flags.
79
struct IndexMapping {
80
    int32_t index = -1;
81
    std::map<int32_t, std::shared_ptr<IndexMapping>> child_mapping;
82
};
83
84
// Recursive result produced after one table/global column is assigned to a file-local source.
85
struct ColumnMapResult {
86
    std::optional<LocalColumnId> local_column_id;
87
    std::optional<LocalColumnIndex> column_index;
88
    std::optional<IndexMapping> mapping;
89
};
90
91
// Final mapping entry from one global result column to one file-local source.
92
struct ColumnMapEntry {
93
    IndexMapping mapping;
94
    DataTypePtr local_type;
95
    DataTypePtr global_type;
96
    FilterConversionType filter_conversion = FilterConversionType::FINALIZE_ONLY;
97
};
98
99
// Collection of final result-column mappings produced for one file/split.
100
struct ResultColumnMapping {
101
    std::map<GlobalIndex, ColumnMapEntry> global_to_local;
102
};
103
104
// Mapping result from one table column to one file column.
105
// This is the main boundary object between table-level schema semantics and file-local schema
106
// semantics.
107
struct ColumnMapping {
108
    // Position of the top-level projected column in the table/global output block. Table-level
109
    // filters and column predicates refer to this index after FileScannerV2 translates FE ids at
110
    // the scanner boundary.
111
    GlobalIndex global_index;
112
    std::string table_column_name;
113
    // File-reader local id for the mapped node.
114
    //
115
    // For a root mapping it is convertible to LocalColumnId. For a nested mapping it is the
116
    // LocalColumnIndex child id under the parent projection. This is deliberately separated from
117
    // ColumnDefinition::identifier, which is the table-to-file matching key such as Parquet/Iceberg
118
    // field_id or column name.
119
    //
120
    // Empty means the table column is constant, missing, partition-only, or virtual.
121
    std::optional<int32_t> file_local_id;
122
    std::string file_column_name;
123
    // Full file type/children before nested projection pruning. Used to rebuild projected types
124
    // and to localize nested filters that reference children not present in the output projection.
125
    DataTypePtr original_file_type;
126
    std::vector<ColumnDefinition> original_file_children;
127
    // File children after applying the scan projection. The order follows the file-local semantic
128
    // schema, not table child order. TableReader uses this to map table-output children back to the
129
    // file-local block layout when projection, predicate-only children, and schema evolution mix.
130
    std::vector<ColumnDefinition> projected_file_children;
131
    std::optional<bool> timestamp_is_adjusted_to_utc = std::nullopt;
132
    // Table-side Variant object-key paths retained until the physical shredding schema is known.
133
    std::vector<std::vector<std::string>> variant_access_paths;
134
    // Split/file-local constant entry when this mapping is produced from partition/default/virtual
135
    // expression instead of physical file data.
136
    std::optional<ConstantIndex> constant_index;
137
    // Effective file type after applying casts/remaps/nested projection pruning.
138
    DataTypePtr file_type;
139
    // Target table/global type after final materialization.
140
    DataTypePtr table_type;
141
142
    // Final projection expression used to convert file-local values into table/global values, such
143
    // as casts, defaults, partition values, generated columns, or complex-column remaps.
144
    VExprContextSPtr projection;
145
146
    // Mapping tree for nested table children. The order follows table output children, while file
147
    // children can be pruned/reordered through each child mapping's file-reader local id.
148
    std::vector<ColumnMapping> child_mappings;
149
    // True when file value can be used directly as table value without cast or child remap.
150
    bool is_trivial = false;
151
    // How filters referencing this table/global column can be converted below table-reader
152
    // finalize. This is metadata for localize_filters() and future constant-filter evaluation.
153
    FilterConversionType filter_conversion = FilterConversionType::FINALIZE_ONLY;
154
    TableVirtualColumnType virtual_column_type = TableVirtualColumnType::INVALID;
155
    VExprContextSPtr default_expr;
156
157
    std::string debug_string() const;
158
};
159
160
struct TableColumnMapperOptions {
161
    TableColumnMappingMode mode = TableColumnMappingMode::BY_FIELD_ID;
162
    // Iceberg requires a missing required field to fail unless an initial default is present.
163
    // Other table formats keep the existing missing-column behavior unless they opt in.
164
    bool reject_missing_required_field = false;
165
    bool allow_idless_complex_wrapper_projection = false;
166
    bool enable_row_lineage_virtual_columns = false;
167
168
    std::string debug_string() const;
169
};
170
171
Status clone_table_expr_tree(const VExprSPtr& expr, VExprSPtr* cloned_expr);
172
const Field* find_partition_value(const ColumnDefinition& table_column,
173
                                  const std::map<std::string, Field>& partition_values);
174
// Apply the same case-insensitive logical name, string identifier, and bidirectional alias rules
175
// used by TableColumnMapper's BY_NAME mode.
176
const ColumnDefinition* find_column_by_name(const ColumnDefinition& table_column,
177
                                            const std::vector<ColumnDefinition>& file_schema);
178
// Apply BY_FIELD_ID matching and, when requested, retain a unique ID-less complex wrapper that
179
// contains a descendant selected by Iceberg field ID.
180
const ColumnDefinition* find_column_by_field_id(const ColumnDefinition& table_column,
181
                                                const std::vector<ColumnDefinition>& file_schema,
182
                                                bool allow_idless_complex_wrapper_projection);
183
184
// Generic mapping layer from table schema to file schema.
185
// Iceberg uses BY_FIELD_ID. Plain by-name formats can reuse this component as well, so keep this
186
// abstraction table-format neutral instead of making it Iceberg-only.
187
class TableColumnMapper {
188
public:
189
    explicit TableColumnMapper(TableColumnMapperOptions options = {})
190
122k
            : _options(std::move(options)) {}
191
122k
    virtual ~TableColumnMapper() = default;
192
193
    // Build column mappings from table schema to file schema.
194
    // The resulting ColumnMapping describes how each table column is produced from a file column,
195
    // a constant, or an expression. Later projection, filter localization, and table-block
196
    // finalization should all reuse the same mapping.
197
    virtual Status create_mapping(const std::vector<ColumnDefinition>& projected_columns,
198
                                  const std::map<std::string, Field>& partition_values,
199
                                  const std::vector<ColumnDefinition>& file_schema);
200
201
    // Convert a table-level scan request into a file-local scan request. table_filters preserve
202
    // row-level filtering semantics and are rewritten as file-local conjuncts. File-layer pruning
203
    // such as ZoneMap, dictionary, and bloom filter derives from those localized VExpr conjuncts.
204
    virtual Status create_scan_request(
205
            const std::vector<TableFilter>& table_filters,
206
            const std::vector<ColumnDefinition>& projected_columns, FileScanRequest* file_request,
207
            RuntimeState* runtime_state = nullptr,
208
            const std::map<LocalColumnId, LocalIndex>* fixed_local_positions = nullptr,
209
            const std::map<LocalColumnId, LocalIndex>* fixed_non_predicate_positions = nullptr);
210
211
    // Localize table-level filters to the file schema.
212
    // Trivial mappings can copy structured predicates directly. Type changes may be localized with
213
    // a safe cast. Expressions that cannot be pushed down safely should be handled by the
214
    // table-level finalize/filter fallback.
215
    virtual Status localize_filters(const std::vector<TableFilter>& table_filters,
216
                                    FileScanRequest* file_request,
217
                                    RuntimeState* runtime_state = nullptr);
218
245k
    void clear() {
219
245k
        _mappings.clear();
220
245k
        _predicate_mappings.clear();
221
245k
        _hidden_mappings.clear();
222
245k
        _constant_map.clear();
223
245k
        _filter_entries.clear();
224
245k
        _file_schema.clear();
225
245k
        _partition_values.clear();
226
245k
    }
227
1.47M
    const std::vector<ColumnMapping>& mappings() const { return _mappings; }
228
65.4k
    const std::map<GlobalIndex, FilterEntry>& filter_entries() const { return _filter_entries; }
229
3.86k
    const ConstantMap& constant_map() const { return _constant_map; }
230
    std::string debug_string() const;
231
232
protected:
233
    // Columnar readers such as Parquet can read predicate columns first, evaluate row filters, and
234
    // lazily read the rest. Row-oriented readers such as CSV/Text materialize one row at a time and
235
    // should keep all required columns in one scan list.
236
138k
    virtual bool enable_lazy_materialization() const { return true; }
237
    // Row-oriented readers such as CSV/Text cannot physically read only a nested child from one
238
    // delimited text field. They must scan the whole complex top-level field and let TableReader
239
    // rematerialize the requested table child after row-level filters have run.
240
323k
    virtual bool force_full_complex_scan_projection() const { return false; }
241
    // Only Parquet currently has a Variant physical shredding schema and a reader that can
242
    // validate residual-value completeness before honoring a typed-leaf projection.
243
292k
    virtual bool enable_variant_leaf_projection() const { return false; }
244
    // Parquet can keep two independent readers/cursors for the same complex root: a narrow eager
245
    // predicate subtree and the final output subtree materialized only for surviving rows.
246
324k
    virtual bool enable_independent_predicate_projection() const { return false; }
247
248
    const ColumnDefinition* _find_file_field(
249
            const ColumnDefinition& table_column,
250
            const std::vector<ColumnDefinition>& file_schema) const;
251
    Status _create_direct_mapping(const ColumnDefinition& table_column,
252
                                  const ColumnDefinition& file_field, ColumnMapping* mapping) const;
253
254
    Status _create_by_index_mapping(const ColumnDefinition& table_column,
255
                                    const std::vector<ColumnDefinition>& file_schema,
256
                                    ColumnMapping* mapping);
257
    Status _build_filter_entries(const FileScanRequest& file_request);
258
    Status _build_result_column_mapping(const FileScanRequest& file_request);
259
260
    void _set_constant_mapping(ColumnMapping* mapping, VExprContextSPtr expr);
261
    Status _create_mapping_for_column(const ColumnDefinition& table_column,
262
                                      GlobalIndex global_index, ColumnMapping* mapping);
263
    Status _create_hidden_filter_mapping(const ColumnDefinition& table_column,
264
                                         GlobalIndex global_index, ColumnMapping* mapping);
265
    Status _build_hidden_filter_mappings(const std::vector<TableFilter>& table_filters);
266
    std::vector<ColumnMapping> _filter_visible_mappings() const;
267
268
    ColumnMapping* _find_mapping(GlobalIndex global_index);
269
    ColumnMapping* _find_predicate_mapping(GlobalIndex global_index);
270
    ColumnMapping* _find_filter_mapping(GlobalIndex global_index);
271
272
    TableColumnMapperOptions _options;
273
    // Column mapping for each projected column, in the same order as projected_columns. Each entry
274
    // describes how to get one table/global column from file-local sources, and carries metadata
275
    // for filter localization and result finalize.
276
    std::vector<ColumnMapping> _mappings;
277
    // Optional mappings built from SlotDescriptor::predicate_access_paths. They deliberately keep
278
    // a different file type/projection shape from the final output mappings above.
279
    std::vector<ColumnMapping> _predicate_mappings;
280
    // Predicate-only top-level columns are not output projection columns, so keep their mappings
281
    // here. They are visible only to filter localization and file-reader predicate construction.
282
    std::vector<ColumnMapping> _hidden_mappings;
283
    std::map<GlobalIndex, FilterEntry> _filter_entries;
284
    ConstantMap _constant_map;
285
    // Split-local schema state retained from create_mapping() so create_scan_request() can build
286
    // hidden mappings for top-level filter slots that are absent from projected_columns.
287
    std::vector<ColumnDefinition> _file_schema;
288
    std::map<std::string, Field> _partition_values;
289
};
290
291
// Parquet consumes the full FileScanRequest shape: predicate columns for lazy materialization and
292
// file-local conjuncts for ZoneMap, dictionary, and bloom-filter pruning.
293
class ParquetColumnMapper final : public TableColumnMapper {
294
public:
295
    using TableColumnMapper::TableColumnMapper;
296
297
protected:
298
223k
    bool enable_variant_leaf_projection() const override { return true; }
299
280k
    bool enable_independent_predicate_projection() const override { return true; }
300
};
301
302
// Mapper for readers that always materialize every required file column before filtering. The
303
// table-to-file schema mapping is still generic, but the FileScanRequest layout is simpler:
304
// predicate_columns are not populated.
305
class MaterializedColumnMapper final : public TableColumnMapper {
306
public:
307
    using TableColumnMapper::TableColumnMapper;
308
309
protected:
310
548
    bool enable_lazy_materialization() const override { return false; }
311
192k
    bool force_full_complex_scan_projection() const override { return true; }
312
};
313
314
} // namespace doris::format