Coverage Report

Created: 2026-07-10 11:56

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/exec/scan/access_path_parser.cpp
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
#include "exec/scan/access_path_parser.h"
19
20
#include <fmt/format.h>
21
22
#include <algorithm>
23
#include <charconv>
24
#include <map>
25
#include <string>
26
#include <string_view>
27
#include <utility>
28
29
#include "common/cast_set.h"
30
#include "common/consts.h"
31
#include "core/assert_cast.h"
32
#include "core/data_type/data_type.h"
33
#include "core/data_type/data_type_array.h"
34
#include "core/data_type/data_type_map.h"
35
#include "core/data_type/data_type_nullable.h"
36
#include "core/data_type/data_type_struct.h"
37
#include "runtime/descriptors.h"
38
#include "util/string_util.h"
39
40
namespace doris {
41
namespace {
42
43
281k
bool is_scanner_materialized_virtual_column(const std::string& column_name) {
44
281k
    return column_name == BeConsts::ICEBERG_ROWID_COL;
45
281k
}
46
47
48.2k
bool parse_non_negative_int(std::string_view value, int32_t* result) {
48
48.2k
    DORIS_CHECK(result != nullptr);
49
48.2k
    int32_t parsed = -1;
50
48.2k
    const auto* begin = value.data();
51
48.2k
    const auto* end = begin + value.size();
52
48.2k
    const auto [ptr, ec] = std::from_chars(begin, end, parsed);
53
48.2k
    if (ec != std::errc() || ptr != end || parsed < 0) {
54
25.5k
        return false;
55
25.5k
    }
56
22.7k
    *result = parsed;
57
22.7k
    return true;
58
48.2k
}
59
60
2
std::string access_path_to_string(const std::vector<std::string>& path) {
61
2
    return fmt::format("{}", fmt::join(path, "."));
62
2
}
63
64
format::ColumnDefinition* find_or_add_child(format::ColumnDefinition* parent, int32_t id,
65
195k
                                            std::string name, DataTypePtr type) {
66
195k
    DORIS_CHECK(parent != nullptr);
67
195k
    for (auto& child : parent->children) {
68
100k
        if ((child.has_identifier_field_id() && child.get_identifier_field_id() == id) ||
69
100k
            child.name == name) {
70
0
            return &child;
71
0
        }
72
100k
    }
73
195k
    parent->children.push_back({
74
195k
            .identifier = Field::create_field<TYPE_INT>(id),
75
195k
            .name = std::move(name),
76
195k
            .type = std::move(type),
77
195k
            .children = {},
78
195k
            .default_expr = nullptr,
79
195k
            .is_partition_key = false,
80
195k
    });
81
195k
    return &parent->children.back();
82
195k
}
83
84
void inherit_schema_metadata(format::ColumnDefinition* column,
85
195k
                             const format::ColumnDefinition* schema_column) {
86
195k
    if (column == nullptr || schema_column == nullptr) {
87
120k
        return;
88
120k
    }
89
75.3k
    column->name_mapping = schema_column->name_mapping;
90
75.3k
}
91
92
const format::ColumnDefinition* find_schema_child_by_path(
93
55.6k
        const format::ColumnDefinition* schema_column, const std::string& child_path) {
94
55.6k
    if (schema_column == nullptr) {
95
28.3k
        return nullptr;
96
28.3k
    }
97
27.2k
    int32_t parsed_field_id = -1;
98
27.2k
    if (parse_non_negative_int(child_path, &parsed_field_id)) {
99
1.73k
        const auto child_it = std::ranges::find_if(
100
2.05k
                schema_column->children, [&](const format::ColumnDefinition& child) {
101
2.05k
                    return child.has_identifier_field_id() &&
102
2.05k
                           child.get_identifier_field_id() == parsed_field_id;
103
2.05k
                });
104
1.73k
        return child_it == schema_column->children.end() ? nullptr : &*child_it;
105
1.73k
    }
106
54.3k
    const auto child_it = std::ranges::find_if(schema_column->children, [&](const auto& child) {
107
54.3k
        if (to_lower(child.name) == to_lower(child_path)) {
108
25.5k
            return true;
109
25.5k
        }
110
28.8k
        return std::ranges::any_of(child.name_mapping, [&](const std::string& alias) {
111
13
            return to_lower(alias) == to_lower(child_path);
112
13
        });
113
54.3k
    });
114
25.5k
    return child_it == schema_column->children.end() ? nullptr : &*child_it;
115
27.2k
}
116
117
195k
int32_t schema_field_id(const format::ColumnDefinition* schema_column) {
118
195k
    if (schema_column == nullptr || !schema_column->has_identifier_field_id()) {
119
120k
        return -1;
120
120k
    }
121
75.3k
    return schema_column->get_identifier_field_id();
122
195k
}
123
124
193k
int32_t schema_field_id_or(const format::ColumnDefinition* schema_column, int32_t fallback) {
125
193k
    const auto field_id = schema_field_id(schema_column);
126
193k
    return field_id >= 0 ? field_id : fallback;
127
193k
}
128
129
std::string schema_field_name_or(const format::ColumnDefinition* schema_column,
130
53.7k
                                 std::string fallback) {
131
53.7k
    return schema_column == nullptr || schema_column->name.empty() ? std::move(fallback)
132
53.7k
                                                                   : schema_column->name;
133
53.7k
}
134
135
struct AccessPathNode {
136
    bool project_all = false;
137
    std::map<std::string, AccessPathNode> children;
138
};
139
140
971
void merge_access_path_node(AccessPathNode* dst, const AccessPathNode& src) {
141
971
    DORIS_CHECK(dst != nullptr);
142
971
    if (dst->project_all) {
143
0
        return;
144
0
    }
145
971
    if (src.project_all) {
146
597
        dst->project_all = true;
147
597
        dst->children.clear();
148
597
        return;
149
597
    }
150
408
    for (const auto& [path, child] : src.children) {
151
408
        merge_access_path_node(&dst->children[path], child);
152
408
    }
153
374
}
154
155
void insert_access_path(AccessPathNode* root, const std::vector<std::string>& path,
156
47.7k
                        size_t path_idx) {
157
47.7k
    DORIS_CHECK(root != nullptr);
158
47.7k
    if (root->project_all) {
159
0
        return;
160
0
    }
161
47.7k
    if (path_idx >= path.size()) {
162
44.9k
        root->project_all = true;
163
44.9k
        root->children.clear();
164
44.9k
        return;
165
44.9k
    }
166
2.70k
    insert_access_path(&root->children[path[path_idx]], path, path_idx + 1);
167
2.70k
}
168
169
Status build_nested_children_from_access_node(format::ColumnDefinition* column,
170
                                              const DataTypePtr& type, const AccessPathNode& node,
171
                                              const std::string& path,
172
                                              const format::ColumnDefinition* schema_column);
173
174
// Expand a full complex-column projection into table-schema children when the table format provides
175
// an external/current schema. Without this, `SELECT complex_col` or `SELECT *` leaves
176
// ColumnDefinition::children empty, so ColumnMapper treats the root complex column as a scalar
177
// mapping and later tries to cast the old file shape to the current table shape directly.
178
//
179
// Examples:
180
//   - STRUCT country/city projected from an old file STRUCT country/population/location should
181
//     create children country and city, so city can be materialized as missing/default.
182
//   - ARRAY<STRUCT<item, quantity>> should create the array element wrapper and then the element
183
//     struct children item and quantity.
184
//   - MAP<STRING, STRUCT<full_name, age>> should create semantic children key/value directly, then
185
//     expand the value struct children full_name and age. Do not introduce a physical entries
186
//     wrapper here: ColumnMapper and TableReader treat MAP children as [key, value].
187
Status build_all_nested_children_from_schema(format::ColumnDefinition* column,
188
                                             const DataTypePtr& type, const std::string& path,
189
295k
                                             const format::ColumnDefinition* schema_column) {
190
295k
    DORIS_CHECK(column != nullptr);
191
192
295k
    const auto nested_type = remove_nullable(type);
193
295k
    AccessPathNode project_all;
194
295k
    project_all.project_all = true;
195
295k
    switch (nested_type->get_primitive_type()) {
196
24.9k
    case TYPE_STRUCT: {
197
24.9k
        const auto& struct_type = assert_cast<const DataTypeStruct&>(*nested_type);
198
78.6k
        for (size_t field_idx = 0; field_idx < struct_type.get_elements().size(); ++field_idx) {
199
53.7k
            const auto field_name = struct_type.get_element_name(field_idx);
200
53.7k
            const auto* schema_child = find_schema_child_by_path(schema_column, field_name);
201
53.7k
            auto* child = find_or_add_child(
202
53.7k
                    column, schema_field_id_or(schema_child, cast_set<int32_t>(field_idx)),
203
53.7k
                    schema_field_name_or(schema_child, field_name),
204
53.7k
                    struct_type.get_element(field_idx));
205
53.7k
            inherit_schema_metadata(child, schema_child);
206
53.7k
            RETURN_IF_ERROR(build_nested_children_from_access_node(
207
53.7k
                    child, child->type, project_all, path + "." + child->name, schema_child));
208
53.7k
        }
209
24.9k
        return Status::OK();
210
24.9k
    }
211
49.1k
    case TYPE_ARRAY: {
212
49.1k
        const auto& array_type = assert_cast<const DataTypeArray&>(*nested_type);
213
49.1k
        const auto* element_schema = schema_column != nullptr && !schema_column->children.empty()
214
49.1k
                                             ? &schema_column->children[0]
215
49.1k
                                             : nullptr;
216
49.1k
        auto* child = find_or_add_child(column, schema_field_id_or(element_schema, 0), "element",
217
49.1k
                                        array_type.get_nested_type());
218
49.1k
        inherit_schema_metadata(child, element_schema);
219
49.1k
        return build_nested_children_from_access_node(child, child->type, project_all, path + ".*",
220
49.1k
                                                      element_schema);
221
24.9k
    }
222
44.8k
    case TYPE_MAP: {
223
44.8k
        const auto& map_type = assert_cast<const DataTypeMap&>(*nested_type);
224
44.8k
        const auto* key_schema = schema_column != nullptr && !schema_column->children.empty()
225
44.8k
                                         ? &schema_column->children[0]
226
44.8k
                                         : nullptr;
227
44.8k
        const auto* value_schema = schema_column != nullptr && schema_column->children.size() > 1
228
44.8k
                                           ? &schema_column->children[1]
229
44.8k
                                           : nullptr;
230
44.8k
        auto* key_child = find_or_add_child(column, schema_field_id_or(key_schema, 0), "key",
231
44.8k
                                            map_type.get_key_type());
232
44.8k
        inherit_schema_metadata(key_child, key_schema);
233
44.8k
        RETURN_IF_ERROR(build_nested_children_from_access_node(
234
44.8k
                key_child, key_child->type, project_all, path + ".KEYS", key_schema));
235
44.8k
        auto* value_child = find_or_add_child(column, schema_field_id_or(value_schema, 1), "value",
236
44.8k
                                              map_type.get_value_type());
237
44.8k
        inherit_schema_metadata(value_child, value_schema);
238
44.8k
        RETURN_IF_ERROR(build_nested_children_from_access_node(
239
44.8k
                value_child, value_child->type, project_all, path + ".VALUES", value_schema));
240
44.8k
        return Status::OK();
241
44.8k
    }
242
176k
    default:
243
176k
        return Status::OK();
244
295k
    }
245
295k
}
246
247
Status build_struct_children_from_access_node(format::ColumnDefinition* column,
248
                                              const DataTypeStruct& struct_type,
249
                                              const AccessPathNode& node, const std::string& path,
250
1.55k
                                              const format::ColumnDefinition* schema_column) {
251
1.55k
    DORIS_CHECK(column != nullptr);
252
1.85k
    for (const auto& [child_path, child_node] : node.children) {
253
        // Struct children are resolved by name or schema field id. We do not treat a numeric
254
        // child token as a struct ordinal, because `col.0` becomes ambiguous once the struct
255
        // evolves. Position-based access needs a separate design if it is required later.
256
1.85k
        if (child_path == "OFFSET" || child_path == "*" || child_path == "KEYS" ||
257
1.85k
            child_path == "VALUES") {
258
4
            return Status::NotSupported(
259
4
                    "AccessPathParser does not support access path {} for slot {}",
260
4
                    path + "." + child_path, column->name);
261
4
        }
262
263
        // Prefer the table/schema ColumnDefinition because it carries field ids and aliases.
264
        // Fallback to the struct type name only for formats without external schema metadata.
265
1.84k
        const auto* schema_child = find_schema_child_by_path(schema_column, child_path);
266
1.84k
        int32_t field_id = schema_field_id(schema_child);
267
1.84k
        std::string field_name = schema_child == nullptr ? child_path : schema_child->name;
268
1.84k
        DataTypePtr field_type = schema_child == nullptr ? nullptr : schema_child->type;
269
1.84k
        if (field_id < 0 || field_type == nullptr) {
270
139
            for (size_t field_idx = 0; field_idx < struct_type.get_elements().size(); ++field_idx) {
271
136
                if (to_lower(struct_type.get_element_name(field_idx)) == to_lower(field_name)) {
272
103
                    field_id = cast_set<int32_t>(field_idx);
273
103
                    field_name = struct_type.get_element_name(field_idx);
274
103
                    field_type = struct_type.get_element(field_idx);
275
103
                    break;
276
103
                }
277
136
            }
278
106
        }
279
280
1.84k
        if (field_id < 0 || field_type == nullptr) {
281
3
            return Status::NotSupported(
282
3
                    "AccessPathParser does not support access path {} for slot {}",
283
3
                    path + "." + child_path, column->name);
284
3
        }
285
        // TODO: For TVF Parquet files without field ids, this fallback uses the struct ordinal as
286
        // the table child identifier. BY_NAME mapping should instead keep a string identifier and
287
        // let TableColumnMapper resolve the file-local child id from the Parquet schema.
288
1.84k
        auto* child = find_or_add_child(column, field_id, field_name, field_type);
289
1.84k
        inherit_schema_metadata(child, schema_child);
290
1.84k
        RETURN_IF_ERROR(build_nested_children_from_access_node(
291
1.84k
                child, child->type, child_node, path + "." + child_path, schema_child));
292
1.84k
    }
293
1.55k
    return Status::OK();
294
1.55k
}
295
296
Status build_map_children_from_access_node(format::ColumnDefinition* column,
297
                                           const DataTypeMap& map_type, const AccessPathNode& node,
298
                                           const std::string& path,
299
492
                                           const format::ColumnDefinition* schema_column) {
300
492
    DORIS_CHECK(column != nullptr);
301
492
    AccessPathNode key_node;
302
492
    AccessPathNode value_node;
303
492
    bool need_key = false;
304
492
    bool need_value = false;
305
306
565
    for (const auto& [child_path, child_node] : node.children) {
307
565
        if (child_path == "OFFSET") {
308
1
            return Status::NotSupported(
309
1
                    "AccessPathParser does not support access path {} for slot {}",
310
1
                    path + "." + child_path, column->name);
311
1
        }
312
564
        if (child_path == "KEYS") {
313
117
            need_key = true;
314
117
            merge_access_path_node(&key_node, child_node);
315
117
            continue;
316
117
        }
317
447
        if (child_path == "VALUES") {
318
391
            need_key = true;
319
391
            key_node.project_all = true;
320
391
            key_node.children.clear();
321
391
            need_value = true;
322
391
            merge_access_path_node(&value_node, child_node);
323
391
            continue;
324
391
        }
325
56
        if (child_path == "*") {
326
55
            need_key = true;
327
55
            key_node.project_all = true;
328
55
            key_node.children.clear();
329
55
            need_value = true;
330
55
            merge_access_path_node(&value_node, child_node);
331
55
            continue;
332
55
        }
333
1
        return Status::NotSupported("AccessPathParser does not support access path {} for slot {}",
334
1
                                    path + "." + child_path, column->name);
335
56
    }
336
490
    if (need_key && !need_value) {
337
        // A key-only MAP projection is not independently materializable yet. FileScannerV2 can
338
        // describe a projection such as `m.KEYS`, but the downstream file block -> table block path
339
        // still builds a ColumnMap from key column + value column + offsets. If the value child is
340
        // omitted here, TableReader/ColumnMapper cannot reconstruct a valid table MAP column even
341
        // though the query only needs keys.
342
        //
343
        // Example:
344
        //   SELECT map_keys(m) FROM t;
345
        // or
346
        //   SELECT * FROM t WHERE array_contains(map_keys(m), 'k1');
347
        //
348
        // The access path only asks for `m.KEYS`, but the scan still has to read `m.VALUES` as a
349
        // temporary full projection until map materialization supports constructing a table MAP
350
        // from keys only.
351
45
        need_value = true;
352
45
        value_node.project_all = true;
353
45
        value_node.children.clear();
354
45
    }
355
356
490
    if (!need_key && !need_value) {
357
0
        return Status::OK();
358
0
    }
359
360
490
    const auto* key_schema = schema_column != nullptr && !schema_column->children.empty()
361
490
                                     ? &schema_column->children[0]
362
490
                                     : nullptr;
363
490
    const auto* value_schema = schema_column != nullptr && schema_column->children.size() > 1
364
490
                                       ? &schema_column->children[1]
365
490
                                       : nullptr;
366
490
    if (need_key) {
367
490
        auto* key_child = find_or_add_child(column, schema_field_id_or(key_schema, 0), "key",
368
490
                                            map_type.get_key_type());
369
490
        inherit_schema_metadata(key_child, key_schema);
370
490
        RETURN_IF_ERROR(build_nested_children_from_access_node(key_child, key_child->type, key_node,
371
490
                                                               path + ".KEYS", key_schema));
372
490
    }
373
490
    if (need_value) {
374
490
        auto* value_child = find_or_add_child(column, schema_field_id_or(value_schema, 1), "value",
375
490
                                              map_type.get_value_type());
376
490
        inherit_schema_metadata(value_child, value_schema);
377
490
        RETURN_IF_ERROR(build_nested_children_from_access_node(
378
490
                value_child, value_child->type, value_node, path + ".VALUES", value_schema));
379
490
    }
380
489
    return Status::OK();
381
490
}
382
383
Status build_nested_children_from_access_node(format::ColumnDefinition* column,
384
                                              const DataTypePtr& type, const AccessPathNode& node,
385
                                              const std::string& path,
386
297k
                                              const format::ColumnDefinition* schema_column) {
387
297k
    DORIS_CHECK(column != nullptr);
388
297k
    if (node.project_all || node.children.empty()) {
389
295k
        return build_all_nested_children_from_schema(column, type, path, schema_column);
390
295k
    }
391
392
2.28k
    const auto nested_type = remove_nullable(type);
393
2.28k
    switch (nested_type->get_primitive_type()) {
394
1.55k
    case TYPE_STRUCT:
395
1.55k
        return build_struct_children_from_access_node(
396
1.55k
                column, assert_cast<const DataTypeStruct&>(*nested_type), node, path,
397
1.55k
                schema_column);
398
236
    case TYPE_ARRAY: {
399
236
        if (node.children.size() != 1 || !node.children.contains("*")) {
400
2
            return Status::NotSupported(
401
2
                    "AccessPathParser does not support access path {} for slot {}", path,
402
2
                    column->name);
403
2
        }
404
234
        const auto& array_type = assert_cast<const DataTypeArray&>(*nested_type);
405
234
        const auto* element_schema = schema_column != nullptr && !schema_column->children.empty()
406
234
                                             ? &schema_column->children[0]
407
234
                                             : nullptr;
408
234
        auto* child = find_or_add_child(column, schema_field_id_or(element_schema, 0), "element",
409
234
                                        array_type.get_nested_type());
410
234
        inherit_schema_metadata(child, element_schema);
411
234
        return build_nested_children_from_access_node(child, child->type, node.children.at("*"),
412
234
                                                      path + ".*", element_schema);
413
236
    }
414
492
    case TYPE_MAP:
415
492
        return build_map_children_from_access_node(
416
492
                column, assert_cast<const DataTypeMap&>(*nested_type), node, path, schema_column);
417
0
    default:
418
0
        return Status::NotSupported("AccessPathParser does not support access path {} for slot {}",
419
0
                                    path, column->name);
420
2.28k
    }
421
2.28k
}
422
423
} // namespace
424
425
Status AccessPathParser::build_nested_children(format::ColumnDefinition* column,
426
                                               const std::vector<TColumnAccessPath>& access_paths,
427
281k
                                               const format::ColumnDefinition* schema_column) {
428
281k
    DORIS_CHECK(column != nullptr);
429
281k
    if (is_scanner_materialized_virtual_column(column->name)) {
430
191
        return Status::OK();
431
191
    }
432
280k
    if (!is_complex_type(remove_nullable(column->type)->get_primitive_type())) {
433
179k
        return Status::OK();
434
179k
    }
435
436
101k
    AccessPathNode root;
437
    // Build tree for AccessPathNode.
438
    // For example, for access paths ["a.b", "a.c", "d"], the tree will be:
439
    // root
440
    // ├── a
441
    // │   ├── b
442
    // │   └── c
443
    // └── d
444
101k
    for (const auto& access_path : access_paths) {
445
        // TODO: Support META access paths if needed. Currently AccessPathParser only supports
446
        // DATA access paths.
447
44.9k
        if (access_path.type != TAccessPathType::DATA || !access_path.__isset.data_access_path) {
448
2
            return Status::NotSupported(
449
2
                    "AccessPathParser only supports DATA access paths for slot {}", column->name);
450
2
        }
451
44.9k
        const auto& path = access_path.data_access_path.path;
452
44.9k
        if (path.empty()) {
453
0
            insert_access_path(&root, path, 0);
454
0
            continue;
455
0
        }
456
44.9k
        int32_t top_level_id = -1;
457
44.9k
        if (to_lower(path.front()) != to_lower(column->name) &&
458
44.9k
            (!parse_non_negative_int(path.front(), &top_level_id) ||
459
21.0k
             !column->has_identifier_field_id() ||
460
21.0k
             top_level_id != column->get_identifier_field_id())) {
461
2
            return Status::NotSupported("AccessPathParser access path {} does not match slot {}",
462
2
                                        access_path_to_string(path), column->name);
463
2
        }
464
44.9k
        insert_access_path(&root, path, 1);
465
44.9k
    }
466
    // Recursively build nested children for the column based on the AccessPathNode tree.
467
101k
    return build_nested_children_from_access_node(column, column->type, root, column->name,
468
101k
                                                  schema_column);
469
101k
}
470
471
Status AccessPathParser::build_nested_children(format::ColumnDefinition* column,
472
                                               const SlotDescriptor* slot_desc,
473
281k
                                               const format::ColumnDefinition* schema_column) {
474
281k
    DORIS_CHECK(column != nullptr);
475
281k
    DORIS_CHECK(slot_desc != nullptr);
476
281k
    return build_nested_children(column, slot_desc->all_access_paths(), schema_column);
477
281k
}
478
479
} // namespace doris