blob: 1c48b9285d9eda8d9baf4cf7733c8fa5848eb257 [file]
// 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