| /* | |
| * 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 <format> | |
| #include <sstream> | |
| #include <string_view> | |
| #include <arrow/type.h> | |
| #include <avro/CustomAttributes.hh> | |
| #include <avro/LogicalType.hh> | |
| #include <avro/NodeImpl.hh> | |
| #include <avro/Schema.hh> | |
| #include <avro/Types.hh> | |
| #include <avro/ValidSchema.hh> | |
| #include "iceberg/avro/avro_constants.h" | |
| #include "iceberg/avro/avro_schema_util_internal.h" | |
| #include "iceberg/metadata_columns.h" | |
| #include "iceberg/name_mapping.h" | |
| #include "iceberg/schema.h" | |
| #include "iceberg/schema_util_internal.h" | |
| #include "iceberg/util/formatter.h" // IWYU pragma: keep | |
| #include "iceberg/util/macros.h" | |
| #include "iceberg/util/string_util.h" | |
| #include "iceberg/util/visit_type.h" | |
| namespace iceberg::avro { | |
| namespace { | |
| ::avro::LogicalType GetMapLogicalType() { | |
| return ::avro::LogicalType(std::make_shared<MapLogicalType>()); | |
| } | |
| ::avro::CustomAttributes GetAttributesWithFieldId(int32_t field_id) { | |
| ::avro::CustomAttributes attributes; | |
| attributes.addAttribute(std::string(kFieldIdProp), std::to_string(field_id), | |
| /*addQuotes=*/false); | |
| return attributes; | |
| } | |
| void SanitizeChar(char c, std::ostringstream& os) { | |
| if (std::isdigit(c)) { | |
| os << '_' << c; | |
| } else { | |
| os << "_x" << std::uppercase << std::hex << static_cast<int>(c); | |
| } | |
| } | |
| } // namespace | |
| bool ValidAvroName(std::string_view name) { | |
| if (name.empty()) { | |
| return false; | |
| } | |
| char first = name[0]; | |
| if (!(std::isalpha(first) || first == '_')) { | |
| return false; | |
| } | |
| for (size_t i = 1; i < name.length(); i++) { | |
| char character = name[i]; | |
| if (!(std::isalnum(character) || character == '_')) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| std::string SanitizeFieldName(std::string_view field_name) { | |
| if (field_name.empty()) { | |
| return ""; | |
| } | |
| std::ostringstream result; | |
| if (!std::isalpha(field_name[0]) && field_name[0] != '_') { | |
| SanitizeChar(field_name[0], result); | |
| } else { | |
| result << field_name[0]; | |
| } | |
| for (size_t i = 1; i < field_name.size(); ++i) { | |
| char c = field_name[i]; | |
| if (std::isalnum(c) || c == '_') { | |
| result << c; | |
| } else { | |
| SanitizeChar(c, result); | |
| } | |
| } | |
| return result.str(); | |
| } | |
| std::string ToString(const ::avro::NodePtr& node) { | |
| std::stringstream ss; | |
| ss << *node; | |
| return ss.str(); | |
| } | |
| std::string ToString(const ::avro::LogicalType& logical_type) { | |
| std::stringstream ss; | |
| logical_type.printJson(ss); | |
| return ss.str(); | |
| } | |
| std::string ToString(const ::avro::LogicalType::Type& logical_type) { | |
| return ToString(::avro::LogicalType(logical_type)); | |
| } | |
| bool IsAvroDateOrPlainInt(const ::avro::NodePtr& node) { | |
| return node->type() == ::avro::AVRO_INT && | |
| (node->logicalType().type() == ::avro::LogicalType::DATE || | |
| node->logicalType().type() == ::avro::LogicalType::NONE); | |
| } | |
| Status ToAvroNodeVisitor::Visit(const BooleanType& type, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodePrimitive>(::avro::AVRO_BOOL); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const IntType& type, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodePrimitive>(::avro::AVRO_INT); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const LongType& type, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodePrimitive>(::avro::AVRO_LONG); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const FloatType& type, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodePrimitive>(::avro::AVRO_FLOAT); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const DoubleType& type, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodePrimitive>(::avro::AVRO_DOUBLE); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const DecimalType& type, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodeFixed>(); | |
| (*node)->setName( | |
| ::avro::Name(std::format("decimal_{}_{}", type.precision(), type.scale()))); | |
| (*node)->setFixedSize(::arrow::DecimalType::DecimalSize(type.precision())); | |
| ::avro::LogicalType logical_type(::avro::LogicalType::DECIMAL); | |
| logical_type.setPrecision(type.precision()); | |
| logical_type.setScale(type.scale()); | |
| (*node)->setLogicalType(logical_type); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const DateType& type, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodePrimitive>(::avro::AVRO_INT); | |
| (*node)->setLogicalType(::avro::LogicalType{::avro::LogicalType::DATE}); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const TimeType& type, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodePrimitive>(::avro::AVRO_LONG); | |
| (*node)->setLogicalType(::avro::LogicalType{::avro::LogicalType::TIME_MICROS}); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const TimestampType& type, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodePrimitive>(::avro::AVRO_LONG); | |
| (*node)->setLogicalType(::avro::LogicalType{::avro::LogicalType::TIMESTAMP_MICROS}); | |
| ::avro::CustomAttributes attributes; | |
| attributes.addAttribute(std::string(kAdjustToUtcProp), "false", /*addQuotes=*/false); | |
| (*node)->addCustomAttributesForField(attributes); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const TimestampTzType& type, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodePrimitive>(::avro::AVRO_LONG); | |
| (*node)->setLogicalType(::avro::LogicalType{::avro::LogicalType::TIMESTAMP_MICROS}); | |
| ::avro::CustomAttributes attributes; | |
| attributes.addAttribute(std::string(kAdjustToUtcProp), "true", /*addQuotes=*/false); | |
| (*node)->addCustomAttributesForField(attributes); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const TimestampNsType& type, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodePrimitive>(::avro::AVRO_LONG); | |
| (*node)->setLogicalType(::avro::LogicalType{::avro::LogicalType::TIMESTAMP_NANOS}); | |
| ::avro::CustomAttributes attributes; | |
| attributes.addAttribute(std::string(kAdjustToUtcProp), "false", /*addQuotes=*/false); | |
| (*node)->addCustomAttributesForField(attributes); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const TimestampTzNsType& type, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodePrimitive>(::avro::AVRO_LONG); | |
| (*node)->setLogicalType(::avro::LogicalType{::avro::LogicalType::TIMESTAMP_NANOS}); | |
| ::avro::CustomAttributes attributes; | |
| attributes.addAttribute(std::string(kAdjustToUtcProp), "true", /*addQuotes=*/false); | |
| (*node)->addCustomAttributesForField(attributes); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const StringType& type, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodePrimitive>(::avro::AVRO_STRING); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const UuidType& type, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodeFixed>(); | |
| (*node)->setName(::avro::Name("uuid_fixed")); | |
| (*node)->setFixedSize(16); | |
| (*node)->setLogicalType(::avro::LogicalType{::avro::LogicalType::UUID}); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const FixedType& type, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodeFixed>(); | |
| (*node)->setName(::avro::Name(std::format("fixed_{}", type.length()))); | |
| (*node)->setFixedSize(type.length()); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const BinaryType& type, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodePrimitive>(::avro::AVRO_BYTES); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const UnknownType&, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodePrimitive>(::avro::AVRO_NULL); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const VariantType&, ::avro::NodePtr*) { | |
| return NotSupported("Writing Iceberg variant type to Avro is not supported"); | |
| } | |
| Status ToAvroNodeVisitor::Visit(const GeometryType&, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodePrimitive>(::avro::AVRO_BYTES); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const GeographyType&, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodePrimitive>(::avro::AVRO_BYTES); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const StructType& type, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodeRecord>(); | |
| if (field_ids_.empty()) { | |
| (*node)->setName(::avro::Name("iceberg_schema")); // Root node | |
| } else { | |
| (*node)->setName(::avro::Name(std::format("r{}", field_ids_.top()))); | |
| } | |
| for (const SchemaField& sub_field : type.fields()) { | |
| ::avro::NodePtr field_node; | |
| ICEBERG_RETURN_UNEXPECTED(Visit(sub_field, &field_node)); | |
| bool is_valid_field_name = ValidAvroName(sub_field.name()); | |
| std::string field_name = is_valid_field_name ? std::string(sub_field.name()) | |
| : SanitizeFieldName(sub_field.name()); | |
| (*node)->addName(field_name); | |
| (*node)->addLeaf(field_node); | |
| ::avro::CustomAttributes attributes = GetAttributesWithFieldId(sub_field.field_id()); | |
| if (!is_valid_field_name) { | |
| attributes.addAttribute(std::string(kIcebergFieldNameProp), | |
| std::string(sub_field.name()), | |
| /*addQuotes=*/true); | |
| } | |
| (*node)->addCustomAttributesForField(attributes); | |
| } | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const ListType& type, ::avro::NodePtr* node) { | |
| *node = std::make_shared<::avro::NodeArray>(); | |
| const auto& element_field = type.fields().back(); | |
| ::avro::CustomAttributes attributes; | |
| attributes.addAttribute(std::string(kElementIdProp), | |
| std::to_string(element_field.field_id()), | |
| /*addQuotes=*/false); | |
| ::avro::NodePtr element_node; | |
| ICEBERG_RETURN_UNEXPECTED(Visit(element_field, &element_node)); | |
| (*node)->addCustomAttributesForField(attributes); | |
| (*node)->addLeaf(std::move(element_node)); | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const MapType& type, ::avro::NodePtr* node) { | |
| const auto& key_field = type.key(); | |
| const auto& value_field = type.value(); | |
| if (key_field.optional()) [[unlikely]] { | |
| return InvalidArgument("Map key `{}` must be required", key_field.name()); | |
| } | |
| if (key_field.type()->type_id() == TypeId::kString) { | |
| ::avro::CustomAttributes attributes; | |
| attributes.addAttribute(std::string(kKeyIdProp), std::to_string(key_field.field_id()), | |
| /*addQuotes=*/false); | |
| attributes.addAttribute(std::string(kValueIdProp), | |
| std::to_string(value_field.field_id()), | |
| /*addQuotes=*/false); | |
| ::avro::NodePtr value_node; | |
| ICEBERG_RETURN_UNEXPECTED(Visit(value_field, &value_node)); | |
| *node = std::make_shared<::avro::NodeMap>(); | |
| (*node)->addLeaf(std::move(value_node)); | |
| (*node)->addCustomAttributesForField(attributes); | |
| } else { | |
| auto struct_node = std::make_shared<::avro::NodeRecord>(); | |
| struct_node->setName(::avro::Name( | |
| std::format("k{}_v{}", key_field.field_id(), value_field.field_id()))); | |
| ::avro::NodePtr key_node; | |
| ICEBERG_RETURN_UNEXPECTED(Visit(key_field, &key_node)); | |
| struct_node->addLeaf(std::move(key_node)); | |
| struct_node->addName("key"); | |
| struct_node->addCustomAttributesForField( | |
| GetAttributesWithFieldId(key_field.field_id())); | |
| ::avro::NodePtr value_node; | |
| ICEBERG_RETURN_UNEXPECTED(Visit(value_field, &value_node)); | |
| struct_node->addLeaf(std::move(value_node)); | |
| struct_node->addName("value"); | |
| struct_node->addCustomAttributesForField( | |
| GetAttributesWithFieldId(value_field.field_id())); | |
| *node = std::make_shared<::avro::NodeArray>(); | |
| (*node)->addLeaf(std::move(struct_node)); | |
| (*node)->setLogicalType(GetMapLogicalType()); | |
| } | |
| return {}; | |
| } | |
| Status ToAvroNodeVisitor::Visit(const SchemaField& field, ::avro::NodePtr* node) { | |
| field_ids_.push(field.field_id()); | |
| ICEBERG_RETURN_UNEXPECTED(VisitTypeInline(*field.type(), /*visitor=*/this, node)); | |
| if (field.optional() && (*node)->type() != ::avro::AVRO_NULL) { | |
| ::avro::MultiLeaves union_types; | |
| union_types.add(std::make_shared<::avro::NodePrimitive>(::avro::AVRO_NULL)); | |
| union_types.add(std::move(*node)); | |
| *node = std::make_shared<::avro::NodeUnion>(union_types); | |
| } | |
| field_ids_.pop(); | |
| return {}; | |
| } | |
| namespace { | |
| bool HasId(const ::avro::NodePtr& parent_node, size_t field_idx, | |
| const std::string& attr_name) { | |
| if (field_idx >= parent_node->customAttributes()) { | |
| return false; | |
| } | |
| return parent_node->customAttributesAt(field_idx).getAttribute(attr_name).has_value(); | |
| } | |
| } // namespace | |
| Status HasIdVisitor::Visit(const ::avro::NodePtr& node) { | |
| if (!node) [[unlikely]] { | |
| return InvalidSchema("Avro node is null"); | |
| } | |
| switch (node->type()) { | |
| case ::avro::AVRO_RECORD: | |
| return VisitRecord(node); | |
| case ::avro::AVRO_ARRAY: | |
| return VisitArray(node); | |
| case ::avro::AVRO_MAP: | |
| return VisitMap(node); | |
| case ::avro::AVRO_UNION: | |
| return VisitUnion(node); | |
| case ::avro::AVRO_BOOL: | |
| case ::avro::AVRO_INT: | |
| case ::avro::AVRO_LONG: | |
| case ::avro::AVRO_FLOAT: | |
| case ::avro::AVRO_DOUBLE: | |
| case ::avro::AVRO_STRING: | |
| case ::avro::AVRO_BYTES: | |
| case ::avro::AVRO_FIXED: | |
| case ::avro::AVRO_NULL: | |
| return {}; | |
| case ::avro::AVRO_ENUM: | |
| default: | |
| return InvalidSchema("Unsupported Avro type: {}", static_cast<int>(node->type())); | |
| } | |
| } | |
| Status HasIdVisitor::VisitRecord(const ::avro::NodePtr& node) { | |
| static const std::string kFieldIdKey{kFieldIdProp}; | |
| total_fields_ += node->leaves(); | |
| for (size_t i = 0; i < node->leaves(); ++i) { | |
| if (HasId(node, i, kFieldIdKey)) { | |
| fields_with_id_++; | |
| } | |
| ICEBERG_RETURN_UNEXPECTED(Visit(node->leafAt(i))); | |
| } | |
| return {}; | |
| } | |
| Status HasIdVisitor::VisitArray(const ::avro::NodePtr& node) { | |
| if (node->leaves() != 1) [[unlikely]] { | |
| return InvalidSchema("Array type must have exactly one leaf"); | |
| } | |
| if (node->logicalType().type() == ::avro::LogicalType::CUSTOM && | |
| node->logicalType().customLogicalType() != nullptr && | |
| node->logicalType().customLogicalType()->name() == "map") { | |
| return Visit(node->leafAt(0)); | |
| } | |
| total_fields_++; | |
| if (HasId(node, /*field_idx=*/0, std::string(kElementIdProp))) { | |
| fields_with_id_++; | |
| } | |
| return Visit(node->leafAt(0)); | |
| } | |
| Status HasIdVisitor::VisitMap(const ::avro::NodePtr& node) { | |
| if (node->leaves() != 2) [[unlikely]] { | |
| return InvalidSchema("Map type must have exactly two leaves"); | |
| } | |
| total_fields_ += 2; | |
| if (HasId(node, /*field_idx=*/0, std::string(kKeyIdProp))) { | |
| fields_with_id_++; | |
| } | |
| if (HasId(node, /*field_idx=*/0, std::string(kValueIdProp))) { | |
| fields_with_id_++; | |
| } | |
| return Visit(node->leafAt(1)); | |
| } | |
| Status HasIdVisitor::VisitUnion(const ::avro::NodePtr& node) { | |
| if (node->leaves() != 2) [[unlikely]] { | |
| return InvalidSchema("Union type must have exactly two branches"); | |
| } | |
| const auto& branch_0 = node->leafAt(0); | |
| const auto& branch_1 = node->leafAt(1); | |
| if (branch_0->type() == ::avro::AVRO_NULL) { | |
| return Visit(branch_1); | |
| } | |
| if (branch_1->type() == ::avro::AVRO_NULL) { | |
| return Visit(branch_0); | |
| } | |
| return InvalidSchema("Union type must have exactly one null branch"); | |
| } | |
| Status HasIdVisitor::Visit(const ::avro::ValidSchema& schema) { | |
| return Visit(schema.root()); | |
| } | |
| Status HasIdVisitor::Visit(const ::avro::Schema& schema) { return Visit(schema.root()); } | |
| namespace { | |
| bool HasLogicalType(const ::avro::NodePtr& node, | |
| ::avro::LogicalType::Type expected_type) { | |
| return node->logicalType().type() == expected_type; | |
| } | |
| // Relies on Avro preserving custom attributes on primitive nodes (AVRO-4351); | |
| // before that, "adjust-to-utc" was dropped when parsing a file schema and every | |
| // timestamp read from a file looked like it had no timezone. | |
| std::optional<std::string> GetAdjustToUtc(const ::avro::NodePtr& node) { | |
| if (node->customAttributes() == 0) { | |
| return std::nullopt; | |
| } | |
| return node->customAttributesAt(0).getAttribute(std::string(kAdjustToUtcProp)); | |
| } | |
| Result<int32_t> GetId(const ::avro::NodePtr& node, const std::string& attr_name, | |
| size_t field_idx) { | |
| if (field_idx >= node->customAttributes()) { | |
| return InvalidSchema("Field index {} exceeds available custom attributes {}", | |
| field_idx, node->customAttributes()); | |
| } | |
| auto id_str = node->customAttributesAt(field_idx).getAttribute(attr_name); | |
| if (!id_str.has_value()) { | |
| return InvalidSchema("Missing avro attribute: {}", attr_name); | |
| } | |
| return StringUtils::ParseNumber<int32_t>(id_str.value()); | |
| } | |
| Result<int32_t> GetElementId(const ::avro::NodePtr& node) { | |
| static const std::string kElementIdKey{kElementIdProp}; | |
| return GetId(node, kElementIdKey, /*field_idx=*/0); | |
| } | |
| Result<int32_t> GetKeyId(const ::avro::NodePtr& node) { | |
| static const std::string kKeyIdKey{kKeyIdProp}; | |
| return GetId(node, kKeyIdKey, /*field_idx=*/0); | |
| } | |
| Result<int32_t> GetValueId(const ::avro::NodePtr& node) { | |
| static const std::string kValueIdKey{kValueIdProp}; | |
| return GetId(node, kValueIdKey, /*field_idx=*/0); | |
| } | |
| Result<int32_t> GetFieldId(const ::avro::NodePtr& node, size_t field_idx) { | |
| static const std::string kFieldIdKey{kFieldIdProp}; | |
| return GetId(node, kFieldIdKey, field_idx); | |
| } | |
| Status ValidateAvroSchemaEvolution(const Type& expected_type, | |
| const ::avro::NodePtr& avro_node) { | |
| if (avro_node->type() == ::avro::AVRO_NULL) { | |
| return {}; | |
| } | |
| switch (expected_type.type_id()) { | |
| case TypeId::kBoolean: | |
| if (avro_node->type() == ::avro::AVRO_BOOL) { | |
| return {}; | |
| } | |
| break; | |
| case TypeId::kInt: | |
| if (avro_node->type() == ::avro::AVRO_INT) { | |
| return {}; | |
| } | |
| break; | |
| case TypeId::kLong: | |
| if (avro_node->type() == ::avro::AVRO_LONG || | |
| avro_node->type() == ::avro::AVRO_INT) { | |
| return {}; | |
| } | |
| break; | |
| case TypeId::kFloat: | |
| if (avro_node->type() == ::avro::AVRO_FLOAT) { | |
| return {}; | |
| } | |
| break; | |
| case TypeId::kDouble: | |
| if (avro_node->type() == ::avro::AVRO_DOUBLE || | |
| avro_node->type() == ::avro::AVRO_FLOAT) { | |
| return {}; | |
| } | |
| break; | |
| case TypeId::kDate: | |
| if (IsAvroDateOrPlainInt(avro_node)) { | |
| return {}; | |
| } | |
| break; | |
| case TypeId::kTime: | |
| if (avro_node->type() == ::avro::AVRO_LONG && | |
| HasLogicalType(avro_node, ::avro::LogicalType::TIME_MICROS)) { | |
| return {}; | |
| } | |
| break; | |
| case TypeId::kTimestamp: | |
| if (avro_node->type() == ::avro::AVRO_LONG && | |
| HasLogicalType(avro_node, ::avro::LogicalType::TIMESTAMP_MICROS) && | |
| GetAdjustToUtc(avro_node).value_or("false") == "false") { | |
| return {}; | |
| } | |
| break; | |
| case TypeId::kTimestampTz: | |
| if (avro_node->type() == ::avro::AVRO_LONG && | |
| HasLogicalType(avro_node, ::avro::LogicalType::TIMESTAMP_MICROS) && | |
| GetAdjustToUtc(avro_node).value_or("false") == "true") { | |
| return {}; | |
| } | |
| break; | |
| case TypeId::kTimestampNs: | |
| if (avro_node->type() == ::avro::AVRO_LONG && | |
| HasLogicalType(avro_node, ::avro::LogicalType::TIMESTAMP_NANOS) && | |
| GetAdjustToUtc(avro_node).value_or("false") == "false") { | |
| return {}; | |
| } | |
| break; | |
| case TypeId::kTimestampTzNs: | |
| if (avro_node->type() == ::avro::AVRO_LONG && | |
| HasLogicalType(avro_node, ::avro::LogicalType::TIMESTAMP_NANOS) && | |
| GetAdjustToUtc(avro_node).value_or("false") == "true") { | |
| return {}; | |
| } | |
| break; | |
| case TypeId::kString: | |
| if (avro_node->type() == ::avro::AVRO_STRING) { | |
| return {}; | |
| } | |
| break; | |
| case TypeId::kDecimal: | |
| if (avro_node->type() == ::avro::AVRO_FIXED && | |
| HasLogicalType(avro_node, ::avro::LogicalType::DECIMAL)) { | |
| const auto& decimal_type = | |
| internal::checked_cast<const DecimalType&>(expected_type); | |
| const auto logical_type = avro_node->logicalType(); | |
| if (decimal_type.scale() == logical_type.scale() && | |
| decimal_type.precision() >= logical_type.precision()) { | |
| return {}; | |
| } | |
| } | |
| break; | |
| case TypeId::kUuid: | |
| if (avro_node->type() == ::avro::AVRO_FIXED && avro_node->fixedSize() == 16 && | |
| HasLogicalType(avro_node, ::avro::LogicalType::UUID)) { | |
| return {}; | |
| } | |
| break; | |
| case TypeId::kFixed: | |
| if (avro_node->type() == ::avro::AVRO_FIXED && | |
| avro_node->fixedSize() == | |
| internal::checked_cast<const FixedType&>(expected_type).length()) { | |
| return {}; | |
| } | |
| break; | |
| case TypeId::kBinary: | |
| case TypeId::kGeometry: | |
| case TypeId::kGeography: | |
| if (avro_node->type() == ::avro::AVRO_BYTES) { | |
| return {}; | |
| } | |
| break; | |
| case TypeId::kUnknown: | |
| return {}; | |
| case TypeId::kVariant: | |
| return NotSupported("Reading Iceberg type {} from Avro is not supported", | |
| expected_type); | |
| default: | |
| break; | |
| } | |
| return InvalidSchema("Cannot read Iceberg type: {} from Avro type: {}", expected_type, | |
| ToString(avro_node)); | |
| } | |
| // XXX: Result<::avro::NodePtr> leads to unresolved external symbol error on Windows. | |
| Status UnwrapUnion(const ::avro::NodePtr& node, ::avro::NodePtr* result) { | |
| if (node->type() != ::avro::AVRO_UNION) { | |
| *result = node; | |
| return {}; | |
| } | |
| if (node->leaves() != 2) { | |
| return InvalidSchema("Union type must have exactly two branches"); | |
| } | |
| auto branch_0 = node->leafAt(0); | |
| auto branch_1 = node->leafAt(1); | |
| if (branch_0->type() == ::avro::AVRO_NULL) { | |
| *result = branch_1; | |
| } else if (branch_1->type() == ::avro::AVRO_NULL) { | |
| *result = branch_0; | |
| } else { | |
| return InvalidSchema("Union type must have exactly one null branch, got {}", | |
| ToString(node)); | |
| } | |
| return {}; | |
| } | |
| // Forward declaration | |
| Result<FieldProjection> ProjectNested(const Type& expected_type, | |
| const ::avro::NodePtr& avro_node, | |
| bool prune_source); | |
| Result<FieldProjection> ProjectField(const SchemaField& expected_field, | |
| const ::avro::NodePtr& avro_node, | |
| size_t source_index, bool prune_source) { | |
| const Type& expected_type = *expected_field.type(); | |
| ::avro::NodePtr field_node; | |
| ICEBERG_RETURN_UNEXPECTED(UnwrapUnion(avro_node, &field_node)); | |
| FieldProjection projection; | |
| if (expected_type.type_id() == TypeId::kUnknown || | |
| field_node->type() == ::avro::AVRO_NULL) { | |
| if (!expected_field.optional()) { | |
| return InvalidSchema("Cannot project required field with ID: {} as null", | |
| expected_field.field_id()); | |
| } | |
| projection.kind = FieldProjection::Kind::kNull; | |
| return projection; | |
| } | |
| if (expected_type.is_nested()) { | |
| ICEBERG_ASSIGN_OR_RAISE(projection, | |
| ProjectNested(expected_type, field_node, prune_source)); | |
| } else { | |
| ICEBERG_RETURN_UNEXPECTED(ValidateAvroSchemaEvolution(expected_type, field_node)); | |
| } | |
| projection.from = source_index; | |
| projection.kind = FieldProjection::Kind::kProjected; | |
| return projection; | |
| } | |
| Result<FieldProjection> ProjectStruct(const StructType& struct_type, | |
| const ::avro::NodePtr& avro_node, | |
| bool prune_source) { | |
| if (avro_node->type() != ::avro::AVRO_RECORD) { | |
| return InvalidSchema("Expected AVRO_RECORD type, but got {}", ToString(avro_node)); | |
| } | |
| const auto& expected_fields = struct_type.fields(); | |
| struct NodeInfo { | |
| size_t local_index; | |
| ::avro::NodePtr field_node; | |
| }; | |
| std::unordered_map<int32_t, NodeInfo> node_info_map; | |
| node_info_map.reserve(avro_node->leaves()); | |
| for (size_t i = 0; i < avro_node->leaves(); ++i) { | |
| ICEBERG_ASSIGN_OR_RAISE(int32_t field_id, GetFieldId(avro_node, i)); | |
| ::avro::NodePtr field_node = avro_node->leafAt(i); | |
| if (const auto [iter, inserted] = node_info_map.emplace( | |
| std::piecewise_construct, std::forward_as_tuple(field_id), | |
| std::forward_as_tuple(i, field_node)); | |
| !inserted) [[unlikely]] { | |
| return InvalidSchema("Duplicate field id found in Avro schema: {}", field_id); | |
| } | |
| } | |
| FieldProjection result; | |
| result.children.reserve(expected_fields.size()); | |
| for (const auto& expected_field : expected_fields) { | |
| int32_t field_id = expected_field.field_id(); | |
| FieldProjection child_projection; | |
| if (auto iter = node_info_map.find(field_id); iter != node_info_map.cend()) { | |
| ICEBERG_ASSIGN_OR_RAISE(child_projection, | |
| ProjectField(expected_field, iter->second.field_node, | |
| iter->second.local_index, prune_source)); | |
| } else if (MetadataColumns::IsMetadataColumn(field_id)) { | |
| child_projection.kind = FieldProjection::Kind::kMetadata; | |
| } else if (expected_field.initial_default() != nullptr) { | |
| // Rows written before the field existed assume its `initial-default` value. | |
| child_projection.kind = FieldProjection::Kind::kDefault; | |
| child_projection.from = *expected_field.initial_default(); | |
| } else if (expected_field.optional()) { | |
| child_projection.kind = FieldProjection::Kind::kNull; | |
| } else { | |
| return InvalidSchema("Missing required field with ID: {}", field_id); | |
| } | |
| result.children.emplace_back(std::move(child_projection)); | |
| } | |
| if (prune_source) { | |
| PruneFieldProjection(result); | |
| } | |
| return result; | |
| } | |
| Result<FieldProjection> ProjectList(const ListType& list_type, | |
| const ::avro::NodePtr& avro_node, bool prune_source) { | |
| if (avro_node->type() != ::avro::AVRO_ARRAY) { | |
| return InvalidSchema("Expected AVRO_ARRAY type, but got {}", ToString(avro_node)); | |
| } | |
| if (avro_node->leaves() != 1) { | |
| return InvalidSchema("Array type must have exactly one node, got {}", | |
| avro_node->leaves()); | |
| } | |
| const auto& expected_element_field = list_type.fields().back(); | |
| ICEBERG_ASSIGN_OR_RAISE(int32_t avro_element_id, GetElementId(avro_node)); | |
| if (expected_element_field.field_id() != avro_element_id) [[unlikely]] { | |
| return InvalidSchema("element-id mismatch, expected {}, got {}", | |
| expected_element_field.field_id(), avro_element_id); | |
| } | |
| FieldProjection element_projection; | |
| ICEBERG_ASSIGN_OR_RAISE(element_projection, | |
| ProjectField(expected_element_field, avro_node->leafAt(0), | |
| /*source_index*/ size_t{0}, prune_source)); | |
| FieldProjection result; | |
| result.children.emplace_back(std::move(element_projection)); | |
| return result; | |
| } | |
| Result<FieldProjection> ProjectMap(const MapType& map_type, | |
| const ::avro::NodePtr& avro_node, bool prune_source) { | |
| const auto& expected_key_field = map_type.key(); | |
| const auto& expected_value_field = map_type.value(); | |
| FieldProjection result; | |
| int32_t avro_key_id; | |
| int32_t avro_value_id; | |
| ::avro::NodePtr map_node; | |
| if (avro_node->type() == ::avro::AVRO_MAP) { | |
| if (avro_node->leaves() != 2) { | |
| return InvalidSchema("Map type must have exactly two nodes, got {}", | |
| avro_node->leaves()); | |
| } | |
| map_node = avro_node; | |
| ICEBERG_ASSIGN_OR_RAISE(avro_key_id, GetKeyId(avro_node)); | |
| ICEBERG_ASSIGN_OR_RAISE(avro_value_id, GetValueId(avro_node)); | |
| } else if (avro_node->type() == ::avro::AVRO_ARRAY && HasMapLogicalType(avro_node)) { | |
| if (avro_node->leaves() != 1) { | |
| return InvalidSchema("Array-backed map type must have exactly one node, got {}", | |
| avro_node->leaves()); | |
| } | |
| map_node = avro_node->leafAt(0); | |
| if (map_node->type() != ::avro::AVRO_RECORD || map_node->leaves() != 2) { | |
| return InvalidSchema( | |
| "Array-backed map type must have a record node with two fields"); | |
| } | |
| ICEBERG_ASSIGN_OR_RAISE(avro_key_id, GetFieldId(map_node, 0)); | |
| ICEBERG_ASSIGN_OR_RAISE(avro_value_id, GetFieldId(map_node, 1)); | |
| } else { | |
| return InvalidSchema("Expected a map type, but got Avro type {}", | |
| ToString(avro_node)); | |
| } | |
| if (expected_key_field.field_id() != avro_key_id) { | |
| return InvalidSchema("key-id mismatch, expected {}, got {}", | |
| expected_key_field.field_id(), avro_key_id); | |
| } | |
| if (expected_value_field.field_id() != avro_value_id) { | |
| return InvalidSchema("value-id mismatch, expected {}, got {}", | |
| expected_value_field.field_id(), avro_value_id); | |
| } | |
| for (size_t i = 0; i < map_node->leaves(); ++i) { | |
| FieldProjection sub_projection; | |
| const auto& expected_sub_field = map_type.fields()[i]; | |
| ICEBERG_ASSIGN_OR_RAISE( | |
| sub_projection, | |
| ProjectField(expected_sub_field, map_node->leafAt(i), i, prune_source)); | |
| result.children.emplace_back(std::move(sub_projection)); | |
| } | |
| return result; | |
| } | |
| Result<FieldProjection> ProjectNested(const Type& expected_type, | |
| const ::avro::NodePtr& avro_node, | |
| bool prune_source) { | |
| if (!expected_type.is_nested()) { | |
| return InvalidSchema("Expected a nested type, but got {}", expected_type); | |
| } | |
| switch (expected_type.type_id()) { | |
| case TypeId::kStruct: | |
| return ProjectStruct(internal::checked_cast<const StructType&>(expected_type), | |
| avro_node, prune_source); | |
| case TypeId::kList: | |
| return ProjectList(internal::checked_cast<const ListType&>(expected_type), | |
| avro_node, prune_source); | |
| case TypeId::kMap: | |
| return ProjectMap(internal::checked_cast<const MapType&>(expected_type), avro_node, | |
| prune_source); | |
| default: | |
| return InvalidSchema("Unsupported nested type: {}", expected_type); | |
| } | |
| } | |
| } // namespace | |
| bool HasMapLogicalType(const ::avro::NodePtr& node) { | |
| return node->logicalType().type() == ::avro::LogicalType::CUSTOM && | |
| node->logicalType().customLogicalType() != nullptr && | |
| node->logicalType().customLogicalType()->name() == "map"; | |
| } | |
| Result<SchemaProjection> Project(const Schema& expected_schema, | |
| const ::avro::NodePtr& avro_node, bool prune_source) { | |
| ICEBERG_ASSIGN_OR_RAISE( | |
| auto field_projection, | |
| ProjectNested(static_cast<const Type&>(expected_schema), avro_node, prune_source)); | |
| return SchemaProjection{std::move(field_projection.children)}; | |
| } | |
| namespace { | |
| class NamesGuard { | |
| public: | |
| NamesGuard(std::vector<std::string>& names, const std::string& name) : names_(names) { | |
| names_.push_back(name); | |
| } | |
| ~NamesGuard() { names_.pop_back(); } | |
| private: | |
| std::vector<std::string>& names_; | |
| }; | |
| // Forward declaration | |
| Result<::avro::NodePtr> MakeAvroNodeWithFieldIds(const ::avro::NodePtr& original_node, | |
| const NameMapping& mapping, | |
| std::vector<std::string>& names); | |
| void CopyCustomAttributes(const ::avro::CustomAttributes& source, | |
| ::avro::CustomAttributes& target) { | |
| for (const auto& [key, value] : source.attributes()) { // NOLINT(modernize-type-traits) | |
| target.addAttribute(key, value, /*addQuote=*/false); | |
| } | |
| } | |
| Result<MappedFieldConstRef> FindMappedField(const NameMapping& mapping, | |
| const std::vector<std::string>& names) { | |
| auto field_opt = mapping.Find(names); | |
| if (!field_opt.has_value()) { | |
| return InvalidSchema("Field '{}' not found in name mapping", names.back()); | |
| } | |
| const MappedField& field = field_opt.value().get(); | |
| if (!field.field_id.has_value()) { | |
| return InvalidSchema("Field ID is missing for field '{}' in name mapping", | |
| names.back()); | |
| } | |
| return field_opt.value(); | |
| } | |
| template <typename NodeType> | |
| Status ProcessChildNode(const ::avro::NodePtr& parent_node, size_t child_index, | |
| const std::string& child_name, std::string_view fieldIdPropKy, | |
| const NameMapping& mapping, std::vector<std::string>& names, | |
| NodeType& new_parent_node) { | |
| NamesGuard guard(names, child_name); | |
| ICEBERG_ASSIGN_OR_RAISE(auto mapped_field, FindMappedField(mapping, names)); | |
| ICEBERG_ASSIGN_OR_RAISE( | |
| auto new_child_node, | |
| MakeAvroNodeWithFieldIds(parent_node->leafAt(child_index), mapping, names)); | |
| ::avro::CustomAttributes attributes; | |
| attributes.addAttribute(std::string(fieldIdPropKy), | |
| std::to_string(*mapped_field.get().field_id), | |
| /*addQuote=*/false); | |
| if (parent_node->customAttributes() > child_index) { | |
| CopyCustomAttributes(parent_node->customAttributesAt(child_index), attributes); | |
| } | |
| new_parent_node->addLeaf(new_child_node); | |
| new_parent_node->addCustomAttributesForField(attributes); | |
| return {}; | |
| } | |
| Result<::avro::NodePtr> MakeRecordNodeWithFieldIds(const ::avro::NodePtr& original_node, | |
| const NameMapping& mapping, | |
| std::vector<std::string>& names) { | |
| auto new_record_node = std::make_shared<::avro::NodeRecord>(); | |
| new_record_node->setName(original_node->name()); | |
| for (size_t i = 0; i < original_node->leaves(); ++i) { | |
| if (i >= original_node->names()) { | |
| return InvalidSchema("Index {} is out of bounds for names (size: {})", i, | |
| original_node->names()); | |
| } | |
| const std::string& field_name = original_node->nameAt(i); | |
| ::avro::NodePtr field_node = original_node->leafAt(i); | |
| ICEBERG_RETURN_UNEXPECTED(ProcessChildNode(original_node, i, field_name, kFieldIdProp, | |
| mapping, names, new_record_node)); | |
| new_record_node->addName(field_name); | |
| } | |
| return new_record_node; | |
| } | |
| Result<::avro::NodePtr> MakeArrayNodeWithFieldIds(const ::avro::NodePtr& original_node, | |
| const NameMapping& mapping, | |
| std::vector<std::string>& names) { | |
| if (original_node->leaves() != 1) { | |
| return InvalidSchema("Array type must have exactly one leaf"); | |
| } | |
| auto new_array_node = std::make_shared<::avro::NodeArray>(); | |
| // Check if this is a map represented as array | |
| if (HasMapLogicalType(original_node)) { | |
| ICEBERG_ASSIGN_OR_RAISE( | |
| auto new_element_node, | |
| MakeAvroNodeWithFieldIds(original_node->leafAt(0), mapping, names)); | |
| new_array_node->addLeaf(new_element_node); | |
| if (original_node->customAttributes() > 0) { | |
| new_array_node->addCustomAttributesForField(original_node->customAttributesAt(0)); | |
| } | |
| return new_array_node; | |
| } | |
| ICEBERG_RETURN_UNEXPECTED(ProcessChildNode(original_node, 0, "element", kElementIdProp, | |
| mapping, names, new_array_node)); | |
| return new_array_node; | |
| } | |
| Result<::avro::NodePtr> MakeMapNodeWithFieldIds(const ::avro::NodePtr& original_node, | |
| const NameMapping& mapping, | |
| std::vector<std::string>& names) { | |
| if (original_node->leaves() != 2) { | |
| return InvalidSchema("Map type must have exactly two leaves"); | |
| } | |
| auto new_map_node = std::make_shared<::avro::NodeMap>(); | |
| ICEBERG_RETURN_UNEXPECTED(ProcessChildNode(original_node, 0, "key", kKeyIdProp, mapping, | |
| names, new_map_node)); | |
| ICEBERG_RETURN_UNEXPECTED(ProcessChildNode(original_node, 1, "value", kValueIdProp, | |
| mapping, names, new_map_node)); | |
| return new_map_node; | |
| } | |
| Result<::avro::NodePtr> MakeUnionNodeWithFieldIds(const ::avro::NodePtr& original_node, | |
| const NameMapping& mapping, | |
| std::vector<std::string>& names) { | |
| if (original_node->leaves() != 2) { | |
| return InvalidSchema("Union type must have exactly two branches"); | |
| } | |
| const auto& branch_0 = original_node->leafAt(0); | |
| const auto& branch_1 = original_node->leafAt(1); | |
| bool branch_0_is_null = (branch_0->type() == ::avro::AVRO_NULL); | |
| bool branch_1_is_null = (branch_1->type() == ::avro::AVRO_NULL); | |
| if (branch_0_is_null && !branch_1_is_null) { | |
| // branch_0 is null, branch_1 is not null | |
| ICEBERG_ASSIGN_OR_RAISE(auto new_branch_1, | |
| MakeAvroNodeWithFieldIds(branch_1, mapping, names)); | |
| auto new_union_node = std::make_shared<::avro::NodeUnion>(); | |
| new_union_node->addLeaf(branch_0); // null branch | |
| new_union_node->addLeaf(new_branch_1); | |
| return new_union_node; | |
| } else if (!branch_0_is_null && branch_1_is_null) { | |
| // branch_0 is not null, branch_1 is null | |
| ICEBERG_ASSIGN_OR_RAISE(auto new_branch_0, | |
| MakeAvroNodeWithFieldIds(branch_0, mapping, names)); | |
| auto new_union_node = std::make_shared<::avro::NodeUnion>(); | |
| new_union_node->addLeaf(new_branch_0); | |
| new_union_node->addLeaf(branch_1); // null branch | |
| return new_union_node; | |
| } else if (branch_0_is_null && branch_1_is_null) { | |
| // Both branches are null - this is invalid | |
| return InvalidSchema("Union type cannot have two null branches"); | |
| } else { | |
| // Neither branch is null - this is invalid | |
| return InvalidSchema("Union type must have exactly one null branch"); | |
| } | |
| } | |
| Result<::avro::NodePtr> MakeAvroNodeWithFieldIds(const ::avro::NodePtr& original_node, | |
| const NameMapping& mapping, | |
| std::vector<std::string>& names) { | |
| switch (original_node->type()) { | |
| case ::avro::AVRO_RECORD: | |
| return MakeRecordNodeWithFieldIds(original_node, mapping, names); | |
| case ::avro::AVRO_ARRAY: | |
| return MakeArrayNodeWithFieldIds(original_node, mapping, names); | |
| case ::avro::AVRO_MAP: | |
| return MakeMapNodeWithFieldIds(original_node, mapping, names); | |
| case ::avro::AVRO_UNION: | |
| return MakeUnionNodeWithFieldIds(original_node, mapping, names); | |
| case ::avro::AVRO_BOOL: | |
| case ::avro::AVRO_INT: | |
| case ::avro::AVRO_LONG: | |
| case ::avro::AVRO_FLOAT: | |
| case ::avro::AVRO_DOUBLE: | |
| case ::avro::AVRO_STRING: | |
| case ::avro::AVRO_BYTES: | |
| case ::avro::AVRO_FIXED: | |
| case ::avro::AVRO_NULL: | |
| // For primitive types, just return a copy | |
| return original_node; | |
| case ::avro::AVRO_ENUM: | |
| default: | |
| return InvalidSchema("Unsupported Avro type for field ID application: {}", | |
| ToString(original_node)); | |
| } | |
| } | |
| } // namespace | |
| Result<::avro::NodePtr> MakeAvroNodeWithFieldIds(const ::avro::NodePtr& original_node, | |
| const NameMapping& mapping) { | |
| std::vector<std::string> names; | |
| return MakeAvroNodeWithFieldIds(original_node, mapping, names); | |
| } | |
| } // namespace iceberg::avro |