Coverage Report

Created: 2026-08-04 17:23

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
43
bool is_scanner_materialized_virtual_column(const std::string& column_name) {
44
43
    return column_name == BeConsts::ICEBERG_ROWID_COL;
45
43
}
46
47
32
bool parse_non_negative_int(std::string_view value, int32_t* result) {
48
32
    DORIS_CHECK(result != nullptr);
49
32
    int32_t parsed = -1;
50
32
    const auto* begin = value.data();
51
32
    const auto* end = begin + value.size();
52
32
    const auto [ptr, ec] = std::from_chars(begin, end, parsed);
53
32
    if (ec != std::errc() || ptr != end || parsed < 0) {
54
25
        return false;
55
25
    }
56
7
    *result = parsed;
57
7
    return true;
58
32
}
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
44
                                            std::string name, DataTypePtr type) {
66
44
    DORIS_CHECK(parent != nullptr);
67
44
    for (auto& child : parent->children) {
68
16
        if ((child.has_identifier_field_id() && child.get_identifier_field_id() == id) ||
69
16
            child.name == name) {
70
0
            return &child;
71
0
        }
72
16
    }
73
44
    parent->children.push_back({
74
44
            .identifier = Field::create_field<TYPE_INT>(id),
75
44
            .name = std::move(name),
76
44
            .type = std::move(type),
77
44
            .children = {},
78
44
            .default_expr = nullptr,
79
44
            .is_partition_key = false,
80
44
    });
81
44
    return &parent->children.back();
82
44
}
83
84
void inherit_schema_metadata(format::ColumnDefinition* column,
85
44
                             const format::ColumnDefinition* schema_column) {
86
44
    if (column == nullptr || schema_column == nullptr) {
87
8
        return;
88
8
    }
89
36
    column->name_mapping = schema_column->name_mapping;
90
    // The presence bit is part of the mapping contract: an explicit empty mapping must remain
91
    // authoritative after access-path pruning instead of enabling current-name fallback.
92
36
    column->has_name_mapping = schema_column->has_name_mapping;
93
    // Initial defaults describe the logical value of fields absent from older files. Nested
94
    // access-path pruning must retain them just like it retains rename metadata.
95
36
    column->initial_default_value = schema_column->initial_default_value;
96
36
    column->initial_default_value_is_base64 = schema_column->initial_default_value_is_base64;
97
36
}
98
99
const format::ColumnDefinition* find_schema_child_by_path(
100
        const format::ColumnDefinition* schema_column, const std::string& child_path,
101
30
        bool prefer_exact_name_match) {
102
30
    if (schema_column == nullptr) {
103
5
        return nullptr;
104
5
    }
105
25
    int32_t parsed_field_id = -1;
106
25
    if (parse_non_negative_int(child_path, &parsed_field_id)) {
107
1
        const auto child_it = std::ranges::find_if(
108
2
                schema_column->children, [&](const format::ColumnDefinition& child) {
109
2
                    return child.has_identifier_field_id() &&
110
2
                           child.get_identifier_field_id() == parsed_field_id;
111
2
                });
112
1
        return child_it == schema_column->children.end() ? nullptr : &*child_it;
113
1
    }
114
24
    if (!prefer_exact_name_match) {
115
1
        const auto child_it = std::ranges::find_if(schema_column->children, [&](const auto& child) {
116
1
            if (to_lower(child.name) == to_lower(child_path)) {
117
0
                return true;
118
0
            }
119
1
            return std::ranges::any_of(child.name_mapping, [&](const std::string& alias) {
120
1
                return to_lower(alias) == to_lower(child_path);
121
1
            });
122
1
        });
123
1
        return child_it == schema_column->children.end() ? nullptr : &*child_it;
124
1
    }
125
    // Iceberg can reuse a historical name for a newly added sibling. Current names therefore
126
    // have precedence across the entire struct; an earlier alias must not steal that access path.
127
45
    const auto exact_it = std::ranges::find_if(schema_column->children, [&](const auto& child) {
128
45
        return to_lower(child.name) == to_lower(child_path);
129
45
    });
130
23
    if (exact_it != schema_column->children.end()) {
131
17
        return &*exact_it;
132
17
    }
133
11
    const auto alias_it = std::ranges::find_if(schema_column->children, [&](const auto& child) {
134
11
        return std::ranges::any_of(child.name_mapping, [&](const std::string& alias) {
135
6
            return to_lower(alias) == to_lower(child_path);
136
6
        });
137
11
    });
138
6
    return alias_it == schema_column->children.end() ? nullptr : &*alias_it;
139
23
}
140
141
47
int32_t schema_field_id(const format::ColumnDefinition* schema_column) {
142
47
    if (schema_column == nullptr || !schema_column->has_identifier_field_id()) {
143
11
        return -1;
144
11
    }
145
36
    return schema_column->get_identifier_field_id();
146
47
}
147
148
29
int32_t schema_field_id_or(const format::ColumnDefinition* schema_column, int32_t fallback) {
149
29
    const auto field_id = schema_field_id(schema_column);
150
29
    return field_id >= 0 ? field_id : fallback;
151
29
}
152
153
std::string schema_field_name_or(const format::ColumnDefinition* schema_column,
154
12
                                 std::string fallback) {
155
12
    return schema_column == nullptr || schema_column->name.empty() ? std::move(fallback)
156
12
                                                                   : schema_column->name;
157
12
}
158
159
struct AccessPathNode {
160
    bool project_all = false;
161
    std::map<std::string, AccessPathNode> children;
162
};
163
164
11
void merge_access_path_node(AccessPathNode* dst, const AccessPathNode& src) {
165
11
    DORIS_CHECK(dst != nullptr);
166
11
    if (dst->project_all) {
167
0
        return;
168
0
    }
169
11
    if (src.project_all) {
170
6
        dst->project_all = true;
171
6
        dst->children.clear();
172
6
        return;
173
6
    }
174
5
    for (const auto& [path, child] : src.children) {
175
5
        merge_access_path_node(&dst->children[path], child);
176
5
    }
177
5
}
178
179
void insert_access_path(AccessPathNode* root, const std::vector<std::string>& path,
180
72
                        size_t path_idx) {
181
72
    DORIS_CHECK(root != nullptr);
182
72
    if (root->project_all) {
183
0
        return;
184
0
    }
185
72
    if (path_idx >= path.size()) {
186
33
        root->project_all = true;
187
33
        root->children.clear();
188
33
        return;
189
33
    }
190
39
    insert_access_path(&root->children[path[path_idx]], path, path_idx + 1);
191
39
}
192
193
void collect_variant_access_paths(const AccessPathNode& node, std::vector<std::string>* path,
194
3
                                  std::vector<std::vector<std::string>>* result) {
195
3
    DORIS_CHECK(path != nullptr && result != nullptr);
196
3
    for (const auto& [segment, child] : node.children) {
197
3
        path->push_back(segment);
198
3
        if (child.project_all || child.children.empty()) {
199
3
            result->push_back(*path);
200
3
        } else {
201
0
            collect_variant_access_paths(child, path, result);
202
0
        }
203
3
        path->pop_back();
204
3
    }
205
3
}
206
207
Status build_nested_children_from_access_node(format::ColumnDefinition* column,
208
                                              const DataTypePtr& type, const AccessPathNode& node,
209
                                              const std::string& path,
210
                                              const format::ColumnDefinition* schema_column,
211
                                              bool prefer_exact_name_match);
212
213
// Expand a full complex-column projection into table-schema children when the table format provides
214
// an external/current schema. Without this, `SELECT complex_col` or `SELECT *` leaves
215
// ColumnDefinition::children empty, so ColumnMapper treats the root complex column as a scalar
216
// mapping and later tries to cast the old file shape to the current table shape directly.
217
//
218
// Examples:
219
//   - STRUCT country/city projected from an old file STRUCT country/population/location should
220
//     create children country and city, so city can be materialized as missing/default.
221
//   - ARRAY<STRUCT<item, quantity>> should create the array element wrapper and then the element
222
//     struct children item and quantity.
223
//   - MAP<STRING, STRUCT<full_name, age>> should create semantic children key/value directly, then
224
//     expand the value struct children full_name and age. Do not introduce a physical entries
225
//     wrapper here: ColumnMapper and TableReader treat MAP children as [key, value].
226
Status build_all_nested_children_from_schema(format::ColumnDefinition* column,
227
                                             const DataTypePtr& type, const std::string& path,
228
                                             const format::ColumnDefinition* schema_column,
229
39
                                             bool prefer_exact_name_match) {
230
39
    DORIS_CHECK(column != nullptr);
231
232
39
    const auto nested_type = remove_nullable(type);
233
39
    AccessPathNode project_all;
234
39
    project_all.project_all = true;
235
39
    switch (nested_type->get_primitive_type()) {
236
5
    case TYPE_STRUCT: {
237
5
        const auto& struct_type = assert_cast<const DataTypeStruct&>(*nested_type);
238
17
        for (size_t field_idx = 0; field_idx < struct_type.get_elements().size(); ++field_idx) {
239
12
            const auto field_name = struct_type.get_element_name(field_idx);
240
12
            const auto* schema_child =
241
12
                    find_schema_child_by_path(schema_column, field_name, prefer_exact_name_match);
242
12
            auto* child = find_or_add_child(
243
12
                    column, schema_field_id_or(schema_child, cast_set<int32_t>(field_idx)),
244
12
                    schema_field_name_or(schema_child, field_name),
245
12
                    struct_type.get_element(field_idx));
246
12
            inherit_schema_metadata(child, schema_child);
247
12
            RETURN_IF_ERROR(build_nested_children_from_access_node(
248
12
                    child, child->type, project_all, path + "." + child->name, schema_child,
249
12
                    prefer_exact_name_match));
250
12
        }
251
5
        return Status::OK();
252
5
    }
253
1
    case TYPE_ARRAY: {
254
1
        const auto& array_type = assert_cast<const DataTypeArray&>(*nested_type);
255
1
        const auto* element_schema = schema_column != nullptr && !schema_column->children.empty()
256
1
                                             ? &schema_column->children[0]
257
1
                                             : nullptr;
258
1
        auto* child = find_or_add_child(column, schema_field_id_or(element_schema, 0), "element",
259
1
                                        array_type.get_nested_type());
260
1
        inherit_schema_metadata(child, element_schema);
261
1
        return build_nested_children_from_access_node(child, child->type, project_all, path + ".*",
262
1
                                                      element_schema, prefer_exact_name_match);
263
5
    }
264
1
    case TYPE_MAP: {
265
1
        const auto& map_type = assert_cast<const DataTypeMap&>(*nested_type);
266
1
        const auto* key_schema = schema_column != nullptr && !schema_column->children.empty()
267
1
                                         ? &schema_column->children[0]
268
1
                                         : nullptr;
269
1
        const auto* value_schema = schema_column != nullptr && schema_column->children.size() > 1
270
1
                                           ? &schema_column->children[1]
271
1
                                           : nullptr;
272
1
        auto* key_child = find_or_add_child(column, schema_field_id_or(key_schema, 0), "key",
273
1
                                            map_type.get_key_type());
274
1
        inherit_schema_metadata(key_child, key_schema);
275
1
        RETURN_IF_ERROR(build_nested_children_from_access_node(
276
1
                key_child, key_child->type, project_all, path + ".KEYS", key_schema,
277
1
                prefer_exact_name_match));
278
1
        auto* value_child = find_or_add_child(column, schema_field_id_or(value_schema, 1), "value",
279
1
                                              map_type.get_value_type());
280
1
        inherit_schema_metadata(value_child, value_schema);
281
1
        RETURN_IF_ERROR(build_nested_children_from_access_node(
282
1
                value_child, value_child->type, project_all, path + ".VALUES", value_schema,
283
1
                prefer_exact_name_match));
284
1
        return Status::OK();
285
1
    }
286
32
    default:
287
32
        return Status::OK();
288
39
    }
289
39
}
290
291
Status build_struct_children_from_access_node(format::ColumnDefinition* column,
292
                                              const DataTypeStruct& struct_type,
293
                                              const AccessPathNode& node, const std::string& path,
294
                                              const format::ColumnDefinition* schema_column,
295
21
                                              bool prefer_exact_name_match) {
296
21
    DORIS_CHECK(column != nullptr);
297
22
    for (const auto& [child_path, child_node] : node.children) {
298
        // Struct children are resolved by name or schema field id. We do not treat a numeric
299
        // child token as a struct ordinal, because `col.0` becomes ambiguous once the struct
300
        // evolves. Position-based access needs a separate design if it is required later.
301
22
        if (child_path == "OFFSET" || child_path == "*" || child_path == "KEYS" ||
302
22
            child_path == "VALUES") {
303
4
            return Status::NotSupported(
304
4
                    "AccessPathParser does not support access path {} for slot {}",
305
4
                    path + "." + child_path, column->name);
306
4
        }
307
308
        // Prefer the table/schema ColumnDefinition because it carries field ids and aliases.
309
        // Fallback to the struct type name only for formats without external schema metadata.
310
18
        const auto* schema_child =
311
18
                find_schema_child_by_path(schema_column, child_path, prefer_exact_name_match);
312
18
        int32_t field_id = schema_field_id(schema_child);
313
18
        std::string field_name = schema_child == nullptr ? child_path : schema_child->name;
314
18
        DataTypePtr field_type = schema_child == nullptr ? nullptr : schema_child->type;
315
18
        if (field_id < 0 || field_type == nullptr) {
316
14
            for (size_t field_idx = 0; field_idx < struct_type.get_elements().size(); ++field_idx) {
317
11
                if (to_lower(struct_type.get_element_name(field_idx)) == to_lower(field_name)) {
318
3
                    field_id = cast_set<int32_t>(field_idx);
319
3
                    field_name = struct_type.get_element_name(field_idx);
320
3
                    field_type = struct_type.get_element(field_idx);
321
3
                    break;
322
3
                }
323
11
            }
324
6
        }
325
326
18
        if (field_id < 0 || field_type == nullptr) {
327
3
            return Status::NotSupported(
328
3
                    "AccessPathParser does not support access path {} for slot {}",
329
3
                    path + "." + child_path, column->name);
330
3
        }
331
        // TODO: For TVF Parquet files without field ids, this fallback uses the struct ordinal as
332
        // the table child identifier. BY_NAME mapping should instead keep a string identifier and
333
        // let TableColumnMapper resolve the file-local child id from the Parquet schema.
334
15
        auto* child = find_or_add_child(column, field_id, field_name, field_type);
335
15
        inherit_schema_metadata(child, schema_child);
336
15
        RETURN_IF_ERROR(build_nested_children_from_access_node(
337
15
                child, child->type, child_node, path + "." + child_path, schema_child,
338
15
                prefer_exact_name_match));
339
15
    }
340
14
    return Status::OK();
341
21
}
342
343
Status build_map_children_from_access_node(format::ColumnDefinition* column,
344
                                           const DataTypeMap& map_type, const AccessPathNode& node,
345
                                           const std::string& path,
346
                                           const format::ColumnDefinition* schema_column,
347
7
                                           bool prefer_exact_name_match) {
348
7
    DORIS_CHECK(column != nullptr);
349
7
    AccessPathNode key_node;
350
7
    AccessPathNode value_node;
351
7
    bool need_key = false;
352
7
    bool need_value = false;
353
354
8
    for (const auto& [child_path, child_node] : node.children) {
355
8
        if (child_path == "OFFSET") {
356
1
            return Status::NotSupported(
357
1
                    "AccessPathParser does not support access path {} for slot {}",
358
1
                    path + "." + child_path, column->name);
359
1
        }
360
7
        if (child_path == "KEYS") {
361
1
            need_key = true;
362
1
            merge_access_path_node(&key_node, child_node);
363
1
            continue;
364
1
        }
365
6
        if (child_path == "VALUES") {
366
3
            need_key = true;
367
3
            key_node.project_all = true;
368
3
            key_node.children.clear();
369
3
            need_value = true;
370
3
            merge_access_path_node(&value_node, child_node);
371
3
            continue;
372
3
        }
373
3
        if (child_path == "*") {
374
2
            need_key = true;
375
2
            key_node.project_all = true;
376
2
            key_node.children.clear();
377
2
            need_value = true;
378
2
            merge_access_path_node(&value_node, child_node);
379
2
            continue;
380
2
        }
381
1
        return Status::NotSupported("AccessPathParser does not support access path {} for slot {}",
382
1
                                    path + "." + child_path, column->name);
383
3
    }
384
5
    if (need_key && !need_value) {
385
        // A key-only MAP projection is not independently materializable yet. FileScannerV2 can
386
        // describe a projection such as `m.KEYS`, but the downstream file block -> table block path
387
        // still builds a ColumnMap from key column + value column + offsets. If the value child is
388
        // omitted here, TableReader/ColumnMapper cannot reconstruct a valid table MAP column even
389
        // though the query only needs keys.
390
        //
391
        // Example:
392
        //   SELECT map_keys(m) FROM t;
393
        // or
394
        //   SELECT * FROM t WHERE array_contains(map_keys(m), 'k1');
395
        //
396
        // The access path only asks for `m.KEYS`, but the scan still has to read `m.VALUES` as a
397
        // temporary full projection until map materialization supports constructing a table MAP
398
        // from keys only.
399
1
        need_value = true;
400
1
        value_node.project_all = true;
401
1
        value_node.children.clear();
402
1
    }
403
404
5
    if (!need_key && !need_value) {
405
0
        return Status::OK();
406
0
    }
407
408
5
    const auto* key_schema = schema_column != nullptr && !schema_column->children.empty()
409
5
                                     ? &schema_column->children[0]
410
5
                                     : nullptr;
411
5
    const auto* value_schema = schema_column != nullptr && schema_column->children.size() > 1
412
5
                                       ? &schema_column->children[1]
413
5
                                       : nullptr;
414
5
    if (need_key) {
415
5
        auto* key_child = find_or_add_child(column, schema_field_id_or(key_schema, 0), "key",
416
5
                                            map_type.get_key_type());
417
5
        inherit_schema_metadata(key_child, key_schema);
418
5
        RETURN_IF_ERROR(build_nested_children_from_access_node(key_child, key_child->type, key_node,
419
5
                                                               path + ".KEYS", key_schema,
420
5
                                                               prefer_exact_name_match));
421
5
    }
422
5
    if (need_value) {
423
5
        auto* value_child = find_or_add_child(column, schema_field_id_or(value_schema, 1), "value",
424
5
                                              map_type.get_value_type());
425
5
        inherit_schema_metadata(value_child, value_schema);
426
5
        RETURN_IF_ERROR(build_nested_children_from_access_node(
427
5
                value_child, value_child->type, value_node, path + ".VALUES", value_schema,
428
5
                prefer_exact_name_match));
429
5
    }
430
4
    return Status::OK();
431
5
}
432
433
Status build_nested_children_from_access_node(format::ColumnDefinition* column,
434
                                              const DataTypePtr& type, const AccessPathNode& node,
435
                                              const std::string& path,
436
                                              const format::ColumnDefinition* schema_column,
437
76
                                              bool prefer_exact_name_match) {
438
76
    DORIS_CHECK(column != nullptr);
439
76
    if (node.project_all || node.children.empty()) {
440
39
        return build_all_nested_children_from_schema(column, type, path, schema_column,
441
39
                                                     prefer_exact_name_match);
442
39
    }
443
444
37
    const auto nested_type = remove_nullable(type);
445
37
    switch (nested_type->get_primitive_type()) {
446
21
    case TYPE_STRUCT:
447
21
        return build_struct_children_from_access_node(
448
21
                column, assert_cast<const DataTypeStruct&>(*nested_type), node, path, schema_column,
449
21
                prefer_exact_name_match);
450
6
    case TYPE_ARRAY: {
451
6
        if (node.children.size() != 1 || !node.children.contains("*")) {
452
2
            return Status::NotSupported(
453
2
                    "AccessPathParser does not support access path {} for slot {}", path,
454
2
                    column->name);
455
2
        }
456
4
        const auto& array_type = assert_cast<const DataTypeArray&>(*nested_type);
457
4
        const auto* element_schema = schema_column != nullptr && !schema_column->children.empty()
458
4
                                             ? &schema_column->children[0]
459
4
                                             : nullptr;
460
4
        auto* child = find_or_add_child(column, schema_field_id_or(element_schema, 0), "element",
461
4
                                        array_type.get_nested_type());
462
4
        inherit_schema_metadata(child, element_schema);
463
4
        return build_nested_children_from_access_node(child, child->type, node.children.at("*"),
464
4
                                                      path + ".*", element_schema,
465
4
                                                      prefer_exact_name_match);
466
6
    }
467
7
    case TYPE_MAP:
468
7
        return build_map_children_from_access_node(
469
7
                column, assert_cast<const DataTypeMap&>(*nested_type), node, path, schema_column,
470
7
                prefer_exact_name_match);
471
3
    case TYPE_VARIANT: {
472
        // A Variant nested below STRUCT/ARRAY/MAP owns paths relative to this terminal. Keeping
473
        // them on the nested ColumnDefinition lets ColumnMapper select the same physical leaves
474
        // as a root Variant without flattening away the surrounding container.
475
3
        column->variant_access_paths.clear();
476
3
        std::vector<std::string> variant_path;
477
3
        collect_variant_access_paths(node, &variant_path, &column->variant_access_paths);
478
3
        std::ranges::sort(column->variant_access_paths);
479
3
        column->variant_access_paths.erase(std::unique(column->variant_access_paths.begin(),
480
3
                                                       column->variant_access_paths.end()),
481
3
                                           column->variant_access_paths.end());
482
3
        return Status::OK();
483
6
    }
484
0
    default:
485
0
        return Status::NotSupported("AccessPathParser does not support access path {} for slot {}",
486
0
                                    path, column->name);
487
37
    }
488
37
}
489
490
} // namespace
491
492
Status AccessPathParser::build_nested_children(format::ColumnDefinition* column,
493
                                               const std::vector<TColumnAccessPath>& access_paths,
494
                                               const format::ColumnDefinition* schema_column,
495
43
                                               bool prefer_exact_name_match) {
496
43
    DORIS_CHECK(column != nullptr);
497
43
    if (is_scanner_materialized_virtual_column(column->name)) {
498
1
        return Status::OK();
499
1
    }
500
42
    if (remove_nullable(column->type)->get_primitive_type() == TYPE_VARIANT) {
501
5
        column->variant_access_paths.clear();
502
7
        for (const auto& access_path : access_paths) {
503
7
            if (access_path.type != TAccessPathType::DATA ||
504
7
                !access_path.__isset.data_access_path) {
505
0
                return Status::NotSupported(
506
0
                        "AccessPathParser only supports DATA access paths for Variant slot {}",
507
0
                        column->name);
508
0
            }
509
7
            const auto& path = access_path.data_access_path.path;
510
7
            if (path.empty()) {
511
                // Match the generic access-path tree: an empty DATA path denotes the whole slot
512
                // and dominates every narrower Variant path in the same request.
513
1
                column->variant_access_paths.clear();
514
1
                return Status::OK();
515
1
            }
516
6
            int32_t top_level_id = -1;
517
6
            if (to_lower(path.front()) != to_lower(column->name) &&
518
6
                (!parse_non_negative_int(path.front(), &top_level_id) ||
519
4
                 !column->has_identifier_field_id() ||
520
4
                 top_level_id != column->get_identifier_field_id())) {
521
0
                return Status::NotSupported(
522
0
                        "AccessPathParser access path {} does not match Variant slot {}",
523
0
                        access_path_to_string(path), column->name);
524
0
            }
525
6
            if (path.size() == 1) {
526
                // A whole-root access covers every subpath and must disable physical leaf pruning.
527
2
                column->variant_access_paths.clear();
528
2
                return Status::OK();
529
2
            }
530
4
            column->variant_access_paths.emplace_back(path.begin() + 1, path.end());
531
4
        }
532
2
        std::ranges::sort(column->variant_access_paths);
533
2
        column->variant_access_paths.erase(std::unique(column->variant_access_paths.begin(),
534
2
                                                       column->variant_access_paths.end()),
535
2
                                           column->variant_access_paths.end());
536
2
        return Status::OK();
537
5
    }
538
37
    if (!is_complex_type(remove_nullable(column->type)->get_primitive_type())) {
539
1
        return Status::OK();
540
1
    }
541
542
36
    AccessPathNode root;
543
    // Build tree for AccessPathNode.
544
    // For example, for access paths ["a.b", "a.c", "d"], the tree will be:
545
    // root
546
    // ├── a
547
    // │   ├── b
548
    // │   └── c
549
    // └── d
550
37
    for (const auto& access_path : access_paths) {
551
        // TODO: Support META access paths if needed. Currently AccessPathParser only supports
552
        // DATA access paths.
553
37
        if (access_path.type != TAccessPathType::DATA || !access_path.__isset.data_access_path) {
554
2
            return Status::NotSupported(
555
2
                    "AccessPathParser only supports DATA access paths for slot {}", column->name);
556
2
        }
557
35
        const auto& path = access_path.data_access_path.path;
558
35
        if (path.empty()) {
559
0
            insert_access_path(&root, path, 0);
560
0
            continue;
561
0
        }
562
35
        int32_t top_level_id = -1;
563
35
        if (to_lower(path.front()) != to_lower(column->name) &&
564
35
            (!parse_non_negative_int(path.front(), &top_level_id) ||
565
3
             !column->has_identifier_field_id() ||
566
3
             top_level_id != column->get_identifier_field_id())) {
567
2
            return Status::NotSupported("AccessPathParser access path {} does not match slot {}",
568
2
                                        access_path_to_string(path), column->name);
569
2
        }
570
33
        insert_access_path(&root, path, 1);
571
33
    }
572
    // Recursively build nested children for the column based on the AccessPathNode tree.
573
32
    return build_nested_children_from_access_node(column, column->type, root, column->name,
574
32
                                                  schema_column, prefer_exact_name_match);
575
36
}
576
577
Status AccessPathParser::build_nested_children(format::ColumnDefinition* column,
578
                                               const SlotDescriptor* slot_desc,
579
                                               const format::ColumnDefinition* schema_column,
580
0
                                               bool prefer_exact_name_match) {
581
0
    DORIS_CHECK(column != nullptr);
582
0
    DORIS_CHECK(slot_desc != nullptr);
583
0
    return build_nested_children(column, slot_desc->all_access_paths(),
584
0
                                 slot_desc->predicate_access_paths(), schema_column,
585
0
                                 prefer_exact_name_match);
586
0
}
587
588
Status AccessPathParser::build_nested_children(
589
        format::ColumnDefinition* column, const std::vector<TColumnAccessPath>& all_access_paths,
590
        const std::vector<TColumnAccessPath>& predicate_access_paths,
591
2
        const format::ColumnDefinition* schema_column, bool prefer_exact_name_match) {
592
2
    DORIS_CHECK(column != nullptr);
593
2
    auto predicate_column = *column;
594
2
    RETURN_IF_ERROR(build_nested_children(column, all_access_paths, schema_column,
595
2
                                          prefer_exact_name_match));
596
2
    column->has_predicate_access_paths = !predicate_access_paths.empty();
597
2
    column->predicate_children.clear();
598
2
    column->predicate_variant_access_paths.clear();
599
2
    if (predicate_access_paths.empty()) {
600
0
        return Status::OK();
601
0
    }
602
603
2
    predicate_column.children.clear();
604
2
    predicate_column.variant_access_paths.clear();
605
2
    predicate_column.has_predicate_access_paths = false;
606
2
    predicate_column.predicate_children.clear();
607
2
    predicate_column.predicate_variant_access_paths.clear();
608
2
    RETURN_IF_ERROR(build_nested_children(&predicate_column, predicate_access_paths, schema_column,
609
2
                                          prefer_exact_name_match));
610
2
    column->predicate_children = std::move(predicate_column.children);
611
2
    column->predicate_variant_access_paths = std::move(predicate_column.variant_access_paths);
612
2
    return Status::OK();
613
2
}
614
615
} // namespace doris