| /* |
| * 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 "iceberg/name_mapping.h" |
| |
| #include <format> |
| #include <sstream> |
| |
| #include "iceberg/util/formatter_internal.h" |
| #include "iceberg/util/macros.h" |
| #include "iceberg/util/visit_type.h" |
| |
| namespace iceberg { |
| |
| namespace { |
| |
| // Helper function to join a list of field names with a dot |
| std::string JoinByDot(std::span<const std::string> parts) { |
| std::stringstream ss; |
| for (size_t i = 0; i < parts.size(); ++i) { |
| if (i > 0) { |
| ss << "."; |
| } |
| ss << parts[i]; |
| } |
| return ss.str(); |
| } |
| |
| // Helper class to recursively index MappedField by field id |
| struct IndexByIdVisitor { |
| std::unordered_map<int32_t, MappedFieldConstRef> field_by_id; |
| |
| void Visit(const MappedField& field) { |
| if (field.field_id.has_value()) { |
| field_by_id.emplace(field.field_id.value(), std::cref(field)); |
| } |
| if (field.nested_mapping != nullptr) { |
| Visit(*field.nested_mapping); |
| } |
| } |
| |
| void Visit(const MappedFields& fields) { |
| for (const auto& field : fields.fields()) { |
| Visit(field); |
| } |
| } |
| |
| void Visit(const NameMapping& name_mapping) { Visit(name_mapping.AsMappedFields()); } |
| }; |
| |
| // Helper class to recursively index MappedField by field name |
| struct IndexByNameVisitor { |
| std::unordered_map<std::string, MappedFieldConstRef> field_by_name; |
| |
| void Visit(const MappedField& field) { |
| for (const auto& name : field.names) { |
| field_by_name.emplace(name, std::cref(field)); |
| } |
| |
| if (field.nested_mapping != nullptr) { |
| IndexByNameVisitor nested_visitor; |
| nested_visitor.Visit(*field.nested_mapping); |
| |
| for (const auto& [name, mapped_field] : nested_visitor.field_by_name) { |
| for (const auto& prefix : field.names) { |
| std::vector<std::string> parts = {prefix, name}; |
| field_by_name.emplace(JoinByDot(parts), std::cref(mapped_field)); |
| } |
| } |
| } |
| } |
| |
| void Visit(const MappedFields& fields) { |
| for (const auto& field : fields.fields()) { |
| Visit(field); |
| } |
| } |
| |
| void Visit(const NameMapping& name_mapping) { Visit(name_mapping.AsMappedFields()); } |
| }; |
| |
| } // namespace |
| |
| MappedFields::MappedFields(std::vector<MappedField> fields) |
| : fields_(std::move(fields)) {} |
| |
| std::unique_ptr<MappedFields> MappedFields::Make(std::vector<MappedField> fields) { |
| return std::unique_ptr<MappedFields>(new MappedFields(std::move(fields))); |
| } |
| |
| std::optional<MappedFieldConstRef> MappedFields::Field(int32_t id) const { |
| const auto& id_to_field = LazyIdToField(); |
| if (auto it = id_to_field.find(id); it != id_to_field.cend()) { |
| return it->second; |
| } |
| return std::nullopt; |
| } |
| |
| std::optional<int32_t> MappedFields::Id(std::string_view name) const { |
| const auto& name_to_id = LazyNameToId(); |
| if (auto it = name_to_id.find(name); it != name_to_id.cend()) { |
| return it->second; |
| } |
| return std::nullopt; |
| } |
| |
| size_t MappedFields::Size() const { return fields_.size(); } |
| |
| std::span<const MappedField> MappedFields::fields() const { return fields_; } |
| |
| const std::unordered_map<std::string_view, int32_t>& MappedFields::LazyNameToId() const { |
| if (name_to_id_.empty() && !fields_.empty()) { |
| for (const auto& field : fields_) { |
| for (const auto& name : field.names) { |
| if (field.field_id.has_value()) { |
| name_to_id_.emplace(name, field.field_id.value()); |
| } |
| } |
| } |
| } |
| return name_to_id_; |
| } |
| |
| const std::unordered_map<int32_t, MappedFieldConstRef>& MappedFields::LazyIdToField() |
| const { |
| if (id_to_field_.empty() && !fields_.empty()) { |
| for (const auto& field : fields_) { |
| if (field.field_id.has_value()) { |
| id_to_field_.emplace(field.field_id.value(), std::cref(field)); |
| } |
| } |
| } |
| return id_to_field_; |
| } |
| |
| NameMapping::NameMapping(std::unique_ptr<MappedFields> mapping) |
| : mapping_(std::move(mapping)) {} |
| |
| std::optional<MappedFieldConstRef> NameMapping::Find(int32_t id) const { |
| const auto& fields_by_id = LazyFieldsById(); |
| if (auto iter = fields_by_id.find(id); iter != fields_by_id.cend()) { |
| return iter->second; |
| } |
| return std::nullopt; |
| } |
| |
| std::optional<MappedFieldConstRef> NameMapping::Find( |
| std::span<const std::string> names) const { |
| if (names.empty()) { |
| return std::nullopt; |
| } |
| return Find(JoinByDot(names)); |
| } |
| |
| std::optional<MappedFieldConstRef> NameMapping::Find(const std::string& name) const { |
| const auto& fields_by_name = LazyFieldsByName(); |
| if (auto iter = fields_by_name.find(name); iter != fields_by_name.cend()) { |
| return iter->second; |
| } |
| return std::nullopt; |
| } |
| |
| const MappedFields& NameMapping::AsMappedFields() const { |
| if (mapping_ == nullptr) { |
| const static std::unique_ptr<MappedFields> kEmptyFields = MappedFields::Make({}); |
| return *kEmptyFields; |
| } |
| return *mapping_; |
| } |
| |
| const std::unordered_map<int32_t, MappedFieldConstRef>& NameMapping::LazyFieldsById() |
| const { |
| if (fields_by_id_.empty()) { |
| IndexByIdVisitor visitor; |
| visitor.Visit(AsMappedFields()); |
| fields_by_id_ = std::move(visitor.field_by_id); |
| } |
| return fields_by_id_; |
| } |
| |
| const std::unordered_map<std::string, MappedFieldConstRef>& |
| NameMapping::LazyFieldsByName() const { |
| if (fields_by_name_.empty()) { |
| IndexByNameVisitor visitor; |
| visitor.Visit(AsMappedFields()); |
| fields_by_name_ = std::move(visitor.field_by_name); |
| } |
| return fields_by_name_; |
| } |
| |
| std::unique_ptr<NameMapping> NameMapping::MakeEmpty() { |
| return std::unique_ptr<NameMapping>(new NameMapping(MappedFields::Make({}))); |
| } |
| |
| std::unique_ptr<NameMapping> NameMapping::Make(std::unique_ptr<MappedFields> fields) { |
| return std::unique_ptr<NameMapping>(new NameMapping(std::move(fields))); |
| } |
| |
| std::unique_ptr<NameMapping> NameMapping::Make(std::vector<MappedField> fields) { |
| return Make(MappedFields::Make(std::move(fields))); |
| } |
| |
| bool operator==(const MappedField& lhs, const MappedField& rhs) { |
| if (lhs.field_id != rhs.field_id) { |
| return false; |
| } |
| if (lhs.names != rhs.names) { |
| return false; |
| } |
| if (lhs.nested_mapping == nullptr && rhs.nested_mapping == nullptr) { |
| return true; |
| } |
| if (lhs.nested_mapping == nullptr || rhs.nested_mapping == nullptr) { |
| return false; |
| } |
| return *lhs.nested_mapping == *rhs.nested_mapping; |
| } |
| |
| bool operator==(const MappedFields& lhs, const MappedFields& rhs) { |
| if (lhs.Size() != rhs.Size()) { |
| return false; |
| } |
| auto lhs_fields = lhs.fields(); |
| auto rhs_fields = rhs.fields(); |
| for (size_t i = 0; i < lhs.Size(); ++i) { |
| if (lhs_fields[i] != rhs_fields[i]) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool operator==(const NameMapping& lhs, const NameMapping& rhs) { |
| return lhs.AsMappedFields() == rhs.AsMappedFields(); |
| } |
| |
| std::string ToString(const MappedField& field) { |
| return std::format( |
| "({} -> {}{})", field.names, |
| field.field_id.has_value() ? std::to_string(field.field_id.value()) : "null", |
| field.nested_mapping ? std::format(", {}", ToString(*field.nested_mapping)) : ""); |
| } |
| |
| std::string ToString(const MappedFields& fields) { |
| return std::format("{}", fields.fields()); |
| } |
| |
| std::string ToString(const NameMapping& name_mapping) { |
| const auto& fields = name_mapping.AsMappedFields(); |
| if (fields.Size() == 0) { |
| return "[]"; |
| } |
| std::string repr = "[\n"; |
| for (const auto& field : fields.fields()) { |
| std::format_to(std::back_inserter(repr), " {}\n", ToString(field)); |
| } |
| repr += "]"; |
| return repr; |
| } |
| |
| namespace { |
| |
| // Visitor class for creating name mappings from schema types |
| class CreateMappingVisitor { |
| public: |
| Result<std::unique_ptr<MappedFields>> Visit(const StructType& type) const { |
| std::vector<MappedField> fields; |
| fields.reserve(type.fields().size()); |
| for (const auto& field : type.fields()) { |
| ICEBERG_RETURN_UNEXPECTED(AddMappedField(fields, std::string(field.name()), field)); |
| } |
| return MappedFields::Make(std::move(fields)); |
| } |
| |
| Result<std::unique_ptr<MappedFields>> Visit(const ListType& type) const { |
| std::vector<MappedField> fields; |
| ICEBERG_RETURN_UNEXPECTED(AddMappedField(fields, "element", type.fields().back())); |
| return MappedFields::Make(std::move(fields)); |
| } |
| |
| Result<std::unique_ptr<MappedFields>> Visit(const MapType& type) const { |
| std::vector<MappedField> fields; |
| fields.reserve(2); |
| ICEBERG_RETURN_UNEXPECTED(AddMappedField(fields, "key", type.key())); |
| ICEBERG_RETURN_UNEXPECTED(AddMappedField(fields, "value", type.value())); |
| return MappedFields::Make(std::move(fields)); |
| } |
| |
| template <typename T> |
| Result<std::unique_ptr<MappedFields>> Visit(const T& type) const { |
| return nullptr; |
| } |
| |
| private: |
| Status AddMappedField(std::vector<MappedField>& fields, const std::string& name, |
| const SchemaField& field) const { |
| ICEBERG_ASSIGN_OR_RAISE( |
| auto visit_result, |
| VisitType(*field.type(), [this](const auto& type) { return this->Visit(type); })); |
| |
| fields.emplace_back(MappedField{ |
| .names = {name}, |
| .field_id = field.field_id(), |
| .nested_mapping = std::move(visit_result), |
| }); |
| return {}; |
| } |
| }; |
| |
| // Visitor class for updating name mappings with schema changes |
| class UpdateMappingVisitor { |
| public: |
| UpdateMappingVisitor( |
| const std::unordered_map<int32_t, std::shared_ptr<SchemaField>>& updates, |
| const std::multimap<int32_t, int32_t>& adds) |
| : updates_(updates), adds_(adds) {} |
| |
| Result<std::unique_ptr<MappedFields>> VisitMapping(const NameMapping& mapping) { |
| ICEBERG_ASSIGN_OR_RAISE(auto fields_result, VisitFields(mapping.AsMappedFields())); |
| return AddNewFields(std::move(fields_result), kRootId); |
| } |
| |
| private: |
| static constexpr int32_t kRootId = -1; |
| |
| Result<std::unique_ptr<MappedFields>> VisitFields(const MappedFields& fields) { |
| // Recursively visit all fields |
| std::vector<MappedField> field_results; |
| field_results.reserve(fields.Size()); |
| |
| for (const auto& field : fields.fields()) { |
| ICEBERG_ASSIGN_OR_RAISE(auto field_result, VisitField(field)); |
| field_results.push_back(std::move(field_result)); |
| } |
| |
| // Build update assignments map for removing reassigned names |
| std::unordered_map<std::string, int32_t> update_assignments; |
| for (const auto& field : field_results) { |
| if (field.field_id.has_value()) { |
| auto update_it = updates_.find(field.field_id.value()); |
| if (update_it != updates_.end()) { |
| update_assignments.emplace(std::string(update_it->second->name()), |
| field.field_id.value()); |
| } |
| } |
| } |
| |
| // Remove reassigned names from all fields |
| for (auto& field : field_results) { |
| field = RemoveReassignedNames(field, update_assignments); |
| } |
| |
| return MappedFields::Make(std::move(field_results)); |
| } |
| |
| Result<MappedField> VisitField(const MappedField& field) { |
| // Update this field's names |
| std::unordered_set<std::string> field_names = field.names; |
| if (field.field_id.has_value()) { |
| auto update_it = updates_.find(field.field_id.value()); |
| if (update_it != updates_.end()) { |
| field_names.insert(std::string(update_it->second->name())); |
| } |
| } |
| |
| std::unique_ptr<MappedFields> nested_mapping = nullptr; |
| if (field.nested_mapping != nullptr) { |
| ICEBERG_ASSIGN_OR_RAISE(nested_mapping, VisitFields(*field.nested_mapping)); |
| } |
| |
| // Add a new mapping for any new nested fields |
| if (field.field_id.has_value()) { |
| ICEBERG_ASSIGN_OR_RAISE(nested_mapping, AddNewFields(std::move(nested_mapping), |
| field.field_id.value())); |
| } |
| |
| return MappedField{ |
| .names = std::move(field_names), |
| .field_id = field.field_id, |
| .nested_mapping = std::move(nested_mapping), |
| }; |
| } |
| |
| Result<std::unique_ptr<MappedFields>> AddNewFields( |
| std::unique_ptr<MappedFields> mapping, int32_t parent_id) { |
| auto range = adds_.equal_range(parent_id); |
| std::vector<const SchemaField*> fields_to_add; |
| for (auto it = range.first; it != range.second; ++it) { |
| auto update_it = updates_.find(it->second); |
| if (update_it != updates_.end()) { |
| fields_to_add.push_back(update_it->second.get()); |
| } |
| } |
| |
| if (fields_to_add.empty()) { |
| return std::move(mapping); |
| } |
| |
| std::vector<MappedField> new_fields; |
| CreateMappingVisitor create_visitor; |
| for (const auto* field_to_add : fields_to_add) { |
| ICEBERG_ASSIGN_OR_RAISE( |
| auto nested_result, |
| VisitType(*field_to_add->type(), [&create_visitor](const auto& type) { |
| return create_visitor.Visit(type); |
| })); |
| |
| new_fields.emplace_back(MappedField{ |
| .names = {std::string(field_to_add->name())}, |
| .field_id = field_to_add->field_id(), |
| .nested_mapping = std::move(nested_result), |
| }); |
| } |
| |
| if (mapping == nullptr || mapping->Size() == 0) { |
| return MappedFields::Make(std::move(new_fields)); |
| } |
| |
| // Build assignments map for removing reassigned names |
| std::unordered_map<std::string, int32_t> assignments; |
| for (const auto* field_to_add : fields_to_add) { |
| assignments.emplace(std::string(field_to_add->name()), field_to_add->field_id()); |
| } |
| |
| // create a copy of fields that can be updated (append new fields, replace existing |
| // for reassignment) |
| std::vector<MappedField> fields; |
| fields.reserve(mapping->Size() + new_fields.size()); |
| for (const auto& field : mapping->fields()) { |
| fields.push_back(RemoveReassignedNames(field, assignments)); |
| } |
| |
| fields.insert(fields.end(), std::make_move_iterator(new_fields.begin()), |
| std::make_move_iterator(new_fields.end())); |
| |
| return MappedFields::Make(std::move(fields)); |
| } |
| |
| static MappedField RemoveReassignedNames( |
| const MappedField& field, |
| const std::unordered_map<std::string, int32_t>& assignments) { |
| std::unordered_set<std::string> updated_names = field.names; |
| std::erase_if(updated_names, [&](const std::string& name) { |
| auto assign_it = assignments.find(name); |
| return assign_it != assignments.end() && |
| (!field.field_id.has_value() || assign_it->second != field.field_id.value()); |
| }); |
| return MappedField{ |
| .names = std::move(updated_names), |
| .field_id = field.field_id, |
| .nested_mapping = field.nested_mapping, |
| }; |
| } |
| |
| const std::unordered_map<int32_t, std::shared_ptr<SchemaField>>& updates_; |
| const std::multimap<int32_t, int32_t>& adds_; |
| }; |
| |
| } // namespace |
| |
| Result<std::unique_ptr<NameMapping>> CreateMapping(const Schema& schema) { |
| CreateMappingVisitor visitor; |
| ICEBERG_ASSIGN_OR_RAISE( |
| auto result, |
| VisitType(schema, |
| [&visitor](const auto& type) -> Result<std::unique_ptr<MappedFields>> { |
| return visitor.Visit(type); |
| })); |
| return NameMapping::Make(std::move(result)); |
| } |
| |
| Result<std::unique_ptr<NameMapping>> UpdateMapping( |
| const NameMapping& mapping, |
| const std::unordered_map<int32_t, std::shared_ptr<SchemaField>>& updates, |
| const std::multimap<int32_t, int32_t>& adds) { |
| UpdateMappingVisitor visitor(updates, adds); |
| ICEBERG_ASSIGN_OR_RAISE(auto result, visitor.VisitMapping(mapping)); |
| return NameMapping::Make(std::move(result)); |
| } |
| |
| } // namespace iceberg |