| /* |
| * 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/schema.h" |
| |
| #include <format> |
| #include <functional> |
| #include <stack> |
| |
| #include "iceberg/result.h" |
| #include "iceberg/row/struct_like.h" |
| #include "iceberg/schema_internal.h" |
| #include "iceberg/table_metadata.h" |
| #include "iceberg/type.h" |
| #include "iceberg/util/formatter.h" // IWYU pragma: keep |
| #include "iceberg/util/macros.h" |
| #include "iceberg/util/type_util.h" |
| #include "iceberg/util/visit_type.h" |
| |
| namespace iceberg { |
| |
| struct SchemaReassignIdContext { |
| Schema::IdMap ids_to_reassigned; |
| Schema::IdMap ids_to_original; |
| }; |
| |
| namespace { |
| |
| const Schema::IdMap& EmptyIdMap() { |
| static const Schema::IdMap kEmpty; |
| return kEmpty; |
| } |
| |
| void RecordIdReassignment(int32_t old_id, int32_t new_id, |
| Schema::IdMap& ids_to_reassigned, |
| Schema::IdMap& ids_to_original) { |
| if (new_id != old_id) { |
| ids_to_reassigned[old_id] = new_id; |
| ids_to_original[new_id] = old_id; |
| } |
| } |
| |
| SchemaField ReassignField(const SchemaField& field, int32_t new_id, |
| const Schema::GetId& get_id, Schema::IdMap& ids_to_reassigned, |
| Schema::IdMap& ids_to_original); |
| |
| std::shared_ptr<Type> ReassignTypeIds(const std::shared_ptr<Type>& type, |
| const Schema::GetId& get_id, |
| Schema::IdMap& ids_to_reassigned, |
| Schema::IdMap& ids_to_original) { |
| switch (type->type_id()) { |
| case TypeId::kStruct: { |
| const auto& struct_type = static_cast<const StructType&>(*type); |
| const auto& fields = struct_type.fields(); |
| std::vector<int32_t> new_ids; |
| new_ids.reserve(fields.size()); |
| for (const auto& field : fields) { |
| const auto new_id = get_id(field.field_id()); |
| RecordIdReassignment(field.field_id(), new_id, ids_to_reassigned, |
| ids_to_original); |
| new_ids.push_back(new_id); |
| } |
| |
| std::vector<SchemaField> reassigned_fields; |
| reassigned_fields.reserve(fields.size()); |
| for (size_t i = 0; i < fields.size(); ++i) { |
| reassigned_fields.emplace_back(ReassignField(fields[i], new_ids[i], get_id, |
| ids_to_reassigned, ids_to_original)); |
| } |
| return std::make_shared<StructType>(std::move(reassigned_fields)); |
| } |
| case TypeId::kList: { |
| const auto& list_type = static_cast<const ListType&>(*type); |
| const auto& element = list_type.element(); |
| const auto new_id = get_id(element.field_id()); |
| RecordIdReassignment(element.field_id(), new_id, ids_to_reassigned, |
| ids_to_original); |
| return std::make_shared<ListType>( |
| ReassignField(element, new_id, get_id, ids_to_reassigned, ids_to_original)); |
| } |
| case TypeId::kMap: { |
| const auto& map_type = static_cast<const MapType&>(*type); |
| const auto& key = map_type.key(); |
| const auto& value = map_type.value(); |
| const auto new_key_id = get_id(key.field_id()); |
| const auto new_value_id = get_id(value.field_id()); |
| RecordIdReassignment(key.field_id(), new_key_id, ids_to_reassigned, |
| ids_to_original); |
| RecordIdReassignment(value.field_id(), new_value_id, ids_to_reassigned, |
| ids_to_original); |
| return std::make_shared<MapType>( |
| ReassignField(key, new_key_id, get_id, ids_to_reassigned, ids_to_original), |
| ReassignField(value, new_value_id, get_id, ids_to_reassigned, ids_to_original)); |
| } |
| default: |
| return type; |
| } |
| } |
| |
| SchemaField ReassignField(const SchemaField& field, int32_t new_id, |
| const Schema::GetId& get_id, Schema::IdMap& ids_to_reassigned, |
| Schema::IdMap& ids_to_original) { |
| return {new_id, std::string(field.name()), |
| ReassignTypeIds(field.type(), get_id, ids_to_reassigned, ids_to_original), |
| field.optional(), std::string(field.doc())}; |
| } |
| |
| std::vector<SchemaField> ReassignIds(std::vector<SchemaField> fields, |
| const Schema::GetId& get_id, |
| SchemaReassignIdContext& reassign_id_context) { |
| auto reassigned_type = ReassignTypeIds(std::make_shared<StructType>(std::move(fields)), |
| get_id, reassign_id_context.ids_to_reassigned, |
| reassign_id_context.ids_to_original); |
| const auto& reassigned_fields = |
| internal::checked_cast<const StructType&>(*reassigned_type).fields(); |
| return {reassigned_fields.begin(), reassigned_fields.end()}; |
| } |
| |
| Status ValidateFieldNullability(const Type& type) { |
| auto validate_field = [&](const SchemaField& field) -> Status { |
| ICEBERG_PRECHECK(field.optional() || field.type()->type_id() != TypeId::kUnknown, |
| "Unknown type field '{}' must be optional", field.name()); |
| return ValidateFieldNullability(*field.type()); |
| }; |
| |
| switch (type.type_id()) { |
| case TypeId::kStruct: { |
| const auto& struct_type = static_cast<const StructType&>(type); |
| for (const auto& field : struct_type.fields()) { |
| ICEBERG_RETURN_UNEXPECTED(validate_field(field)); |
| } |
| return {}; |
| } |
| case TypeId::kList: { |
| const auto& list_type = static_cast<const ListType&>(type); |
| const auto& element = list_type.element(); |
| return validate_field(element); |
| } |
| case TypeId::kMap: { |
| const auto& map_type = static_cast<const MapType&>(type); |
| const auto& key = map_type.key(); |
| const auto& value = map_type.value(); |
| ICEBERG_PRECHECK(key.type()->type_id() != TypeId::kUnknown, |
| "Map 'key' cannot be unknown type"); |
| ICEBERG_RETURN_UNEXPECTED(ValidateFieldNullability(*key.type())); |
| return validate_field(value); |
| } |
| default: |
| return {}; |
| } |
| } |
| |
| } // namespace |
| |
| Schema::Schema(std::vector<SchemaField> fields, int32_t schema_id, GetId get_id) |
| : StructType(std::move(fields)), |
| schema_id_(schema_id), |
| cache_(std::make_unique<SchemaCache>(this)) { |
| if (get_id) { |
| reassign_id_context_ = std::make_unique<SchemaReassignIdContext>(); |
| fields_ = ReassignIds(std::move(fields_), get_id, *reassign_id_context_); |
| } |
| } |
| |
| Schema::~Schema() = default; |
| |
| Result<std::unique_ptr<Schema>> Schema::Make(std::vector<SchemaField> fields, |
| int32_t schema_id, |
| std::vector<int32_t> identifier_field_ids, |
| GetId get_id) { |
| auto schema = std::make_unique<Schema>(std::move(fields), schema_id, std::move(get_id)); |
| |
| if (!identifier_field_ids.empty()) { |
| auto id_to_parent = IndexParents(*schema); |
| for (auto field_id : identifier_field_ids) { |
| ICEBERG_RETURN_UNEXPECTED( |
| ValidateIdentifierFields(field_id, *schema, id_to_parent)); |
| } |
| } |
| |
| schema->identifier_field_ids_ = std::move(identifier_field_ids); |
| return schema; |
| } |
| |
| Result<std::unique_ptr<Schema>> Schema::Make( |
| std::vector<SchemaField> fields, int32_t schema_id, |
| const std::vector<std::string>& identifier_field_names, GetId get_id) { |
| auto schema = std::make_unique<Schema>(std::move(fields), schema_id, std::move(get_id)); |
| |
| std::vector<int32_t> fresh_identifier_ids; |
| for (const auto& name : identifier_field_names) { |
| ICEBERG_ASSIGN_OR_RAISE(auto field, schema->FindFieldByName(name)); |
| if (!field) { |
| return InvalidSchema("Cannot find identifier field: {}", name); |
| } |
| fresh_identifier_ids.push_back(field.value().get().field_id()); |
| } |
| |
| if (!fresh_identifier_ids.empty()) { |
| auto id_to_parent = IndexParents(*schema); |
| for (auto field_id : fresh_identifier_ids) { |
| ICEBERG_RETURN_UNEXPECTED( |
| ValidateIdentifierFields(field_id, *schema, id_to_parent)); |
| } |
| } |
| |
| schema->identifier_field_ids_ = std::move(fresh_identifier_ids); |
| return schema; |
| } |
| |
| Status Schema::ValidateIdentifierFields( |
| int32_t field_id, const Schema& schema, |
| const std::unordered_map<int32_t, int32_t>& id_to_parent) { |
| ICEBERG_ASSIGN_OR_RAISE(auto field_opt, schema.FindFieldById(field_id)); |
| ICEBERG_PRECHECK(field_opt.has_value(), |
| "Cannot add field {} as an identifier field: field does not exist", |
| field_id); |
| |
| const SchemaField& field = field_opt.value().get(); |
| ICEBERG_PRECHECK( |
| field.type()->is_primitive(), |
| "Cannot add field {} as an identifier field: not a primitive type field", field_id); |
| ICEBERG_PRECHECK(!field.optional(), |
| "Cannot add field {} as an identifier field: not a required field", |
| field_id); |
| ICEBERG_PRECHECK( |
| field.type()->type_id() != TypeId::kDouble && |
| field.type()->type_id() != TypeId::kFloat, |
| "Cannot add field {} as an identifier field: must not be float or double field", |
| field_id); |
| |
| // check whether the nested field is in a chain of required struct fields |
| // exploring from root for better error message for list and map types |
| std::stack<int32_t> ancestors; |
| auto parent_it = id_to_parent.find(field.field_id()); |
| while (parent_it != id_to_parent.end()) { |
| ancestors.push(parent_it->second); |
| parent_it = id_to_parent.find(parent_it->second); |
| } |
| |
| while (!ancestors.empty()) { |
| ICEBERG_ASSIGN_OR_RAISE(auto parent_opt, schema.FindFieldById(ancestors.top())); |
| ICEBERG_PRECHECK( |
| parent_opt.has_value(), |
| "Cannot add field {} as an identifier field: parent field id {} does not exist", |
| field_id, ancestors.top()); |
| const SchemaField& parent = parent_opt.value().get(); |
| ICEBERG_PRECHECK( |
| parent.type()->type_id() == TypeId::kStruct, |
| "Cannot add field {} as an identifier field: must not be nested in {}", field_id, |
| *parent.type()); |
| ICEBERG_PRECHECK(!parent.optional(), |
| "Cannot add field {} as an identifier field: must not be nested in " |
| "optional field {}", |
| field_id, parent.field_id()); |
| ancestors.pop(); |
| } |
| return {}; |
| } |
| |
| const std::shared_ptr<Schema>& Schema::EmptySchema() { |
| static const auto empty_schema = |
| std::make_shared<Schema>(std::vector<SchemaField>{}, kInitialSchemaId); |
| return empty_schema; |
| } |
| |
| int32_t Schema::schema_id() const { return schema_id_; } |
| |
| const Schema::IdMap& Schema::IdsToReassigned() const { |
| return reassign_id_context_ ? reassign_id_context_->ids_to_reassigned : EmptyIdMap(); |
| } |
| |
| const Schema::IdMap& Schema::IdsToOriginal() const { |
| return reassign_id_context_ ? reassign_id_context_->ids_to_original : EmptyIdMap(); |
| } |
| |
| std::string Schema::ToString() const { |
| std::string repr = "schema<"; |
| for (const auto& field : fields_) { |
| std::format_to(std::back_inserter(repr), " {}\n", field); |
| } |
| repr += ">"; |
| return repr; |
| } |
| |
| bool Schema::Equals(const Schema& other) const { |
| return schema_id_ == other.schema_id_ && fields_ == other.fields_ && |
| identifier_field_ids_ == other.identifier_field_ids_; |
| } |
| |
| Result<std::optional<std::reference_wrapper<const SchemaField>>> Schema::FindFieldByName( |
| std::string_view name, bool case_sensitive) const { |
| if (case_sensitive) { |
| ICEBERG_ASSIGN_OR_RAISE(auto name_id_map, cache_->GetNameIdMap()); |
| auto it = name_id_map.get().name_to_id.find(name); |
| if (it == name_id_map.get().name_to_id.end()) { |
| return std::nullopt; |
| }; |
| return FindFieldById(it->second); |
| } |
| ICEBERG_ASSIGN_OR_RAISE(auto lowercase_name_to_id, cache_->GetLowercaseNameToIdMap()); |
| auto it = lowercase_name_to_id.get().find(StringUtils::ToLower(name)); |
| if (it == lowercase_name_to_id.get().end()) { |
| return std::nullopt; |
| } |
| return FindFieldById(it->second); |
| } |
| |
| Result<std::optional<std::reference_wrapper<const SchemaField>>> Schema::FindFieldById( |
| int32_t field_id) const { |
| ICEBERG_ASSIGN_OR_RAISE(auto id_to_field, cache_->GetIdToFieldMap()); |
| auto it = id_to_field.get().find(field_id); |
| if (it == id_to_field.get().end()) { |
| return std::nullopt; |
| } |
| return it->second; |
| } |
| |
| Result<std::optional<std::string_view>> Schema::FindColumnNameById( |
| int32_t field_id) const { |
| ICEBERG_ASSIGN_OR_RAISE(auto name_id_map, cache_->GetNameIdMap()); |
| auto it = name_id_map.get().id_to_name.find(field_id); |
| if (it == name_id_map.get().id_to_name.end()) { |
| return std::nullopt; |
| } |
| return it->second; |
| } |
| |
| Result<std::unique_ptr<StructLikeAccessor>> Schema::GetAccessorById( |
| int32_t field_id) const { |
| ICEBERG_ASSIGN_OR_RAISE(auto id_to_position_path, cache_->GetIdToPositionPathMap()); |
| if (auto it = id_to_position_path.get().find(field_id); |
| it != id_to_position_path.get().cend()) { |
| ICEBERG_ASSIGN_OR_RAISE(auto field, FindFieldById(field_id)); |
| if (!field.has_value()) { |
| return NotFound("Cannot get accessor for field id: {}", field_id); |
| } |
| return std::make_unique<StructLikeAccessor>(field.value().get().type(), it->second); |
| } |
| return NotFound("Cannot get accessor for field id: {}", field_id); |
| } |
| |
| Result<std::unique_ptr<Schema>> Schema::Select(std::span<const std::string> names, |
| bool case_sensitive) const { |
| if (std::ranges::find(names, kAllColumns) != names.end()) { |
| auto struct_type = ToStructType(*this); |
| return FromStructType(std::move(*struct_type), std::nullopt); |
| } |
| |
| std::unordered_set<int32_t> selected_ids; |
| for (const auto& name : names) { |
| ICEBERG_ASSIGN_OR_RAISE(auto result, FindFieldByName(name, case_sensitive)); |
| if (result.has_value()) { |
| selected_ids.insert(result.value().get().field_id()); |
| } |
| } |
| |
| PruneColumnVisitor visitor(selected_ids, /*select_full_types=*/true); |
| ICEBERG_ASSIGN_OR_RAISE( |
| auto pruned_type, visitor.Visit(std::shared_ptr<StructType>(ToStructType(*this)))); |
| |
| if (!pruned_type) { |
| return std::make_unique<Schema>(std::vector<SchemaField>{}, kInitialSchemaId); |
| } |
| |
| if (pruned_type->type_id() != TypeId::kStruct) { |
| return InvalidSchema("Projected type must be a struct type"); |
| } |
| |
| return FromStructType(std::move(internal::checked_cast<StructType&>(*pruned_type)), |
| std::nullopt); |
| } |
| |
| Result<std::unique_ptr<Schema>> Schema::Project( |
| const std::unordered_set<int32_t>& field_ids) const { |
| PruneColumnVisitor visitor(field_ids, /*select_full_types=*/false); |
| ICEBERG_ASSIGN_OR_RAISE( |
| auto project_type, visitor.Visit(std::shared_ptr<StructType>(ToStructType(*this)))); |
| |
| if (!project_type) { |
| return std::make_unique<Schema>(std::vector<SchemaField>{}, kInitialSchemaId); |
| } |
| |
| if (project_type->type_id() != TypeId::kStruct) { |
| return InvalidSchema("Projected type must be a struct type"); |
| } |
| |
| return FromStructType(std::move(internal::checked_cast<StructType&>(*project_type)), |
| std::nullopt); |
| } |
| |
| const std::vector<int32_t>& Schema::IdentifierFieldIds() const { |
| return identifier_field_ids_; |
| } |
| |
| Result<std::vector<std::string>> Schema::IdentifierFieldNames() const { |
| std::vector<std::string> names; |
| names.reserve(identifier_field_ids_.size()); |
| for (auto id : identifier_field_ids_) { |
| ICEBERG_ASSIGN_OR_RAISE(auto name, FindColumnNameById(id)); |
| if (!name.has_value()) { |
| return InvalidSchema("Cannot find identifier field id: {}", id); |
| } |
| names.emplace_back(name.value()); |
| } |
| return names; |
| } |
| |
| Result<int32_t> Schema::HighestFieldId() const { return cache_->GetHighestFieldId(); } |
| |
| bool Schema::SameSchema(const Schema& other) const { |
| return fields_ == other.fields_ && identifier_field_ids_ == other.identifier_field_ids_; |
| } |
| |
| Status Schema::Validate(int32_t format_version) const { |
| ICEBERG_RETURN_UNEXPECTED(ValidateFieldNullability(*this)); |
| |
| // Get all fields including nested ones |
| ICEBERG_ASSIGN_OR_RAISE(auto id_to_field, cache_->GetIdToFieldMap()); |
| |
| // Check each field's type and defaults |
| for (const auto& [field_id, field_ref] : id_to_field.get()) { |
| const auto& field = field_ref.get(); |
| |
| // Check if the field's type requires a minimum format version |
| if (auto it = TableMetadata::kMinFormatVersions.find(field.type()->type_id()); |
| it != TableMetadata::kMinFormatVersions.end()) { |
| if (int32_t min_format_version = it->second; format_version < min_format_version) { |
| return InvalidSchema("Invalid type for {}: {} is not supported until v{}", |
| field.name(), *field.type(), min_format_version); |
| } |
| } |
| |
| // TODO(GuoTao.yu): Check default values when they are supported |
| } |
| |
| return {}; |
| } |
| |
| Result<SchemaCache::IdToFieldMapRef> SchemaCache::GetIdToFieldMap() const { |
| return id_to_field_.Get(schema_); |
| } |
| |
| Result<SchemaCache::NameIdMapRef> SchemaCache::GetNameIdMap() const { |
| return name_id_map_.Get(schema_); |
| } |
| |
| Result<SchemaCache::LowercaseNameToIdMapRef> SchemaCache::GetLowercaseNameToIdMap() |
| const { |
| return lowercase_name_to_id_.Get(schema_); |
| } |
| |
| Result<SchemaCache::IdToPositionPathMapRef> SchemaCache::GetIdToPositionPathMap() const { |
| return id_to_position_path_.Get(schema_); |
| } |
| |
| Result<int32_t> SchemaCache::GetHighestFieldId() const { |
| return highest_field_id_.Get(schema_); |
| } |
| |
| Result<SchemaCache::IdToFieldMap> SchemaCache::InitIdToFieldMap(const Schema* schema) { |
| std::unordered_map<int32_t, std::reference_wrapper<const SchemaField>> id_to_field; |
| IdToFieldVisitor visitor(id_to_field); |
| ICEBERG_RETURN_UNEXPECTED(VisitTypeInline(*schema, &visitor)); |
| return id_to_field; |
| } |
| |
| Result<SchemaCache::NameIdMap> SchemaCache::InitNameIdMap(const Schema* schema) { |
| NameIdMap name_id_map; |
| NameToIdVisitor visitor(name_id_map.name_to_id, &name_id_map.id_to_name, |
| /*case_sensitive=*/true); |
| ICEBERG_RETURN_UNEXPECTED( |
| VisitTypeInline(*schema, &visitor, /*path=*/"", /*short_path=*/"")); |
| visitor.Finish(); |
| return name_id_map; |
| } |
| |
| Result<SchemaCache::LowercaseNameToIdMap> SchemaCache::InitLowerCaseNameToIdMap( |
| const Schema* schema) { |
| std::unordered_map<std::string, int32_t, StringHash, std::equal_to<>> |
| lowercase_name_to_id; |
| NameToIdVisitor visitor(lowercase_name_to_id, /*id_to_name=*/nullptr, |
| /*case_sensitive=*/false); |
| ICEBERG_RETURN_UNEXPECTED( |
| VisitTypeInline(*schema, &visitor, /*path=*/"", /*short_path=*/"")); |
| visitor.Finish(); |
| return lowercase_name_to_id; |
| } |
| |
| Result<SchemaCache::IdToPositionPathMap> SchemaCache::InitIdToPositionPath( |
| const Schema* schema) { |
| PositionPathVisitor visitor; |
| ICEBERG_RETURN_UNEXPECTED(VisitTypeInline(*schema, &visitor)); |
| return visitor.Finish(); |
| } |
| |
| Result<int32_t> SchemaCache::InitHighestFieldId(const Schema* schema) { |
| ICEBERG_ASSIGN_OR_RAISE(auto id_to_field, InitIdToFieldMap(schema)); |
| |
| if (id_to_field.empty()) { |
| return Schema::kInitialColumnId; |
| } |
| |
| auto max_it = std::ranges::max_element( |
| id_to_field, |
| [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); |
| |
| return max_it->first; |
| } |
| |
| } // namespace iceberg |