Coverage Report

Created: 2026-07-14 19:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format_v2/column_data.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 <algorithm>
21
#include <cstddef>
22
#include <cstdint>
23
#include <memory>
24
#include <ostream>
25
#include <string>
26
#include <utility>
27
#include <vector>
28
29
#include "common/consts.h"
30
#include "common/status.h"
31
#include "core/data_type/data_type.h"
32
#include "core/data_type/data_type_number.h"
33
#include "core/data_type/data_type_string.h"
34
#include "core/field.h"
35
#include "exprs/vexpr_fwd.h"
36
37
namespace doris::format {
38
39
// File-local top-level column id.
40
//
41
// Scope:
42
// - Only valid inside one physical file schema returned by FileReader::get_schema().
43
// - For Parquet, this is the top-level field ordinal in the new reader schema.
44
// - The synthetic row-position column also uses this type, with a reserved negative id.
45
//
46
// Do not use this for table/global column unique ids, block positions, nested child ids, or
47
// slot ids. Nested child ids are carried by LocalColumnIndex::index below.
48
class LocalColumnId {
49
public:
50
581
    constexpr LocalColumnId() = default;
51
7.37k
    explicit constexpr LocalColumnId(int32_t id) : _id(id) {}
52
53
581
    static constexpr LocalColumnId invalid() { return LocalColumnId(); }
54
55
2.56k
    constexpr int32_t value() const { return _id; }
56
630
    constexpr bool is_valid() const { return _id >= 0; }
57
58
4.16k
    constexpr bool operator==(const LocalColumnId& other) const { return _id == other._id; }
59
257
    constexpr bool operator!=(const LocalColumnId& other) const { return !(*this == other); }
60
6.43k
    constexpr bool operator<(const LocalColumnId& other) const { return _id < other._id; }
61
62
private:
63
    int32_t _id = -1;
64
};
65
66
// Position of a file-local column in the Block produced by one FileScanRequest.
67
//
68
// This is assigned by TableColumnMapper/TableReader after predicate/non-predicate columns are
69
// deduplicated. It is not a file schema id and it is not stable across requests. Use value() only
70
// at the boundary where an existing Block or expression API still expects a size_t/int position.
71
class LocalIndex {
72
public:
73
192
    constexpr LocalIndex() = default;
74
804
    explicit constexpr LocalIndex(size_t index) : _index(index) {}
75
76
3.23k
    constexpr size_t value() const { return _index; }
77
29
    constexpr bool operator==(const LocalIndex& other) const { return _index == other._index; }
78
0
    constexpr bool operator<(const LocalIndex& other) const { return _index < other._index; }
79
80
private:
81
    size_t _index = 0;
82
};
83
84
// Position of a table/global output column in the final Block returned by TableReader.
85
//
86
// This type is reserved for boundaries that need to refer to caller-visible column order. It must
87
// not be used to index a file-local Block, because schema evolution and lazy materialization can
88
// make file-local order different from table output order.
89
class GlobalIndex {
90
public:
91
1.04k
    constexpr GlobalIndex() = default;
92
872
    explicit constexpr GlobalIndex(size_t index) : _index(index) {}
93
94
25
    constexpr size_t value() const { return _index; }
95
740
    constexpr bool operator==(const GlobalIndex& other) const { return _index == other._index; }
96
1.16k
    constexpr bool operator<(const GlobalIndex& other) const { return _index < other._index; }
97
98
private:
99
    size_t _index = 0;
100
};
101
102
// Index of a split-local constant/default value used to materialize columns that are not read from
103
// the physical file, such as partition columns, added columns with default values, and virtual
104
// table-format columns.
105
//
106
// It is separate from LocalIndex because constants do not occupy a position in the file reader
107
// output block unless an expression explicitly materializes them.
108
class ConstantIndex {
109
public:
110
    constexpr ConstantIndex() = default;
111
29
    explicit constexpr ConstantIndex(size_t index) : _index(index) {}
112
113
44
    constexpr size_t value() const { return _index; }
114
3
    constexpr bool operator==(const ConstantIndex& other) const { return _index == other._index; }
115
0
    constexpr bool operator<(const ConstantIndex& other) const { return _index < other._index; }
116
117
private:
118
    size_t _index = 0;
119
};
120
121
2
inline std::ostream& operator<<(std::ostream& os, const LocalColumnId& id) {
122
2
    return os << id.value();
123
2
}
124
125
0
inline std::ostream& operator<<(std::ostream& os, const LocalIndex& index) {
126
0
    return os << index.value();
127
0
}
128
129
12
inline std::ostream& operator<<(std::ostream& os, const GlobalIndex& index) {
130
12
    return os << index.value();
131
12
}
132
133
5
inline std::ostream& operator<<(std::ostream& os, const ConstantIndex& index) {
134
5
    return os << index.value();
135
5
}
136
137
// A split/file-local constant value used to materialize a table/global column without reading a
138
// physical file column.
139
//
140
// Common producers are partition values, schema-evolution default expressions, generated columns
141
// and table-format virtual columns. The entry is keyed by ConstantIndex in ConstantMap; global_index
142
// keeps the link back to the caller-visible output column.
143
struct ConstantEntry {
144
    GlobalIndex global_index;
145
    VExprContextSPtr expr;
146
    DataTypePtr type;
147
};
148
149
// Per mapping/split collection of constants.
150
//
151
// ConstantIndex only has meaning within this container. Keeping constants separate from LocalIndex
152
// makes it explicit that these values do not occupy positions in the file reader output Block.
153
class ConstantMap {
154
public:
155
19
    ConstantIndex add(ConstantEntry entry) {
156
19
        const auto index = ConstantIndex(_entries.size());
157
19
        _entries.push_back(std::move(entry));
158
19
        return index;
159
19
    }
160
161
10
    const ConstantEntry& get(ConstantIndex index) const {
162
10
        DORIS_CHECK(index.value() < _entries.size());
163
10
        return _entries[index.value()];
164
10
    }
165
166
293
    void clear() { _entries.clear(); }
167
2
    bool empty() const { return _entries.empty(); }
168
8
    size_t size() const { return _entries.size(); }
169
170
0
    const std::vector<ConstantEntry>& entries() const { return _entries; }
171
172
private:
173
    std::vector<ConstantEntry> _entries;
174
};
175
176
// Target of a localized filter.
177
//
178
// A filter can either reference a file-local Block position or a constant entry. Unset entries mean
179
// the filter cannot be evaluated below the table-reader finalize stage.
180
struct FilterEntry {
181
    enum class Kind {
182
        UNSET,
183
        LOCAL,
184
        CONSTANT,
185
    };
186
187
171
    static FilterEntry local(LocalIndex index) {
188
171
        return {.kind = Kind::LOCAL, .index = index.value()};
189
171
    }
190
191
12
    static FilterEntry constant(ConstantIndex index) {
192
12
        return {.kind = Kind::CONSTANT, .index = index.value()};
193
12
    }
194
195
0
    bool is_set() const { return kind != Kind::UNSET; }
196
476
    bool is_local() const { return kind == Kind::LOCAL; }
197
35
    bool is_constant() const { return kind == Kind::CONSTANT; }
198
199
176
    LocalIndex local_index() const {
200
176
        DORIS_CHECK(is_local());
201
176
        return LocalIndex(index);
202
176
    }
203
204
5
    ConstantIndex constant_index() const {
205
5
        DORIS_CHECK(is_constant());
206
5
        return ConstantIndex(index);
207
5
    }
208
209
    Kind kind = Kind::UNSET;
210
    size_t index = 0;
211
};
212
213
enum ColumnType {
214
    DATA_COLUMN = 0,  // normal data column
215
    ROW_NUMBER = 1,   // row number in a file
216
    GLOBAL_ROWID = 2, // global unique row id across files, used by TopN filter
217
};
218
219
struct GlobalRowIdContext {
220
    uint8_t version = 0;
221
    int64_t backend_id = 0;
222
    uint32_t file_id = 0;
223
};
224
225
// Column schema definition shared by table/global projection and file-local schema matching.
226
//
227
// ColumnDefinition intentionally carries schema identity only. FE column unique ids are translated
228
// to GlobalIndex at the FileScannerV2 boundary and must not appear in table/file reader APIs.
229
struct ColumnDefinition {
230
    // Typed identifier value used to match a column against another schema.
231
    //
232
    // - TYPE_NULL: no explicit identifier. BY_NAME falls back to ColumnDefinition::name.
233
    // - TYPE_INT: interpreted by TableColumnMapperOptions::mode as a field id or file position.
234
    // - TYPE_STRING: explicit name identifier.
235
    //
236
    // This is not the id that FileReader uses to read data. For example, a Parquet column can be
237
    // matched by its optional Parquet field_id, while the reader still addresses it by a file-local
238
    // ordinal.
239
    Field identifier;
240
    // Reader-local id of this node inside the file schema returned by FileReader::get_schema().
241
    // Top-level fields use the root column ordinal and nested fields use the child ordinal under
242
    // their parent. -1 means unset; special virtual file columns may use other negative ids.
243
    // Table/global ColumnDefinition values can leave this as -1 because they are not read directly
244
    // by a FileReader.
245
    int32_t local_id = -1;
246
    // Logical table column name. This is also the matching name for by-name file formats.
247
    std::string name;
248
    // Historical or external names for the same logical field. Table formats such as Iceberg can
249
    // use this to resolve partition path keys after column rename.
250
    std::vector<std::string> name_mapping {};
251
    DataTypePtr type;
252
    // Semantic nested children for this schema node.
253
    //
254
    // Table/global columns carry projected table children. File-local schemas returned by
255
    // FileReader::get_schema() also expose semantic children, not physical reader wrappers. For
256
    // example, MAP children are key/value and ARRAY children contain only the element field.
257
    std::vector<ColumnDefinition> children {};
258
    // Expression used to materialize missing/default/generated values when the column is not read
259
    // directly from the file.
260
    VExprContextSPtr default_expr = nullptr;
261
    // Table-format initial default normalized for transport from FE. Binary-like values use Base64
262
    // and set initial_default_value_is_base64 because they can map to STRING/CHAR or VARBINARY.
263
    // Unlike default_expr, this metadata is also available for hidden delete-predicate columns
264
    // that are absent from the query projection.
265
    std::optional<std::string> initial_default_value = std::nullopt;
266
    bool initial_default_value_is_base64 = false;
267
    // Partition columns are constants from split metadata and should not be matched against file
268
    // schema unless table-format logic explicitly asks for it.
269
    bool is_partition_key = false;
270
    // File-local column kind. For table/global columns this remains DATA_COLUMN.
271
    ColumnType column_type = ColumnType::DATA_COLUMN;
272
273
0
    bool has_identifier() const { return !identifier.is_null(); }
274
1.16k
    bool has_identifier_field_id() const { return identifier.get_type() == TYPE_INT; }
275
1.07k
    bool has_identifier_name() const { return identifier.get_type() == TYPE_STRING; }
276
277
    // DuckDB-style helper for BY_FIELD_ID matching. The mapper binds the matching mode once, so a
278
    // TYPE_INT identifier is interpreted as a field id only by the field-id matcher.
279
502
    int32_t get_identifier_field_id() const {
280
502
        DORIS_CHECK(has_identifier_field_id());
281
502
        return identifier.get<TYPE_INT>();
282
502
    }
283
    // DuckDB-style helper for BY_NAME matching. When no explicit string identifier is present, the
284
    // logical column name is the identifier.
285
450
    const std::string& get_identifier_name() const {
286
450
        if (identifier.is_null()) {
287
0
            return name;
288
0
        }
289
450
        DORIS_CHECK(has_identifier_name());
290
450
        return identifier.get<TYPE_STRING>();
291
450
    }
292
    // Helper for BY_INDEX matching. BY_INDEX reuses the TYPE_INT identifier as the table-side file
293
    // position, matching DuckDB's typed identifier plus mapper-mode interpretation.
294
23
    int32_t get_identifier_position() const {
295
23
        DORIS_CHECK(has_identifier_field_id());
296
23
        return identifier.get<TYPE_INT>();
297
23
    }
298
299
    // Helper for reader-local projection and scan requests.
300
1.35k
    int32_t file_local_id() const {
301
1.35k
        if (local_id != -1) {
302
1.35k
            return local_id;
303
1.35k
        }
304
0
        return get_identifier_field_id();
305
1.35k
    }
306
307
    std::string debug_string() const;
308
};
309
310
static constexpr int ROW_POSITION_COLUMN_ID = -10001;
311
static constexpr const char* ROW_POSITION_COLUMN_NAME = "__file_row_position";
312
static constexpr int GLOBAL_ROWID_COLUMN_ID = -10002;
313
314
50
inline ColumnDefinition row_position_column_definition() {
315
50
    ColumnDefinition field;
316
50
    field.identifier = Field::create_field<TYPE_INT>(ROW_POSITION_COLUMN_ID);
317
50
    field.local_id = ROW_POSITION_COLUMN_ID;
318
50
    field.name = ROW_POSITION_COLUMN_NAME;
319
50
    field.type = std::make_shared<DataTypeInt64>();
320
50
    field.column_type = ColumnType::ROW_NUMBER;
321
50
    return field;
322
50
}
323
324
9
inline ColumnDefinition global_rowid_column_definition() {
325
9
    ColumnDefinition field;
326
9
    field.identifier = Field::create_field<TYPE_STRING>(BeConsts::GLOBAL_ROWID_COL);
327
9
    field.local_id = GLOBAL_ROWID_COLUMN_ID;
328
9
    field.name = BeConsts::GLOBAL_ROWID_COL;
329
9
    field.type = std::make_shared<DataTypeString>();
330
9
    field.column_type = ColumnType::GLOBAL_ROWID;
331
9
    return field;
332
9
}
333
334
// Recursive file-local projection path.
335
//
336
// For a root entry in FileScanRequest::{predicate_columns, non_predicate_columns}, index is the
337
// top-level file column id and column_id() is valid. For children, index is the file-local child id
338
// under the parent node. This is the reader schema local id, not an Iceberg/Parquet field id, not a
339
// table child id, and not a child output ordinal.
340
//
341
// project_all_children=true means the whole subtree under this node is needed. When false, children
342
// lists the selected child paths. File readers can use this to avoid constructing readers for
343
// unprojected nested children.
344
struct LocalColumnIndex {
345
    int32_t index = -1;
346
    bool project_all_children = true;
347
    std::vector<LocalColumnIndex> children {};
348
349
589
    static LocalColumnIndex top_level(LocalColumnId column_id) {
350
589
        return {.index = column_id.value()};
351
589
    }
352
353
230
    static LocalColumnIndex local(int32_t local_id) { return {.index = local_id}; }
354
355
179
    static LocalColumnIndex partial_local(int32_t local_id) {
356
179
        return {.index = local_id, .project_all_children = false};
357
179
    }
358
359
2.73k
    LocalColumnId column_id() const { return LocalColumnId(index); }
360
2.18k
    int32_t local_id() const { return index; }
361
    std::string debug_string() const;
362
};
363
364
332
inline bool is_full_projection(const LocalColumnIndex* projection) {
365
332
    return projection == nullptr || projection->project_all_children;
366
332
}
367
368
428
inline bool is_partial_projection(const LocalColumnIndex* projection) {
369
428
    return projection != nullptr && !projection->project_all_children;
370
428
}
371
372
inline const LocalColumnIndex* find_child_projection(const LocalColumnIndex* projection,
373
236
                                                     int32_t local_id) {
374
236
    if (is_full_projection(projection)) {
375
134
        return nullptr;
376
134
    }
377
102
    const auto child_it = std::find_if(
378
102
            projection->children.begin(), projection->children.end(),
379
102
            [&](const LocalColumnIndex& child) { return child.local_id() == local_id; });
380
102
    return child_it == projection->children.end() ? nullptr : &*child_it;
381
236
}
382
383
94
inline bool is_child_projected(const LocalColumnIndex* projection, int32_t local_id) {
384
94
    return is_full_projection(projection) || find_child_projection(projection, local_id) != nullptr;
385
94
}
386
387
// Merge two projection trees that point to the same file-local node.
388
//
389
// A full projection dominates a partial projection. Two partial projections are merged by child id
390
// and recursively union their child paths. The caller must only merge projections for the same
391
// root/child node.
392
31
inline Status merge_local_column_index(LocalColumnIndex* target, const LocalColumnIndex& source) {
393
31
    DORIS_CHECK(target != nullptr);
394
31
    DORIS_CHECK(target->index == source.index);
395
31
    if (target->project_all_children) {
396
12
        return Status::OK();
397
12
    }
398
19
    if (source.project_all_children) {
399
1
        target->project_all_children = true;
400
1
        target->children.clear();
401
1
        return Status::OK();
402
1
    }
403
19
    for (const auto& source_child : source.children) {
404
19
        auto target_child_it = std::find_if(
405
19
                target->children.begin(), target->children.end(),
406
22
                [&](const LocalColumnIndex& child) { return child.index == source_child.index; });
407
19
        if (target_child_it == target->children.end()) {
408
13
            target->children.push_back(source_child);
409
13
            continue;
410
13
        }
411
6
        RETURN_IF_ERROR(merge_local_column_index(&*target_child_it, source_child));
412
6
    }
413
18
    return Status::OK();
414
18
}
415
416
} // namespace doris::format