Coverage Report

Created: 2026-07-25 15:41

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format_v2/column_mapper_nested.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 "format_v2/column_mapper_nested.h"
19
20
#include <algorithm>
21
#include <cstdint>
22
#include <memory>
23
#include <optional>
24
#include <utility>
25
26
#include "common/cast_set.h"
27
#include "common/exception.h"
28
#include "core/assert_cast.h"
29
#include "core/data_type/data_type_nullable.h"
30
#include "core/data_type/data_type_struct.h"
31
#include "core/data_type/primitive_type.h"
32
#include "exprs/vexpr.h"
33
#include "format_v2/expr/cast.h"
34
#include "gen_cpp/Exprs_types.h"
35
36
namespace doris::format {
37
38
namespace {
39
40
674
static bool is_cast_expr(const VExprSPtr& expr) {
41
674
    return dynamic_cast<const Cast*>(expr.get()) != nullptr;
42
674
}
43
44
30
static bool is_signed_integer_type(PrimitiveType type) {
45
30
    switch (type) {
46
0
    case TYPE_TINYINT:
47
0
    case TYPE_SMALLINT:
48
10
    case TYPE_INT:
49
22
    case TYPE_BIGINT:
50
22
    case TYPE_LARGEINT:
51
22
        return true;
52
8
    default:
53
8
        return false;
54
30
    }
55
30
}
56
57
20
static int primitive_integer_width(PrimitiveType type) {
58
20
    switch (type) {
59
0
    case TYPE_TINYINT:
60
0
        return 1;
61
0
    case TYPE_SMALLINT:
62
0
        return 2;
63
10
    case TYPE_INT:
64
10
        return 4;
65
10
    case TYPE_BIGINT:
66
10
        return 8;
67
0
    case TYPE_LARGEINT:
68
0
        return 16;
69
0
    default:
70
0
        return 0;
71
20
    }
72
20
}
73
74
10
static bool is_decimal_type(PrimitiveType type) {
75
10
    switch (type) {
76
8
    case TYPE_DECIMAL32:
77
8
    case TYPE_DECIMAL64:
78
8
    case TYPE_DECIMALV2:
79
8
    case TYPE_DECIMAL128I:
80
8
    case TYPE_DECIMAL256:
81
8
        return true;
82
2
    default:
83
2
        return false;
84
10
    }
85
10
}
86
87
static bool is_order_preserving_safe_cast(const DataTypePtr& from_type,
88
22
                                          const DataTypePtr& to_type) {
89
22
    if (from_type == nullptr || to_type == nullptr) {
90
0
        return false;
91
0
    }
92
22
    const auto from_nested_type = remove_nullable(from_type);
93
22
    const auto to_nested_type = remove_nullable(to_type);
94
22
    if (from_nested_type->equals(*to_nested_type)) {
95
4
        return true;
96
4
    }
97
98
18
    const auto from_primitive_type = from_nested_type->get_primitive_type();
99
18
    const auto to_primitive_type = to_nested_type->get_primitive_type();
100
18
    if (is_signed_integer_type(from_primitive_type) && is_signed_integer_type(to_primitive_type)) {
101
10
        return primitive_integer_width(to_primitive_type) >=
102
10
               primitive_integer_width(from_primitive_type);
103
10
    }
104
8
    if (from_primitive_type == TYPE_FLOAT && to_primitive_type == TYPE_DOUBLE) {
105
2
        return true;
106
2
    }
107
6
    if (is_decimal_type(from_primitive_type) && is_decimal_type(to_primitive_type)) {
108
4
        return from_nested_type->get_scale() == to_nested_type->get_scale() &&
109
4
               to_nested_type->get_precision() >= from_nested_type->get_precision();
110
4
    }
111
2
    return false;
112
6
}
113
114
224
static bool parse_struct_child_selector(const VExprSPtr& expr, StructChildSelector* selector) {
115
224
    DORIS_CHECK(selector != nullptr);
116
224
    if (expr == nullptr || !expr->is_literal()) {
117
2
        return false;
118
2
    }
119
222
    const Field field = literal_field(expr);
120
222
    switch (field.get_type()) {
121
198
    case TYPE_STRING:
122
198
    case TYPE_CHAR:
123
198
    case TYPE_VARCHAR:
124
198
        selector->by_name = true;
125
198
        selector->name = std::string(field.as_string_view());
126
198
        return true;
127
4
    case TYPE_BOOLEAN:
128
4
        selector->by_name = false;
129
4
        selector->ordinal = field.get<TYPE_BOOLEAN>() ? 1 : 0;
130
4
        return selector->ordinal > 0;
131
2
    case TYPE_TINYINT:
132
2
        selector->by_name = false;
133
2
        if (field.get<TYPE_TINYINT>() <= 0) {
134
0
            return false;
135
0
        }
136
2
        selector->ordinal = cast_set<size_t>(field.get<TYPE_TINYINT>());
137
2
        return true;
138
2
    case TYPE_SMALLINT:
139
2
        selector->by_name = false;
140
2
        if (field.get<TYPE_SMALLINT>() <= 0) {
141
0
            return false;
142
0
        }
143
2
        selector->ordinal = cast_set<size_t>(field.get<TYPE_SMALLINT>());
144
2
        return true;
145
10
    case TYPE_INT:
146
10
        selector->by_name = false;
147
10
        if (field.get<TYPE_INT>() <= 0) {
148
4
            return false;
149
4
        }
150
6
        selector->ordinal = cast_set<size_t>(field.get<TYPE_INT>());
151
6
        return true;
152
2
    case TYPE_BIGINT:
153
2
        selector->by_name = false;
154
2
        if (field.get<TYPE_BIGINT>() <= 0) {
155
0
            return false;
156
0
        }
157
2
        selector->ordinal = cast_set<size_t>(field.get<TYPE_BIGINT>());
158
2
        return true;
159
4
    default:
160
4
        return false;
161
222
    }
162
222
}
163
164
888
static bool extract_nested_struct_path(const VExprSPtr& expr, NestedStructPath* path) {
165
888
    DORIS_CHECK(path != nullptr);
166
888
    if (!is_struct_element_expr(expr)) {
167
664
        return false;
168
664
    }
169
170
    // Process for element_at(struct, 'field') or element_at(struct, 1) expression.
171
224
    StructChildSelector selector;
172
224
    if (!parse_struct_child_selector(expr->children()[1], &selector)) {
173
12
        return false;
174
12
    }
175
176
212
    const auto& parent = expr->children()[0];
177
212
    if (parent->is_slot_ref()) {
178
166
        const auto* slot_ref = assert_cast<const VSlotRef*>(parent.get());
179
166
        path->root_global_index = slot_ref_global_index(*slot_ref);
180
166
        path->selectors.clear();
181
166
        path->selectors.push_back(std::move(selector));
182
166
        return true;
183
166
    }
184
185
    // Process for element_at(element_at(struct<struct>, 'field'), 'field') or
186
    // element_at(element_at(struct<struct>, 1), 1) expression.
187
46
    if (!extract_nested_struct_path(parent, path)) {
188
6
        return false;
189
6
    }
190
40
    path->selectors.push_back(std::move(selector));
191
40
    return true;
192
46
}
193
194
776
static bool extract_nested_struct_path_for_pruning(const VExprSPtr& expr, NestedStructPath* path) {
195
776
    DORIS_CHECK(path != nullptr);
196
    // Simple `ELEMENT_AT`
197
776
    if (extract_nested_struct_path(expr, path)) {
198
102
        return true;
199
102
    }
200
201
    // `ELEMENT_AT` with `CAST`
202
674
    if (!is_cast_expr(expr) || expr->get_num_children() != 1) {
203
652
        return false;
204
652
    }
205
22
    const auto& child = expr->children()[0];
206
22
    if (!is_order_preserving_safe_cast(child->data_type(), expr->data_type())) {
207
8
        return false;
208
8
    }
209
    // A safe widening cast is null-preserving and keeps the comparison ordering of the nested
210
    // primitive leaf, so file-layer pruning can target the original leaf statistics. The row-level
211
    // filter still evaluates the original cast expression after read.
212
14
    return extract_nested_struct_path_for_pruning(child, path);
213
22
}
214
215
static const ColumnDefinition* resolve_file_child(const std::vector<ColumnDefinition>& children,
216
80
                                                  const StructChildSelector& selector) {
217
80
    if (selector.by_name) {
218
100
        const auto child_it = std::ranges::find_if(children, [&](const ColumnDefinition& child) {
219
100
            return child.name == selector.name;
220
100
        });
221
76
        return child_it == children.end() ? nullptr : &*child_it;
222
76
    }
223
4
    if (selector.ordinal == 0 || selector.ordinal > children.size()) {
224
0
        return nullptr;
225
0
    }
226
4
    return &children[selector.ordinal - 1];
227
4
}
228
229
268
static const DataTypeStruct* struct_type_or_null(const DataTypePtr& type) {
230
268
    if (type == nullptr) {
231
0
        return nullptr;
232
0
    }
233
268
    const auto nested_type = remove_nullable(type);
234
268
    if (nested_type->get_primitive_type() != TYPE_STRUCT) {
235
24
        return nullptr;
236
24
    }
237
244
    return assert_cast<const DataTypeStruct*>(nested_type.get());
238
268
}
239
240
static std::optional<int32_t> struct_child_index(const ColumnMapping& mapping,
241
126
                                                 const StructChildSelector& selector) {
242
126
    const auto* struct_type = struct_type_or_null(mapping.table_type);
243
126
    if (struct_type == nullptr) {
244
12
        return std::nullopt;
245
12
    }
246
114
    if (selector.by_name) {
247
114
        const auto position = struct_type->try_get_position_by_name(selector.name);
248
114
        if (!position.has_value()) {
249
56
            return std::nullopt;
250
56
        }
251
58
        return cast_set<int32_t>(*position);
252
114
    }
253
0
    if (selector.ordinal == 0 || selector.ordinal > struct_type->get_elements().size()) {
254
0
        return std::nullopt;
255
0
    }
256
0
    return cast_set<int32_t>(selector.ordinal - 1);
257
0
}
258
259
// Get the global child index for a child mapping. If the mapping's table type is struct, resolve
260
// the child index by the child mapping's table column name; otherwise, use the fallback child index.
261
static int32_t child_mapping_global_index(const ColumnMapping& mapping,
262
                                          const ColumnMapping& child_mapping,
263
70
                                          size_t fallback_child_idx) {
264
70
    const auto* struct_type = struct_type_or_null(mapping.table_type);
265
70
    if (struct_type == nullptr) {
266
0
        return cast_set<int32_t>(fallback_child_idx);
267
0
    }
268
70
    const auto position = struct_type->try_get_position_by_name(child_mapping.table_column_name);
269
70
    DORIS_CHECK(position.has_value()) << "Cannot find child '" << child_mapping.table_column_name
270
0
                                      << "' in table type " << mapping.table_type->get_name();
271
70
    return cast_set<int32_t>(*position);
272
70
}
273
274
static const ColumnMapping* resolve_mapped_child(const ColumnMapping& mapping,
275
58
                                                 int32_t global_child_index) {
276
74
    for (size_t child_idx = 0; child_idx < mapping.child_mappings.size(); ++child_idx) {
277
70
        const auto& child_mapping = mapping.child_mappings[child_idx];
278
70
        if (child_mapping_global_index(mapping, child_mapping, child_idx) == global_child_index) {
279
54
            return &child_mapping;
280
54
        }
281
70
    }
282
4
    return nullptr;
283
58
}
284
285
enum class NestedProjectionResolveResult {
286
    RESOLVED,
287
    NOT_REPRESENTED,
288
    MISSING_FILE_CHILD,
289
};
290
291
// Resolve a table-side nested struct path through the existing ColumnMapping tree and build the
292
// corresponding file-local projection. For example, if table column `s` has children
293
// `{a, renamed_b}` and file column `s` has children `{a, b}`, the filter path
294
// `struct_element(s, 'renamed_b')` is resolved to the file projection `s -> b` by following the
295
// child mapping instead of matching the table child name against the file schema. Return
296
// MISSING_FILE_CHILD when ColumnMapping explicitly says a table child is absent from this file; in
297
// that case callers must not fall back to schema-name lookup, because Iceberg can drop a field and
298
// later add a different field with the same name.
299
static NestedProjectionResolveResult resolve_nested_projection_with_mapping(
300
        const NestedStructPath& path, const std::vector<ColumnMapping>& mappings,
301
122
        LocalColumnIndex* root_projection) {
302
122
    DORIS_CHECK(root_projection != nullptr);
303
122
    *root_projection = {};
304
122
    if (path.selectors.empty()) {
305
0
        return NestedProjectionResolveResult::NOT_REPRESENTED;
306
0
    }
307
126
    const auto mapping_it = std::ranges::find_if(mappings, [&](const ColumnMapping& mapping) {
308
126
        return mapping.global_index == path.root_global_index;
309
126
    });
310
122
    if (mapping_it == mappings.end() || !mapping_it->file_local_id.has_value()) {
311
0
        return NestedProjectionResolveResult::NOT_REPRESENTED;
312
0
    }
313
314
122
    *root_projection = LocalColumnIndex::partial_local(*mapping_it->file_local_id);
315
122
    auto* current_projection = root_projection;
316
122
    const auto* current_mapping = &*mapping_it;
317
318
    // Traverse the ColumnMapping tree according to the table-side struct selectors and emit the
319
    // corresponding file-local child ids. A missing child mapping means this predicate-only path
320
    // may need schema fallback; an existing child mapping without a file id means the table child
321
    // is genuinely absent from this file and must stay above the file reader.
322
176
    for (size_t selector_idx = 0; selector_idx < path.selectors.size(); ++selector_idx) {
323
126
        const auto global_child_index =
324
126
                struct_child_index(*current_mapping, path.selectors[selector_idx]);
325
126
        if (!global_child_index.has_value()) {
326
68
            *root_projection = {};
327
68
            return NestedProjectionResolveResult::NOT_REPRESENTED;
328
68
        }
329
58
        const auto* child_mapping = resolve_mapped_child(*current_mapping, *global_child_index);
330
58
        if (child_mapping == nullptr) {
331
4
            *root_projection = {};
332
4
            return NestedProjectionResolveResult::NOT_REPRESENTED;
333
4
        }
334
54
        if (!child_mapping->file_local_id.has_value()) {
335
0
            *root_projection = {};
336
0
            return NestedProjectionResolveResult::MISSING_FILE_CHILD;
337
0
        }
338
339
54
        auto child_projection = LocalColumnIndex::partial_local(*child_mapping->file_local_id);
340
54
        child_projection.project_all_children = selector_idx + 1 == path.selectors.size();
341
54
        current_projection->children.push_back(std::move(child_projection));
342
54
        current_projection = &current_projection->children.back();
343
54
        current_mapping = child_mapping;
344
54
    }
345
50
    return NestedProjectionResolveResult::RESOLVED;
346
122
}
347
348
72
static bool table_root_is_struct(const ColumnMapping& mapping) {
349
72
    return struct_type_or_null(mapping.table_type) != nullptr;
350
72
}
351
352
56
static const std::vector<ColumnDefinition>& scan_file_children(const ColumnMapping& mapping) {
353
56
    return !mapping.projected_file_children.empty() ? mapping.projected_file_children
354
56
                                                    : mapping.original_file_children;
355
56
}
356
357
static bool collect_file_child_names_from_projection(const std::vector<ColumnDefinition>& children,
358
                                                     const LocalColumnIndex& projection,
359
                                                     std::vector<std::string>* file_child_names,
360
122
                                                     std::vector<DataTypePtr>* file_child_types) {
361
122
    DORIS_CHECK(file_child_names != nullptr);
362
122
    DORIS_CHECK(file_child_types != nullptr);
363
158
    const auto child_it = std::ranges::find_if(children, [&](const ColumnDefinition& child) {
364
158
        return child.file_local_id() == projection.local_id();
365
158
    });
366
122
    if (child_it == children.end()) {
367
0
        return false;
368
0
    }
369
122
    file_child_names->push_back(child_it->name);
370
122
    file_child_types->push_back(child_it->type);
371
122
    if (projection.children.empty()) {
372
110
        return true;
373
110
    }
374
12
    if (projection.children.size() != 1) {
375
0
        return false;
376
0
    }
377
12
    return collect_file_child_names_from_projection(child_it->children, projection.children[0],
378
12
                                                    file_child_names, file_child_types);
379
12
}
380
381
} // namespace
382
383
SplitLocalFileLiteral::SplitLocalFileLiteral(const DataTypePtr& file_type, const Field& file_field,
384
                                             DataTypePtr original_type, Field original_field)
385
76
        : VLiteral(file_type, file_field),
386
76
          _original_type(std::move(original_type)),
387
76
          _original_field(std::move(original_field)) {}
388
389
558
GlobalIndex slot_ref_global_index(const VSlotRef& slot_ref) {
390
558
    DORIS_CHECK(slot_ref.column_id() >= 0);
391
558
    return GlobalIndex(cast_set<size_t>(slot_ref.column_id()));
392
558
}
393
394
1.35k
bool is_struct_element_expr(const VExprSPtr& expr) {
395
1.35k
    if (expr == nullptr || expr->get_num_children() != 2) {
396
746
        return false;
397
746
    }
398
608
    const auto& function_name = expr->fn().name.function_name;
399
608
    if (function_name == "struct_element") {
400
262
        return true;
401
262
    }
402
346
    if (function_name != "element_at") {
403
226
        return false;
404
226
    }
405
120
    const auto& parent_type = expr->children()[0]->data_type();
406
120
    return parent_type != nullptr &&
407
120
           remove_nullable(parent_type)->get_primitive_type() == TYPE_STRUCT;
408
346
}
409
410
362
Field literal_field(const VExprSPtr& literal_expr) {
411
362
    DORIS_CHECK(literal_expr != nullptr);
412
362
    DORIS_CHECK(literal_expr->is_literal());
413
362
    const auto* literal = dynamic_cast<const VLiteral*>(literal_expr.get());
414
362
    DORIS_CHECK(literal != nullptr);
415
362
    Field field;
416
362
    literal->get_column_ptr()->get(0, field);
417
362
    return field;
418
362
}
419
420
bool resolve_nested_struct_path_for_file(const NestedStructPath& path,
421
                                         const std::vector<ColumnMapping>& mappings,
422
                                         ResolvedNestedStructPath* resolved,
423
122
                                         bool require_scan_projection) {
424
122
    DORIS_CHECK(resolved != nullptr);
425
122
    *resolved = {};
426
126
    const auto mapping_it = std::ranges::find_if(mappings, [&](const ColumnMapping& mapping) {
427
126
        return mapping.global_index == path.root_global_index;
428
126
    });
429
122
    if (mapping_it == mappings.end() || !mapping_it->file_local_id.has_value() ||
430
122
        path.selectors.empty()) {
431
0
        return false;
432
0
    }
433
434
    // Prefer ColumnMapping over schema-name lookup. This is the only path that can correctly
435
    // localize renamed Iceberg fields: a table filter `element_at(s, 'renamed_b')` must become a
436
    // file filter on physical child `b`, even if the old file type is `STRUCT<b ...>`.
437
122
    const auto mapping_result =
438
122
            resolve_nested_projection_with_mapping(path, mappings, &resolved->file_projection);
439
122
    if (mapping_result == NestedProjectionResolveResult::MISSING_FILE_CHILD) {
440
0
        return false;
441
0
    }
442
122
    if (mapping_result == NestedProjectionResolveResult::NOT_REPRESENTED) {
443
72
        if (!table_root_is_struct(*mapping_it)) {
444
12
            return false;
445
12
        }
446
60
        LocalColumnIndex child_projection;
447
60
        if (!build_file_child_projection_from_schema(mapping_it->original_file_children,
448
60
                                                     path.selectors, &child_projection)
449
60
                     .ok() ||
450
60
            child_projection.local_id() < 0) {
451
0
            return false;
452
0
        }
453
60
        resolved->file_projection = LocalColumnIndex::partial_local(*mapping_it->file_local_id);
454
60
        resolved->file_projection.children.push_back(std::move(child_projection));
455
60
    }
456
457
110
    if (resolved->file_projection.children.size() != 1) {
458
0
        *resolved = {};
459
0
        return false;
460
0
    }
461
    // When rewriting the final localized element_at chain, it executes on the file column produced
462
    // by this scan, so the intermediate return types must match the projected file shape, not the
463
    // full historical file schema. Example:
464
    //   SELECT s.c WHERE element_at(element_at(s, 'b'), 'cc') LIKE 'NestedC%'
465
    // reads only b.cc and c; the inner element_at(s, 'b') returns Struct(cc), not
466
    // Struct(cc, new_dd).
467
    //
468
    // Earlier projection collection also calls this resolver before filter-only children have been
469
    // merged into the scan projection. That phase only needs the file path, so it still resolves
470
    // names/types from the original file schema.
471
110
    const auto& child_source = require_scan_projection ? scan_file_children(*mapping_it)
472
110
                                                       : mapping_it->original_file_children;
473
110
    if (!collect_file_child_names_from_projection(
474
110
                child_source, resolved->file_projection.children[0], &resolved->file_child_names,
475
110
                &resolved->file_child_types) ||
476
110
        resolved->file_child_names.size() != path.selectors.size() ||
477
110
        resolved->file_child_types.size() != path.selectors.size()) {
478
0
        *resolved = {};
479
0
        return false;
480
0
    }
481
110
    return true;
482
110
}
483
484
bool resolve_nested_struct_expr_for_file(const VExprSPtr& expr,
485
                                         const std::vector<ColumnMapping>& mappings,
486
66
                                         ResolvedNestedStructPath* resolved) {
487
66
    DORIS_CHECK(resolved != nullptr);
488
66
    NestedStructPath path;
489
66
    if (!extract_nested_struct_path(expr, &path)) {
490
2
        *resolved = {};
491
2
        return false;
492
2
    }
493
64
    return resolve_nested_struct_path_for_file(path, mappings, resolved, true);
494
66
}
495
496
// Collect nested struct leaf references that can be turned into file-reader projections. For
497
// example, from `s.a > 1 AND element_at(s, 'b') = 2`, this records two paths rooted at `s`:
498
// `s -> a` and `s -> b`. Non-struct expressions are traversed recursively, while a recognized
499
// struct path is emitted once so the caller can merge it into the scan projection for that
500
// top-level file column.
501
764
void collect_nested_struct_paths(const VExprSPtr& expr, std::vector<NestedStructPath>* paths) {
502
764
    DORIS_CHECK(paths != nullptr);
503
764
    if (expr == nullptr) {
504
2
        return;
505
2
    }
506
762
    NestedStructPath path;
507
762
    if (extract_nested_struct_path_for_pruning(expr, &path)) {
508
102
        paths->push_back(std::move(path));
509
102
        return;
510
102
    }
511
660
    for (const auto& child : expr->children()) {
512
504
        collect_nested_struct_paths(child, paths);
513
504
    }
514
660
}
515
516
std::vector<const ColumnMapping*> present_child_mappings_in_file_order(
517
510
        const std::vector<ColumnMapping>& child_mappings) {
518
510
    std::vector<const ColumnMapping*> result;
519
510
    result.reserve(child_mappings.size());
520
510
    for (const auto& child_mapping : child_mappings) {
521
388
        if (child_mapping.file_local_id.has_value()) {
522
312
            result.push_back(&child_mapping);
523
312
        }
524
388
    }
525
510
    std::ranges::sort(result, [](const ColumnMapping* lhs, const ColumnMapping* rhs) {
526
104
        DORIS_CHECK(lhs->file_local_id.has_value());
527
104
        DORIS_CHECK(rhs->file_local_id.has_value());
528
104
        return *lhs->file_local_id < *rhs->file_local_id;
529
104
    });
530
510
    return result;
531
510
}
532
533
// Build the nested child projection under a top-level file column by walking file schema children
534
// directly. The returned projection does not include the root column id; callers attach it under a
535
// `LocalColumnIndex::partial_local(root_id)` when merging into the scan request.
536
Status build_file_child_projection_from_schema(const std::vector<ColumnDefinition>& children,
537
                                               std::span<const StructChildSelector> selectors,
538
80
                                               LocalColumnIndex* projection) {
539
80
    DORIS_CHECK(projection != nullptr);
540
80
    if (selectors.empty()) {
541
0
        return Status::InvalidArgument("Nested struct selector path is empty");
542
0
    }
543
80
    const auto* child = resolve_file_child(children, selectors.front());
544
80
    if (child == nullptr) {
545
0
        return Status::OK();
546
0
    }
547
80
    *projection = LocalColumnIndex::local(child->file_local_id());
548
80
    projection->project_all_children = selectors.size() == 1;
549
80
    projection->children.clear();
550
80
    if (selectors.size() == 1) {
551
66
        return Status::OK();
552
66
    }
553
14
    if (child->children.empty() ||
554
14
        remove_nullable(child->type)->get_primitive_type() != TYPE_STRUCT) {
555
0
        *projection = LocalColumnIndex {};
556
0
        return Status::OK();
557
0
    }
558
14
    LocalColumnIndex child_projection;
559
14
    RETURN_IF_ERROR(build_file_child_projection_from_schema(child->children, selectors.subspan(1),
560
14
                                                            &child_projection));
561
14
    if (child_projection.local_id() < 0) {
562
0
        *projection = LocalColumnIndex {};
563
0
        return Status::OK();
564
0
    }
565
14
    projection->children.push_back(std::move(child_projection));
566
14
    return Status::OK();
567
14
}
568
569
} // namespace doris::format