Coverage Report

Created: 2026-08-18 12:47

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
6.92k
    constexpr LocalColumnId() = default;
51
82.3k
    explicit constexpr LocalColumnId(int32_t id) : _id(id) {}
52
53
6.92k
    static constexpr LocalColumnId invalid() { return LocalColumnId(); }
54
55
10.3M
    constexpr int32_t value() const { return _id; }
56
3.23k
    constexpr bool is_valid() const { return _id >= 0; }
57
58
53.3k
    constexpr bool operator==(const LocalColumnId& other) const { return _id == other._id; }
59
2.40k
    constexpr bool operator!=(const LocalColumnId& other) const { return !(*this == other); }
60
84.5k
    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
2.63k
    constexpr LocalIndex() = default;
74
6.78k
    explicit constexpr LocalIndex(size_t index) : _index(index) {}
75
76
10.4M
    constexpr size_t value() const { return _index; }
77
61
    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
4.82k
    constexpr GlobalIndex() = default;
92
6.15k
    explicit constexpr GlobalIndex(size_t index) : _index(index) {}
93
94
37
    constexpr size_t value() const { return _index; }
95
22.6k
    constexpr bool operator==(const GlobalIndex& other) const { return _index == other._index; }
96
30.8k
    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
34
    explicit constexpr ConstantIndex(size_t index) : _index(index) {}
112
113
67
    constexpr size_t value() const { return _index; }
114
    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
24
    ConstantIndex add(ConstantEntry entry) {
156
24
        const auto index = ConstantIndex(_entries.size());
157
24
        _entries.push_back(std::move(entry));
158
24
        return index;
159
24
    }
160
161
11
    const ConstantEntry& get(ConstantIndex index) const {
162
11
        DORIS_CHECK(index.value() < _entries.size());
163
11
        return _entries[index.value()];
164
11
    }
165
166
1.06k
    void clear() { _entries.clear(); }
167
    bool empty() const { return _entries.empty(); }
168
9
    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
3.85k
    static FilterEntry local(LocalIndex index) {
188
3.85k
        return {.kind = Kind::LOCAL, .index = index.value()};
189
3.85k
    }
190
191
32
    static FilterEntry constant(ConstantIndex index) {
192
32
        return {.kind = Kind::CONSTANT, .index = index.value()};
193
32
    }
194
195
0
    bool is_set() const { return kind != Kind::UNSET; }
196
4.02k
    bool is_local() const { return kind == Kind::LOCAL; }
197
153
    bool is_constant() const { return kind == Kind::CONSTANT; }
198
199
1.93k
    LocalIndex local_index() const {
200
1.93k
        DORIS_CHECK(is_local());
201
1.93k
        return LocalIndex(index);
202
1.93k
    }
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
    // Distinguishes no table-level mapping from an explicit empty field mapping. The latter must
252
    // not fall back to the current name when matching fields in legacy files.
253
    bool has_name_mapping = false;
254
    DataTypePtr type;
255
    // Semantic nested children for this schema node.
256
    //
257
    // Table/global columns carry projected table children. File-local schemas returned by
258
    // FileReader::get_schema() also expose semantic children, not physical reader wrappers. For
259
    // example, MAP children are key/value and ARRAY children contain only the element field.
260
    std::vector<ColumnDefinition> children {};
261
    // Full table-schema identity subtree before access-path pruning. ID-less physical complex
262
    // wrappers must be discovered from this view without adding unrequested children to output.
263
    std::vector<ColumnDefinition> identity_children {};
264
    // Logical object-key paths requested from a Variant column. An empty collection means the
265
    // whole Variant is required; non-empty paths may be resolved to format-specific shredded
266
    // physical children after the per-file schema is known.
267
    std::vector<std::vector<std::string>> variant_access_paths {};
268
    // Predicate access paths are kept separately from the final union projection. File Scanner V2
269
    // can lower this smaller semantic tree to an eager predicate projection while deferring the
270
    // final children until rows survive. The flag distinguishes no predicate metadata from a
271
    // whole-root predicate, whose child/path collections are intentionally empty.
272
    bool has_predicate_access_paths = false;
273
    std::vector<ColumnDefinition> predicate_children {};
274
    std::vector<std::vector<std::string>> predicate_variant_access_paths {};
275
    // Expression used to materialize missing/default/generated values when the column is not read
276
    // directly from the file.
277
    VExprContextSPtr default_expr = nullptr;
278
    // Table-format initial default normalized for transport from FE. Binary-like values use Base64
279
    // and set initial_default_value_is_base64 because they can map to STRING/CHAR or VARBINARY.
280
    // Unlike default_expr, this metadata is also available for hidden delete-predicate columns
281
    // that are absent from the query projection.
282
    std::optional<std::string> initial_default_value = std::nullopt;
283
    bool initial_default_value_is_base64 = false;
284
    // Table-format field optionality. std::nullopt means the format did not provide this semantic
285
    // metadata. Iceberg uses an explicit false value to reject old files that are missing a
286
    // required field without an initial default.
287
    std::optional<bool> is_optional = std::nullopt;
288
    // Logical timestamp semantic supplied by a table format when the physical encoding cannot
289
    // carry it (for example, Paimon TIMESTAMP versus TIMESTAMP_LTZ stored as INT96).
290
    std::optional<bool> timestamp_is_adjusted_to_utc = std::nullopt;
291
    // Partition columns are constants from split metadata and should not be matched against file
292
    // schema unless table-format logic explicitly asks for it.
293
    bool is_partition_key = false;
294
    // File-local column kind. For table/global columns this remains DATA_COLUMN.
295
    ColumnType column_type = ColumnType::DATA_COLUMN;
296
297
0
    bool has_identifier() const { return !identifier.is_null(); }
298
2.89k
    bool has_identifier_field_id() const { return identifier.get_type() == TYPE_INT; }
299
57.5k
    bool has_identifier_name() const { return identifier.get_type() == TYPE_STRING; }
300
301
    // DuckDB-style helper for BY_FIELD_ID matching. The mapper binds the matching mode once, so a
302
    // TYPE_INT identifier is interpreted as a field id only by the field-id matcher.
303
1.27k
    int32_t get_identifier_field_id() const {
304
1.27k
        DORIS_CHECK(has_identifier_field_id());
305
1.27k
        return identifier.get<TYPE_INT>();
306
1.27k
    }
307
    // DuckDB-style helper for BY_NAME matching. When no explicit string identifier is present, the
308
    // logical column name is the identifier.
309
27.6k
    const std::string& get_identifier_name() const {
310
27.6k
        if (identifier.is_null()) {
311
0
            return name;
312
0
        }
313
27.6k
        DORIS_CHECK(has_identifier_name());
314
27.6k
        return identifier.get<TYPE_STRING>();
315
27.6k
    }
316
    // Helper for BY_INDEX matching. BY_INDEX reuses the TYPE_INT identifier as the table-side file
317
    // position, matching DuckDB's typed identifier plus mapper-mode interpretation.
318
23
    int32_t get_identifier_position() const {
319
23
        DORIS_CHECK(has_identifier_field_id());
320
23
        return identifier.get<TYPE_INT>();
321
23
    }
322
323
    // Helper for reader-local projection and scan requests.
324
14.4k
    int32_t file_local_id() const {
325
14.4k
        if (local_id != -1) {
326
14.4k
            return local_id;
327
14.4k
        }
328
0
        return get_identifier_field_id();
329
14.4k
    }
330
331
    std::string debug_string() const;
332
};
333
334
static constexpr int ROW_POSITION_COLUMN_ID = -10001;
335
static constexpr const char* ROW_POSITION_COLUMN_NAME = "__file_row_position";
336
static constexpr int GLOBAL_ROWID_COLUMN_ID = -10002;
337
338
67
inline ColumnDefinition row_position_column_definition() {
339
67
    ColumnDefinition field;
340
67
    field.identifier = Field::create_field<TYPE_INT>(ROW_POSITION_COLUMN_ID);
341
67
    field.local_id = ROW_POSITION_COLUMN_ID;
342
67
    field.name = ROW_POSITION_COLUMN_NAME;
343
67
    field.type = std::make_shared<DataTypeInt64>();
344
67
    field.column_type = ColumnType::ROW_NUMBER;
345
67
    return field;
346
67
}
347
348
22
inline ColumnDefinition global_rowid_column_definition() {
349
22
    ColumnDefinition field;
350
22
    field.identifier = Field::create_field<TYPE_STRING>(BeConsts::GLOBAL_ROWID_COL);
351
22
    field.local_id = GLOBAL_ROWID_COLUMN_ID;
352
22
    field.name = BeConsts::GLOBAL_ROWID_COL;
353
22
    field.type = std::make_shared<DataTypeString>();
354
22
    field.column_type = ColumnType::GLOBAL_ROWID;
355
22
    return field;
356
22
}
357
358
// Recursive file-local projection path.
359
//
360
// For a root entry in FileScanRequest::{predicate_columns, non_predicate_columns}, index is the
361
// top-level file column id and column_id() is valid. For children, index is the file-local child id
362
// under the parent node. This is the reader schema local id, not an Iceberg/Parquet field id, not a
363
// table child id, and not a child output ordinal.
364
//
365
// project_all_children=true means the whole subtree under this node is needed. When false, children
366
// lists the selected child paths. File readers can use this to avoid constructing readers for
367
// unprojected nested children.
368
struct LocalColumnIndex {
369
    int32_t index = -1;
370
    bool project_all_children = true;
371
    std::vector<LocalColumnIndex> children {};
372
    std::optional<bool> timestamp_is_adjusted_to_utc = std::nullopt;
373
374
2.80k
    static LocalColumnIndex top_level(LocalColumnId column_id) {
375
2.80k
        return {.index = column_id.value()};
376
2.80k
    }
377
378
370
    static LocalColumnIndex local(int32_t local_id) { return {.index = local_id}; }
379
380
391
    static LocalColumnIndex partial_local(int32_t local_id) {
381
391
        return {.index = local_id, .project_all_children = false};
382
391
    }
383
384
55.5k
    LocalColumnId column_id() const { return LocalColumnId(index); }
385
8.65k
    int32_t local_id() const { return index; }
386
    std::string debug_string() const;
387
};
388
389
28
inline bool same_local_column_index(const LocalColumnIndex& lhs, const LocalColumnIndex& rhs) {
390
28
    if (lhs.index != rhs.index || lhs.project_all_children != rhs.project_all_children ||
391
28
        lhs.children.size() != rhs.children.size()) {
392
9
        return false;
393
9
    }
394
25
    for (size_t i = 0; i < lhs.children.size(); ++i) {
395
11
        if (!same_local_column_index(lhs.children[i], rhs.children[i])) {
396
5
            return false;
397
5
        }
398
11
    }
399
14
    return true;
400
19
}
401
402
209
inline bool is_full_projection(const LocalColumnIndex* projection) {
403
209
    return projection == nullptr || projection->project_all_children;
404
209
}
405
406
1.86k
inline bool is_partial_projection(const LocalColumnIndex* projection) {
407
1.86k
    return projection != nullptr && !projection->project_all_children;
408
1.86k
}
409
410
inline const LocalColumnIndex* find_child_projection(const LocalColumnIndex* projection,
411
64
                                                     int32_t local_id) {
412
64
    if (is_full_projection(projection)) {
413
45
        return nullptr;
414
45
    }
415
19
    const auto child_it = std::find_if(
416
19
            projection->children.begin(), projection->children.end(),
417
24
            [&](const LocalColumnIndex& child) { return child.local_id() == local_id; });
418
19
    return child_it == projection->children.end() ? nullptr : &*child_it;
419
64
}
420
421
52
inline bool is_child_projected(const LocalColumnIndex* projection, int32_t local_id) {
422
52
    return is_full_projection(projection) || find_child_projection(projection, local_id) != nullptr;
423
52
}
424
425
// Merge two projection trees that point to the same file-local node.
426
//
427
// A full projection dominates a partial projection. Two partial projections are merged by child id
428
// and recursively union their child paths. The caller must only merge projections for the same
429
// root/child node.
430
16
inline Status merge_local_column_index(LocalColumnIndex* target, const LocalColumnIndex& source) {
431
16
    DORIS_CHECK(target != nullptr);
432
16
    DORIS_CHECK(target->index == source.index);
433
16
    if (!target->timestamp_is_adjusted_to_utc.has_value()) {
434
16
        target->timestamp_is_adjusted_to_utc = source.timestamp_is_adjusted_to_utc;
435
16
    } else if (source.timestamp_is_adjusted_to_utc.has_value() &&
436
0
               target->timestamp_is_adjusted_to_utc != source.timestamp_is_adjusted_to_utc) {
437
0
        return Status::InvalidArgument("Conflicting timestamp semantics for file-local column {}",
438
0
                                       target->index);
439
0
    }
440
16
    if (target->project_all_children) {
441
5
        return Status::OK();
442
5
    }
443
11
    if (source.project_all_children) {
444
1
        target->project_all_children = true;
445
1
        target->children.clear();
446
1
        return Status::OK();
447
1
    }
448
11
    for (const auto& source_child : source.children) {
449
11
        auto target_child_it = std::find_if(
450
11
                target->children.begin(), target->children.end(),
451
13
                [&](const LocalColumnIndex& child) { return child.index == source_child.index; });
452
11
        if (target_child_it == target->children.end()) {
453
8
            target->children.push_back(source_child);
454
8
            continue;
455
8
        }
456
3
        RETURN_IF_ERROR(merge_local_column_index(&*target_child_it, source_child));
457
3
    }
458
10
    return Status::OK();
459
10
}
460
461
} // namespace doris::format