| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| #include "exec/scan/access_path_parser.h" |
| |
| #include <fmt/format.h> |
| |
| #include <algorithm> |
| #include <charconv> |
| #include <map> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| |
| #include "common/cast_set.h" |
| #include "common/consts.h" |
| #include "core/assert_cast.h" |
| #include "core/data_type/data_type.h" |
| #include "core/data_type/data_type_array.h" |
| #include "core/data_type/data_type_map.h" |
| #include "core/data_type/data_type_nullable.h" |
| #include "core/data_type/data_type_struct.h" |
| #include "runtime/descriptors.h" |
| #include "util/string_util.h" |
| |
| namespace doris { |
| namespace { |
| |
| bool is_scanner_materialized_virtual_column(const std::string& column_name) { |
| return column_name == BeConsts::ICEBERG_ROWID_COL; |
| } |
| |
| bool parse_non_negative_int(std::string_view value, int32_t* result) { |
| DORIS_CHECK(result != nullptr); |
| int32_t parsed = -1; |
| const auto* begin = value.data(); |
| const auto* end = begin + value.size(); |
| const auto [ptr, ec] = std::from_chars(begin, end, parsed); |
| if (ec != std::errc() || ptr != end || parsed < 0) { |
| return false; |
| } |
| *result = parsed; |
| return true; |
| } |
| |
| std::string access_path_to_string(const std::vector<std::string>& path) { |
| return fmt::format("{}", fmt::join(path, ".")); |
| } |
| |
| format::ColumnDefinition* find_or_add_child(format::ColumnDefinition* parent, int32_t id, |
| std::string name, DataTypePtr type) { |
| DORIS_CHECK(parent != nullptr); |
| for (auto& child : parent->children) { |
| if ((child.has_identifier_field_id() && child.get_identifier_field_id() == id) || |
| child.name == name) { |
| return &child; |
| } |
| } |
| parent->children.push_back({ |
| .identifier = Field::create_field<TYPE_INT>(id), |
| .name = std::move(name), |
| .type = std::move(type), |
| .children = {}, |
| .default_expr = nullptr, |
| .is_partition_key = false, |
| }); |
| return &parent->children.back(); |
| } |
| |
| void inherit_schema_metadata(format::ColumnDefinition* column, |
| const format::ColumnDefinition* schema_column) { |
| if (column == nullptr || schema_column == nullptr) { |
| return; |
| } |
| column->name_mapping = schema_column->name_mapping; |
| // The presence bit is part of the mapping contract: an explicit empty mapping must remain |
| // authoritative after access-path pruning instead of enabling current-name fallback. |
| column->has_name_mapping = schema_column->has_name_mapping; |
| // Initial defaults describe the logical value of fields absent from older files. Nested |
| // access-path pruning must retain them just like it retains rename metadata. |
| column->initial_default_value = schema_column->initial_default_value; |
| column->initial_default_value_is_base64 = schema_column->initial_default_value_is_base64; |
| } |
| |
| const format::ColumnDefinition* find_schema_child_by_path( |
| const format::ColumnDefinition* schema_column, const std::string& child_path, |
| bool prefer_exact_name_match) { |
| if (schema_column == nullptr) { |
| return nullptr; |
| } |
| int32_t parsed_field_id = -1; |
| if (parse_non_negative_int(child_path, &parsed_field_id)) { |
| const auto child_it = std::ranges::find_if( |
| schema_column->children, [&](const format::ColumnDefinition& child) { |
| return child.has_identifier_field_id() && |
| child.get_identifier_field_id() == parsed_field_id; |
| }); |
| return child_it == schema_column->children.end() ? nullptr : &*child_it; |
| } |
| if (!prefer_exact_name_match) { |
| const auto child_it = std::ranges::find_if(schema_column->children, [&](const auto& child) { |
| if (to_lower(child.name) == to_lower(child_path)) { |
| return true; |
| } |
| return std::ranges::any_of(child.name_mapping, [&](const std::string& alias) { |
| return to_lower(alias) == to_lower(child_path); |
| }); |
| }); |
| return child_it == schema_column->children.end() ? nullptr : &*child_it; |
| } |
| // Iceberg can reuse a historical name for a newly added sibling. Current names therefore |
| // have precedence across the entire struct; an earlier alias must not steal that access path. |
| const auto exact_it = std::ranges::find_if(schema_column->children, [&](const auto& child) { |
| return to_lower(child.name) == to_lower(child_path); |
| }); |
| if (exact_it != schema_column->children.end()) { |
| return &*exact_it; |
| } |
| const auto alias_it = std::ranges::find_if(schema_column->children, [&](const auto& child) { |
| return std::ranges::any_of(child.name_mapping, [&](const std::string& alias) { |
| return to_lower(alias) == to_lower(child_path); |
| }); |
| }); |
| return alias_it == schema_column->children.end() ? nullptr : &*alias_it; |
| } |
| |
| int32_t schema_field_id(const format::ColumnDefinition* schema_column) { |
| if (schema_column == nullptr || !schema_column->has_identifier_field_id()) { |
| return -1; |
| } |
| return schema_column->get_identifier_field_id(); |
| } |
| |
| int32_t schema_field_id_or(const format::ColumnDefinition* schema_column, int32_t fallback) { |
| const auto field_id = schema_field_id(schema_column); |
| return field_id >= 0 ? field_id : fallback; |
| } |
| |
| std::string schema_field_name_or(const format::ColumnDefinition* schema_column, |
| std::string fallback) { |
| return schema_column == nullptr || schema_column->name.empty() ? std::move(fallback) |
| : schema_column->name; |
| } |
| |
| struct AccessPathNode { |
| bool project_all = false; |
| std::map<std::string, AccessPathNode> children; |
| }; |
| |
| void merge_access_path_node(AccessPathNode* dst, const AccessPathNode& src) { |
| DORIS_CHECK(dst != nullptr); |
| if (dst->project_all) { |
| return; |
| } |
| if (src.project_all) { |
| dst->project_all = true; |
| dst->children.clear(); |
| return; |
| } |
| for (const auto& [path, child] : src.children) { |
| merge_access_path_node(&dst->children[path], child); |
| } |
| } |
| |
| void insert_access_path(AccessPathNode* root, const std::vector<std::string>& path, |
| size_t path_idx) { |
| DORIS_CHECK(root != nullptr); |
| if (root->project_all) { |
| return; |
| } |
| if (path_idx >= path.size()) { |
| root->project_all = true; |
| root->children.clear(); |
| return; |
| } |
| insert_access_path(&root->children[path[path_idx]], path, path_idx + 1); |
| } |
| |
| Status build_nested_children_from_access_node(format::ColumnDefinition* column, |
| const DataTypePtr& type, const AccessPathNode& node, |
| const std::string& path, |
| const format::ColumnDefinition* schema_column, |
| bool prefer_exact_name_match); |
| |
| // Expand a full complex-column projection into table-schema children when the table format provides |
| // an external/current schema. Without this, `SELECT complex_col` or `SELECT *` leaves |
| // ColumnDefinition::children empty, so ColumnMapper treats the root complex column as a scalar |
| // mapping and later tries to cast the old file shape to the current table shape directly. |
| // |
| // Examples: |
| // - STRUCT country/city projected from an old file STRUCT country/population/location should |
| // create children country and city, so city can be materialized as missing/default. |
| // - ARRAY<STRUCT<item, quantity>> should create the array element wrapper and then the element |
| // struct children item and quantity. |
| // - MAP<STRING, STRUCT<full_name, age>> should create semantic children key/value directly, then |
| // expand the value struct children full_name and age. Do not introduce a physical entries |
| // wrapper here: ColumnMapper and TableReader treat MAP children as [key, value]. |
| Status build_all_nested_children_from_schema(format::ColumnDefinition* column, |
| const DataTypePtr& type, const std::string& path, |
| const format::ColumnDefinition* schema_column, |
| bool prefer_exact_name_match) { |
| DORIS_CHECK(column != nullptr); |
| |
| const auto nested_type = remove_nullable(type); |
| AccessPathNode project_all; |
| project_all.project_all = true; |
| switch (nested_type->get_primitive_type()) { |
| case TYPE_STRUCT: { |
| const auto& struct_type = assert_cast<const DataTypeStruct&>(*nested_type); |
| for (size_t field_idx = 0; field_idx < struct_type.get_elements().size(); ++field_idx) { |
| const auto field_name = struct_type.get_element_name(field_idx); |
| const auto* schema_child = |
| find_schema_child_by_path(schema_column, field_name, prefer_exact_name_match); |
| auto* child = find_or_add_child( |
| column, schema_field_id_or(schema_child, cast_set<int32_t>(field_idx)), |
| schema_field_name_or(schema_child, field_name), |
| struct_type.get_element(field_idx)); |
| inherit_schema_metadata(child, schema_child); |
| RETURN_IF_ERROR(build_nested_children_from_access_node( |
| child, child->type, project_all, path + "." + child->name, schema_child, |
| prefer_exact_name_match)); |
| } |
| return Status::OK(); |
| } |
| case TYPE_ARRAY: { |
| const auto& array_type = assert_cast<const DataTypeArray&>(*nested_type); |
| const auto* element_schema = schema_column != nullptr && !schema_column->children.empty() |
| ? &schema_column->children[0] |
| : nullptr; |
| auto* child = find_or_add_child(column, schema_field_id_or(element_schema, 0), "element", |
| array_type.get_nested_type()); |
| inherit_schema_metadata(child, element_schema); |
| return build_nested_children_from_access_node(child, child->type, project_all, path + ".*", |
| element_schema, prefer_exact_name_match); |
| } |
| case TYPE_MAP: { |
| const auto& map_type = assert_cast<const DataTypeMap&>(*nested_type); |
| const auto* key_schema = schema_column != nullptr && !schema_column->children.empty() |
| ? &schema_column->children[0] |
| : nullptr; |
| const auto* value_schema = schema_column != nullptr && schema_column->children.size() > 1 |
| ? &schema_column->children[1] |
| : nullptr; |
| auto* key_child = find_or_add_child(column, schema_field_id_or(key_schema, 0), "key", |
| map_type.get_key_type()); |
| inherit_schema_metadata(key_child, key_schema); |
| RETURN_IF_ERROR(build_nested_children_from_access_node( |
| key_child, key_child->type, project_all, path + ".KEYS", key_schema, |
| prefer_exact_name_match)); |
| auto* value_child = find_or_add_child(column, schema_field_id_or(value_schema, 1), "value", |
| map_type.get_value_type()); |
| inherit_schema_metadata(value_child, value_schema); |
| RETURN_IF_ERROR(build_nested_children_from_access_node( |
| value_child, value_child->type, project_all, path + ".VALUES", value_schema, |
| prefer_exact_name_match)); |
| return Status::OK(); |
| } |
| default: |
| return Status::OK(); |
| } |
| } |
| |
| Status build_struct_children_from_access_node(format::ColumnDefinition* column, |
| const DataTypeStruct& struct_type, |
| const AccessPathNode& node, const std::string& path, |
| const format::ColumnDefinition* schema_column, |
| bool prefer_exact_name_match) { |
| DORIS_CHECK(column != nullptr); |
| for (const auto& [child_path, child_node] : node.children) { |
| // Struct children are resolved by name or schema field id. We do not treat a numeric |
| // child token as a struct ordinal, because `col.0` becomes ambiguous once the struct |
| // evolves. Position-based access needs a separate design if it is required later. |
| if (child_path == "OFFSET" || child_path == "*" || child_path == "KEYS" || |
| child_path == "VALUES") { |
| return Status::NotSupported( |
| "AccessPathParser does not support access path {} for slot {}", |
| path + "." + child_path, column->name); |
| } |
| |
| // Prefer the table/schema ColumnDefinition because it carries field ids and aliases. |
| // Fallback to the struct type name only for formats without external schema metadata. |
| const auto* schema_child = |
| find_schema_child_by_path(schema_column, child_path, prefer_exact_name_match); |
| int32_t field_id = schema_field_id(schema_child); |
| std::string field_name = schema_child == nullptr ? child_path : schema_child->name; |
| DataTypePtr field_type = schema_child == nullptr ? nullptr : schema_child->type; |
| if (field_id < 0 || field_type == nullptr) { |
| for (size_t field_idx = 0; field_idx < struct_type.get_elements().size(); ++field_idx) { |
| if (to_lower(struct_type.get_element_name(field_idx)) == to_lower(field_name)) { |
| field_id = cast_set<int32_t>(field_idx); |
| field_name = struct_type.get_element_name(field_idx); |
| field_type = struct_type.get_element(field_idx); |
| break; |
| } |
| } |
| } |
| |
| if (field_id < 0 || field_type == nullptr) { |
| return Status::NotSupported( |
| "AccessPathParser does not support access path {} for slot {}", |
| path + "." + child_path, column->name); |
| } |
| // TODO: For TVF Parquet files without field ids, this fallback uses the struct ordinal as |
| // the table child identifier. BY_NAME mapping should instead keep a string identifier and |
| // let TableColumnMapper resolve the file-local child id from the Parquet schema. |
| auto* child = find_or_add_child(column, field_id, field_name, field_type); |
| inherit_schema_metadata(child, schema_child); |
| RETURN_IF_ERROR(build_nested_children_from_access_node( |
| child, child->type, child_node, path + "." + child_path, schema_child, |
| prefer_exact_name_match)); |
| } |
| return Status::OK(); |
| } |
| |
| Status build_map_children_from_access_node(format::ColumnDefinition* column, |
| const DataTypeMap& map_type, const AccessPathNode& node, |
| const std::string& path, |
| const format::ColumnDefinition* schema_column, |
| bool prefer_exact_name_match) { |
| DORIS_CHECK(column != nullptr); |
| AccessPathNode key_node; |
| AccessPathNode value_node; |
| bool need_key = false; |
| bool need_value = false; |
| |
| for (const auto& [child_path, child_node] : node.children) { |
| if (child_path == "OFFSET") { |
| return Status::NotSupported( |
| "AccessPathParser does not support access path {} for slot {}", |
| path + "." + child_path, column->name); |
| } |
| if (child_path == "KEYS") { |
| need_key = true; |
| merge_access_path_node(&key_node, child_node); |
| continue; |
| } |
| if (child_path == "VALUES") { |
| need_key = true; |
| key_node.project_all = true; |
| key_node.children.clear(); |
| need_value = true; |
| merge_access_path_node(&value_node, child_node); |
| continue; |
| } |
| if (child_path == "*") { |
| need_key = true; |
| key_node.project_all = true; |
| key_node.children.clear(); |
| need_value = true; |
| merge_access_path_node(&value_node, child_node); |
| continue; |
| } |
| return Status::NotSupported("AccessPathParser does not support access path {} for slot {}", |
| path + "." + child_path, column->name); |
| } |
| if (need_key && !need_value) { |
| // A key-only MAP projection is not independently materializable yet. FileScannerV2 can |
| // describe a projection such as `m.KEYS`, but the downstream file block -> table block path |
| // still builds a ColumnMap from key column + value column + offsets. If the value child is |
| // omitted here, TableReader/ColumnMapper cannot reconstruct a valid table MAP column even |
| // though the query only needs keys. |
| // |
| // Example: |
| // SELECT map_keys(m) FROM t; |
| // or |
| // SELECT * FROM t WHERE array_contains(map_keys(m), 'k1'); |
| // |
| // The access path only asks for `m.KEYS`, but the scan still has to read `m.VALUES` as a |
| // temporary full projection until map materialization supports constructing a table MAP |
| // from keys only. |
| need_value = true; |
| value_node.project_all = true; |
| value_node.children.clear(); |
| } |
| |
| if (!need_key && !need_value) { |
| return Status::OK(); |
| } |
| |
| const auto* key_schema = schema_column != nullptr && !schema_column->children.empty() |
| ? &schema_column->children[0] |
| : nullptr; |
| const auto* value_schema = schema_column != nullptr && schema_column->children.size() > 1 |
| ? &schema_column->children[1] |
| : nullptr; |
| if (need_key) { |
| auto* key_child = find_or_add_child(column, schema_field_id_or(key_schema, 0), "key", |
| map_type.get_key_type()); |
| inherit_schema_metadata(key_child, key_schema); |
| RETURN_IF_ERROR(build_nested_children_from_access_node(key_child, key_child->type, key_node, |
| path + ".KEYS", key_schema, |
| prefer_exact_name_match)); |
| } |
| if (need_value) { |
| auto* value_child = find_or_add_child(column, schema_field_id_or(value_schema, 1), "value", |
| map_type.get_value_type()); |
| inherit_schema_metadata(value_child, value_schema); |
| RETURN_IF_ERROR(build_nested_children_from_access_node( |
| value_child, value_child->type, value_node, path + ".VALUES", value_schema, |
| prefer_exact_name_match)); |
| } |
| return Status::OK(); |
| } |
| |
| Status build_nested_children_from_access_node(format::ColumnDefinition* column, |
| const DataTypePtr& type, const AccessPathNode& node, |
| const std::string& path, |
| const format::ColumnDefinition* schema_column, |
| bool prefer_exact_name_match) { |
| DORIS_CHECK(column != nullptr); |
| if (node.project_all || node.children.empty()) { |
| return build_all_nested_children_from_schema(column, type, path, schema_column, |
| prefer_exact_name_match); |
| } |
| |
| const auto nested_type = remove_nullable(type); |
| switch (nested_type->get_primitive_type()) { |
| case TYPE_STRUCT: |
| return build_struct_children_from_access_node( |
| column, assert_cast<const DataTypeStruct&>(*nested_type), node, path, schema_column, |
| prefer_exact_name_match); |
| case TYPE_ARRAY: { |
| if (node.children.size() != 1 || !node.children.contains("*")) { |
| return Status::NotSupported( |
| "AccessPathParser does not support access path {} for slot {}", path, |
| column->name); |
| } |
| const auto& array_type = assert_cast<const DataTypeArray&>(*nested_type); |
| const auto* element_schema = schema_column != nullptr && !schema_column->children.empty() |
| ? &schema_column->children[0] |
| : nullptr; |
| auto* child = find_or_add_child(column, schema_field_id_or(element_schema, 0), "element", |
| array_type.get_nested_type()); |
| inherit_schema_metadata(child, element_schema); |
| return build_nested_children_from_access_node(child, child->type, node.children.at("*"), |
| path + ".*", element_schema, |
| prefer_exact_name_match); |
| } |
| case TYPE_MAP: |
| return build_map_children_from_access_node( |
| column, assert_cast<const DataTypeMap&>(*nested_type), node, path, schema_column, |
| prefer_exact_name_match); |
| default: |
| return Status::NotSupported("AccessPathParser does not support access path {} for slot {}", |
| path, column->name); |
| } |
| } |
| |
| } // namespace |
| |
| Status AccessPathParser::build_nested_children(format::ColumnDefinition* column, |
| const std::vector<TColumnAccessPath>& access_paths, |
| const format::ColumnDefinition* schema_column, |
| bool prefer_exact_name_match) { |
| DORIS_CHECK(column != nullptr); |
| if (is_scanner_materialized_virtual_column(column->name)) { |
| return Status::OK(); |
| } |
| if (!is_complex_type(remove_nullable(column->type)->get_primitive_type())) { |
| return Status::OK(); |
| } |
| |
| AccessPathNode root; |
| // Build tree for AccessPathNode. |
| // For example, for access paths ["a.b", "a.c", "d"], the tree will be: |
| // root |
| // ├── a |
| // │ ├── b |
| // │ └── c |
| // └── d |
| for (const auto& access_path : access_paths) { |
| // TODO: Support META access paths if needed. Currently AccessPathParser only supports |
| // DATA access paths. |
| if (access_path.type != TAccessPathType::DATA || !access_path.__isset.data_access_path) { |
| return Status::NotSupported( |
| "AccessPathParser only supports DATA access paths for slot {}", column->name); |
| } |
| const auto& path = access_path.data_access_path.path; |
| if (path.empty()) { |
| insert_access_path(&root, path, 0); |
| continue; |
| } |
| int32_t top_level_id = -1; |
| if (to_lower(path.front()) != to_lower(column->name) && |
| (!parse_non_negative_int(path.front(), &top_level_id) || |
| !column->has_identifier_field_id() || |
| top_level_id != column->get_identifier_field_id())) { |
| return Status::NotSupported("AccessPathParser access path {} does not match slot {}", |
| access_path_to_string(path), column->name); |
| } |
| insert_access_path(&root, path, 1); |
| } |
| // Recursively build nested children for the column based on the AccessPathNode tree. |
| return build_nested_children_from_access_node(column, column->type, root, column->name, |
| schema_column, prefer_exact_name_match); |
| } |
| |
| Status AccessPathParser::build_nested_children(format::ColumnDefinition* column, |
| const SlotDescriptor* slot_desc, |
| const format::ColumnDefinition* schema_column, |
| bool prefer_exact_name_match) { |
| DORIS_CHECK(column != nullptr); |
| DORIS_CHECK(slot_desc != nullptr); |
| return build_nested_children(column, slot_desc->all_access_paths(), schema_column, |
| prefer_exact_name_match); |
| } |
| |
| } // namespace doris |