This is an automated email from the ASF dual-hosted git repository.
zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new 82a1cd6 feat: avro support applying field-ids based on name mapping
(#127)
82a1cd6 is described below
commit 82a1cd6d614bbcbd0063dbc11cfa03d54e1f443f
Author: liuxiaoyu <[email protected]>
AuthorDate: Thu Aug 21 23:15:34 2025 +0800
feat: avro support applying field-ids based on name mapping (#127)
see issue https://github.com/apache/iceberg-cpp/issues/128
---------
Co-authored-by: Gang Wu <[email protected]>
---
src/iceberg/avro/avro_reader.cc | 21 +-
src/iceberg/avro/avro_schema_util.cc | 278 ++++++++++++++++++++
src/iceberg/avro/avro_schema_util_internal.h | 15 ++
src/iceberg/avro/constants.h | 34 +++
src/iceberg/file_reader.h | 3 +
src/iceberg/name_mapping.cc | 7 +-
src/iceberg/name_mapping.h | 6 +-
test/avro_schema_test.cc | 380 +++++++++++++++++++++++++++
test/name_mapping_test.cc | 52 ++++
9 files changed, 785 insertions(+), 11 deletions(-)
diff --git a/src/iceberg/avro/avro_reader.cc b/src/iceberg/avro/avro_reader.cc
index 153951c..c15eb66 100644
--- a/src/iceberg/avro/avro_reader.cc
+++ b/src/iceberg/avro/avro_reader.cc
@@ -36,6 +36,7 @@
#include "iceberg/avro/avro_data_util_internal.h"
#include "iceberg/avro/avro_schema_util_internal.h"
#include "iceberg/avro/avro_stream_internal.h"
+#include "iceberg/name_mapping.h"
#include "iceberg/schema_internal.h"
#include "iceberg/util/checked_cast.h"
#include "iceberg/util/macros.h"
@@ -92,16 +93,26 @@ class AvroReader::Impl {
// Create a base reader without setting reader schema to enable projection.
auto base_reader =
std::make_unique<::avro::DataFileReaderBase>(std::move(input_stream));
- const ::avro::ValidSchema& file_schema = base_reader->dataSchema();
+ ::avro::ValidSchema file_schema = base_reader->dataSchema();
// Validate field ids in the file schema.
HasIdVisitor has_id_visitor;
ICEBERG_RETURN_UNEXPECTED(has_id_visitor.Visit(file_schema));
+
if (has_id_visitor.HasNoIds()) {
- // TODO(gangwu): support applying field-ids based on name mapping
- return NotImplemented("Avro file schema has no field IDs");
- }
- if (!has_id_visitor.AllHaveIds()) {
+ // Apply field IDs based on name mapping if available
+ if (options.name_mapping) {
+ ICEBERG_ASSIGN_OR_RAISE(
+ auto new_root_node,
+ MakeAvroNodeWithFieldIds(file_schema.root(),
*options.name_mapping));
+
+ // Update the file schema to use the new schema with field IDs
+ file_schema = ::avro::ValidSchema(new_root_node);
+ } else {
+ return InvalidSchema(
+ "Avro file schema has no field IDs and no name mapping provided");
+ }
+ } else if (!has_id_visitor.AllHaveIds()) {
return InvalidSchema("Not all fields in the Avro file schema have field
IDs");
}
diff --git a/src/iceberg/avro/avro_schema_util.cc
b/src/iceberg/avro/avro_schema_util.cc
index 28e9e7b..2b16292 100644
--- a/src/iceberg/avro/avro_schema_util.cc
+++ b/src/iceberg/avro/avro_schema_util.cc
@@ -31,7 +31,9 @@
#include "iceberg/avro/avro_register.h"
#include "iceberg/avro/avro_schema_util_internal.h"
+#include "iceberg/avro/constants.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"
@@ -773,4 +775,280 @@ Result<SchemaProjection> Project(const Schema&
expected_schema,
return SchemaProjection{std::move(field_projection.children)};
}
+namespace {
+
+void CopyCustomAttributes(const ::avro::CustomAttributes& source,
+ ::avro::CustomAttributes& target) {
+ for (const auto& attr_pair : source.attributes()) {
+ target.addAttribute(attr_pair.first, attr_pair.second, /*addQuote=*/false);
+ }
+}
+
+Result<::avro::NodePtr> CreateRecordNodeWithFieldIds(const ::avro::NodePtr&
original_node,
+ const MappedField& field)
{
+ 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);
+
+ // TODO(liuxiaoyu): Add support for case sensitivity in name matching.
+ // Try to find nested field by name
+ const MappedField* nested_field = nullptr;
+ if (field.nested_mapping) {
+ auto fields_span = field.nested_mapping->fields();
+ for (const auto& f : fields_span) {
+ if (f.names.find(field_name) != f.names.end()) {
+ nested_field = &f;
+ break;
+ }
+ }
+ }
+
+ if (!nested_field) {
+ return InvalidSchema("Field '{}' not found in nested mapping",
field_name);
+ }
+
+ if (!nested_field->field_id.has_value()) {
+ return InvalidSchema("Field ID is missing for field '{}' in nested
mapping",
+ field_name);
+ }
+
+ // Preserve existing custom attributes for this field
+ ::avro::CustomAttributes attributes;
+ if (i < original_node->customAttributes()) {
+ // Copy all existing attributes from the original node
+ for (const auto& attr_pair :
original_node->customAttributesAt(i).attributes()) {
+ // Copy each existing attribute to preserve original metadata
+ attributes.addAttribute(attr_pair.first, attr_pair.second,
/*addQuote=*/false);
+ }
+ }
+
+ // Add field ID attribute to the new node (preserving existing attributes)
+ attributes.addAttribute(std::string(kFieldIdProp),
+ std::to_string(nested_field->field_id.value()),
+ /*addQuote=*/false);
+
+ new_record_node->addCustomAttributesForField(attributes);
+
+ // Recursively apply field IDs to nested fields
+ ICEBERG_ASSIGN_OR_RAISE(auto new_nested_node,
+ MakeAvroNodeWithFieldIds(field_node,
*nested_field));
+ new_record_node->addName(field_name);
+ new_record_node->addLeaf(new_nested_node);
+ }
+
+ return new_record_node;
+}
+
+Result<::avro::NodePtr> CreateArrayNodeWithFieldIds(const ::avro::NodePtr&
original_node,
+ const MappedField& field) {
+ 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),
field));
+ new_array_node->addLeaf(new_element_node);
+
+ // Check and add custom attributes
+ if (original_node->customAttributes() > 0) {
+ ::avro::CustomAttributes merged_attributes;
+ const auto& original_attrs = original_node->customAttributesAt(0);
+ CopyCustomAttributes(original_attrs, merged_attributes);
+ // Add merged attributes if we found any
+ if (merged_attributes.attributes().size() > 0) {
+ new_array_node->addCustomAttributesForField(merged_attributes);
+ }
+ }
+
+ return new_array_node;
+ }
+
+ // For regular arrays, use the first field from nested mapping as element
field
+ if (!field.nested_mapping || field.nested_mapping->fields().empty()) {
+ return InvalidSchema("Array type requires nested mapping with element
field");
+ }
+
+ const auto& element_field = field.nested_mapping->fields()[0];
+
+ if (!element_field.field_id.has_value()) {
+ return InvalidSchema("Field ID is missing for element field in array");
+ }
+
+ ICEBERG_ASSIGN_OR_RAISE(
+ auto new_element_node,
+ MakeAvroNodeWithFieldIds(original_node->leafAt(0), element_field));
+ new_array_node->addLeaf(new_element_node);
+
+ // Create merged custom attributes with element field ID
+ ::avro::CustomAttributes merged_attributes;
+
+ // First add our element field ID (highest priority)
+ merged_attributes.addAttribute(std::string(kElementIdProp),
+ std::to_string(*element_field.field_id),
+ /*addQuote=*/false);
+
+ // Then merge any custom attributes from original node
+ if (original_node->customAttributes() > 0) {
+ const auto& original_attrs = original_node->customAttributesAt(0);
+ CopyCustomAttributes(original_attrs, merged_attributes);
+ }
+
+ // Add all attributes at once
+ new_array_node->addCustomAttributesForField(merged_attributes);
+
+ return new_array_node;
+}
+
+Result<::avro::NodePtr> CreateMapNodeWithFieldIds(const ::avro::NodePtr&
original_node,
+ const MappedField& field) {
+ if (original_node->leaves() != 2) {
+ return InvalidSchema("Map type must have exactly two leaves");
+ }
+
+ auto new_map_node = std::make_shared<::avro::NodeMap>();
+
+ // For map types, we need to extract key and value field mappings from the
nested
+ // mapping
+ if (!field.nested_mapping) {
+ return InvalidSchema("Map type requires nested mapping for key and value
fields");
+ }
+
+ // For map types, use the first two fields from nested mapping as key and
value
+ if (!field.nested_mapping || field.nested_mapping->fields().size() < 2) {
+ return InvalidSchema("Map type requires nested mapping with key and value
fields");
+ }
+
+ const auto& key_mapped_field = field.nested_mapping->fields()[0];
+ const auto& value_mapped_field = field.nested_mapping->fields()[1];
+
+ if (!key_mapped_field.field_id || !value_mapped_field.field_id) {
+ return InvalidSchema("Map key and value fields must have field IDs");
+ }
+
+ // Add key and value nodes
+ ICEBERG_ASSIGN_OR_RAISE(
+ auto new_key_node,
+ MakeAvroNodeWithFieldIds(original_node->leafAt(0), key_mapped_field));
+ ICEBERG_ASSIGN_OR_RAISE(
+ auto new_value_node,
+ MakeAvroNodeWithFieldIds(original_node->leafAt(1), value_mapped_field));
+ new_map_node->addLeaf(new_key_node);
+ new_map_node->addLeaf(new_value_node);
+
+ // Create key and value attributes
+ ::avro::CustomAttributes key_attributes;
+ ::avro::CustomAttributes value_attributes;
+
+ // Add required field IDs
+ key_attributes.addAttribute(std::string(kKeyIdProp),
+ std::to_string(*key_mapped_field.field_id),
+ /*addQuote=*/false);
+ value_attributes.addAttribute(std::string(kValueIdProp),
+ std::to_string(*value_mapped_field.field_id),
+ /*addQuote=*/false);
+
+ // Merge custom attributes from original node if they exist
+ if (original_node->customAttributes() > 0) {
+ // Merge attributes for key (index 0)
+ const auto& original_key_attrs = original_node->customAttributesAt(0);
+ CopyCustomAttributes(original_key_attrs, key_attributes);
+
+ // Merge attributes for value (index 1)
+ if (original_node->customAttributes() > 1) {
+ const auto& original_value_attrs = original_node->customAttributesAt(1);
+ CopyCustomAttributes(original_value_attrs, value_attributes);
+ }
+ }
+
+ // Add the merged attributes to the new map node
+ new_map_node->addCustomAttributesForField(key_attributes);
+ new_map_node->addCustomAttributesForField(value_attributes);
+
+ return new_map_node;
+}
+
+Result<::avro::NodePtr> CreateUnionNodeWithFieldIds(const ::avro::NodePtr&
original_node,
+ const MappedField& field) {
+ 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, field));
+ 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, field));
+ 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");
+ }
+}
+
+} // namespace
+
+Result<::avro::NodePtr> MakeAvroNodeWithFieldIds(const ::avro::NodePtr&
original_node,
+ const MappedField&
mapped_field) {
+ switch (original_node->type()) {
+ case ::avro::AVRO_RECORD:
+ return CreateRecordNodeWithFieldIds(original_node, mapped_field);
+ case ::avro::AVRO_ARRAY:
+ return CreateArrayNodeWithFieldIds(original_node, mapped_field);
+ case ::avro::AVRO_MAP:
+ return CreateMapNodeWithFieldIds(original_node, mapped_field);
+ case ::avro::AVRO_UNION:
+ return CreateUnionNodeWithFieldIds(original_node, mapped_field);
+ 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:
+ // For primitive types, just return a copy
+ return original_node;
+ case ::avro::AVRO_NULL:
+ case ::avro::AVRO_ENUM:
+ default:
+ return InvalidSchema("Unsupported Avro type for field ID application:
{}",
+ ToString(original_node));
+ }
+}
+
+Result<::avro::NodePtr> MakeAvroNodeWithFieldIds(const ::avro::NodePtr&
original_node,
+ const NameMapping& mapping) {
+ MappedField mapped_field;
+ mapped_field.nested_mapping =
std::make_shared<MappedFields>(mapping.AsMappedFields());
+ return MakeAvroNodeWithFieldIds(original_node, mapped_field);
+}
+
} // namespace iceberg::avro
diff --git a/src/iceberg/avro/avro_schema_util_internal.h
b/src/iceberg/avro/avro_schema_util_internal.h
index de3922a..ac00834 100644
--- a/src/iceberg/avro/avro_schema_util_internal.h
+++ b/src/iceberg/avro/avro_schema_util_internal.h
@@ -23,6 +23,7 @@
#include <avro/Node.hh>
+#include "iceberg/name_mapping.h"
#include "iceberg/result.h"
#include "iceberg/schema_util.h"
#include "iceberg/type.h"
@@ -148,4 +149,18 @@ std::string ToString(const ::avro::LogicalType::Type&
logical_type);
/// \return True if the node has a map logical type, false otherwise.
bool HasMapLogicalType(const ::avro::NodePtr& node);
+/// \brief Create a new Avro node with field IDs from name mapping.
+/// \param original_node The original Avro node to copy.
+/// \param mapped_field The mapped field to apply field IDs from.
+/// \return A new Avro node with field IDs applied, or an error.
+Result<::avro::NodePtr> MakeAvroNodeWithFieldIds(const ::avro::NodePtr&
original_node,
+ const MappedField&
mapped_field);
+
+/// \brief Create a new Avro node with field IDs from name mapping.
+/// \param original_node The original Avro node to copy.
+/// \param mapping The name mapping to apply field IDs from.
+/// \return A new Avro node with field IDs applied, or an error.
+Result<::avro::NodePtr> MakeAvroNodeWithFieldIds(const ::avro::NodePtr&
original_node,
+ const NameMapping& mapping);
+
} // namespace iceberg::avro
diff --git a/src/iceberg/avro/constants.h b/src/iceberg/avro/constants.h
new file mode 100644
index 0000000..b56cc66
--- /dev/null
+++ b/src/iceberg/avro/constants.h
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <string_view>
+
+namespace iceberg::avro {
+
+// Avro logical type constants
+constexpr std::string_view kMapLogicalType = "map";
+
+// Name mapping field constants
+constexpr std::string_view kElement = "element";
+constexpr std::string_view kKey = "key";
+constexpr std::string_view kValue = "value";
+
+} // namespace iceberg::avro
diff --git a/src/iceberg/file_reader.h b/src/iceberg/file_reader.h
index 6fc7522..8a59e33 100644
--- a/src/iceberg/file_reader.h
+++ b/src/iceberg/file_reader.h
@@ -86,6 +86,9 @@ struct ICEBERG_EXPORT ReaderOptions {
/// \brief The filter to apply to the data. Reader implementations may
ignore this if
/// the file format does not support filtering.
std::shared_ptr<class Expression> filter;
+ /// \brief Name mapping for schema evolution compatibility. Used when
reading files
+ /// that may have different field names than the current schema.
+ std::shared_ptr<class NameMapping> name_mapping;
/// \brief Format-specific or implementation-specific properties.
std::unordered_map<std::string, std::string> properties;
};
diff --git a/src/iceberg/name_mapping.cc b/src/iceberg/name_mapping.cc
index ef9a0bb..eaf6199 100644
--- a/src/iceberg/name_mapping.cc
+++ b/src/iceberg/name_mapping.cc
@@ -152,7 +152,7 @@ const std::unordered_map<int32_t, MappedFieldConstRef>&
MappedFields::LazyIdToFi
NameMapping::NameMapping(std::unique_ptr<MappedFields> mapping)
: mapping_(std::move(mapping)) {}
-std::optional<MappedFieldConstRef> NameMapping::Find(int32_t id) {
+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;
@@ -160,14 +160,15 @@ std::optional<MappedFieldConstRef>
NameMapping::Find(int32_t id) {
return std::nullopt;
}
-std::optional<MappedFieldConstRef> NameMapping::Find(std::span<const
std::string> names) {
+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) {
+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;
diff --git a/src/iceberg/name_mapping.h b/src/iceberg/name_mapping.h
index 1b7b6f3..3a66be0 100644
--- a/src/iceberg/name_mapping.h
+++ b/src/iceberg/name_mapping.h
@@ -104,13 +104,13 @@ class ICEBERG_EXPORT NameMapping {
static std::unique_ptr<NameMapping> MakeEmpty();
/// \brief Find a field by its ID.
- std::optional<MappedFieldConstRef> Find(int32_t id);
+ std::optional<MappedFieldConstRef> Find(int32_t id) const;
/// \brief Find a field by its unconcatenated names.
- std::optional<MappedFieldConstRef> Find(std::span<const std::string> names);
+ std::optional<MappedFieldConstRef> Find(std::span<const std::string> names)
const;
/// \brief Find a field by its (concatenated) name.
- std::optional<MappedFieldConstRef> Find(const std::string& name);
+ std::optional<MappedFieldConstRef> Find(const std::string& name) const;
/// \brief Get the underlying MappedFields instance.
const MappedFields& AsMappedFields() const;
diff --git a/test/avro_schema_test.cc b/test/avro_schema_test.cc
index 875bfe9..6fbbb85 100644
--- a/test/avro_schema_test.cc
+++ b/test/avro_schema_test.cc
@@ -26,6 +26,7 @@
#include "iceberg/avro/avro_schema_util_internal.h"
#include "iceberg/metadata_columns.h"
+#include "iceberg/name_mapping.h"
#include "iceberg/schema.h"
#include "matchers.h"
@@ -1057,4 +1058,383 @@ TEST(AvroSchemaProjectionTest,
ProjectDecimalIncompatible) {
ASSERT_THAT(projection_result, HasErrorMessage("Cannot read"));
}
+// NameMapping tests for Avro schema context
+class NameMappingAvroSchemaTest : public ::testing::Test {
+ protected:
+ // Helper function to create a simple name mapping
+ std::unique_ptr<NameMapping> CreateSimpleNameMapping() {
+ std::vector<MappedField> fields;
+ fields.emplace_back(MappedField{.names = {"id"}, .field_id = 1});
+ fields.emplace_back(MappedField{.names = {"name"}, .field_id = 2});
+ fields.emplace_back(MappedField{.names = {"age"}, .field_id = 3});
+ return NameMapping::Make(std::move(fields));
+ }
+
+ // Helper function to create a nested name mapping
+ std::unique_ptr<NameMapping> CreateNestedNameMapping() {
+ std::vector<MappedField> fields;
+ fields.emplace_back(MappedField{.names = {"id"}, .field_id = 1});
+
+ // Nested mapping for address
+ std::vector<MappedField> address_fields;
+ address_fields.emplace_back(MappedField{.names = {"street"}, .field_id =
10});
+ address_fields.emplace_back(MappedField{.names = {"city"}, .field_id =
11});
+ address_fields.emplace_back(MappedField{.names = {"zip"}, .field_id = 12});
+ auto address_mapping = MappedFields::Make(std::move(address_fields));
+
+ fields.emplace_back(MappedField{.names = {"address"},
+ .field_id = 2,
+ .nested_mapping =
std::move(address_mapping)});
+
+ return NameMapping::Make(std::move(fields));
+ }
+
+ // Helper function to create a name mapping for array types
+ std::unique_ptr<NameMapping> CreateArrayNameMapping() {
+ std::vector<MappedField> fields;
+ fields.emplace_back(MappedField{.names = {"id"}, .field_id = 1});
+
+ // Nested mapping for array element
+ std::vector<MappedField> element_fields;
+ element_fields.emplace_back(MappedField{.names = {"element"}, .field_id =
20});
+ auto element_mapping = MappedFields::Make(std::move(element_fields));
+
+ fields.emplace_back(MappedField{
+ .names = {"items"}, .field_id = 2, .nested_mapping =
std::move(element_mapping)});
+
+ return NameMapping::Make(std::move(fields));
+ }
+
+ // Helper function to create a name mapping for map types
+ std::unique_ptr<NameMapping> CreateMapNameMapping() {
+ std::vector<MappedField> fields;
+ fields.emplace_back(MappedField{.names = {"id"}, .field_id = 1});
+
+ // Nested mapping for map key-value
+ std::vector<MappedField> map_fields;
+ map_fields.emplace_back(MappedField{.names = {"key"}, .field_id = 30});
+ map_fields.emplace_back(MappedField{.names = {"value"}, .field_id = 31});
+ auto map_mapping = MappedFields::Make(std::move(map_fields));
+
+ fields.emplace_back(MappedField{.names = {"properties"},
+ .field_id = 2,
+ .nested_mapping = std::move(map_mapping)});
+
+ return NameMapping::Make(std::move(fields));
+ }
+
+ // Helper function to create a name mapping for union types
+ std::unique_ptr<NameMapping> CreateUnionNameMapping() {
+ std::vector<MappedField> fields;
+ fields.emplace_back(MappedField{.names = {"id"}, .field_id = 1});
+ fields.emplace_back(MappedField{.names = {"data"}, .field_id = 2});
+ return NameMapping::Make(std::move(fields));
+ }
+
+ // Helper function to create a name mapping for complex map types
+ // (array<struct<key,value>>)
+ std::unique_ptr<NameMapping> CreateComplexMapNameMapping() {
+ std::vector<MappedField> fields;
+ fields.emplace_back(MappedField{.names = {"id"}, .field_id = 1});
+
+ // Nested mapping for array element (struct<key,value>)
+ std::vector<MappedField> element_fields;
+ element_fields.emplace_back(MappedField{.names = {"key"}, .field_id = 40});
+ element_fields.emplace_back(MappedField{.names = {"value"}, .field_id =
41});
+ auto element_mapping = MappedFields::Make(std::move(element_fields));
+
+ // Nested mapping for array
+ std::vector<MappedField> array_fields;
+ array_fields.emplace_back(MappedField{.names = {"element"},
+ .field_id = 50,
+ .nested_mapping =
std::move(element_mapping)});
+ auto array_mapping = MappedFields::Make(std::move(array_fields));
+
+ fields.emplace_back(MappedField{
+ .names = {"entries"}, .field_id = 2, .nested_mapping =
std::move(array_mapping)});
+
+ return NameMapping::Make(std::move(fields));
+ }
+};
+
+TEST_F(NameMappingAvroSchemaTest, ApplyNameMappingToRecord) {
+ // Create a simple Avro record schema without field IDs
+ std::string avro_schema_json = R"({
+ "type": "record",
+ "name": "test_record",
+ "fields": [
+ {"name": "id", "type": "int"},
+ {"name": "name", "type": "string"},
+ {"name": "age", "type": "int"}
+ ]
+ })";
+ auto avro_schema = ::avro::compileJsonSchemaFromString(avro_schema_json);
+
+ auto name_mapping = CreateSimpleNameMapping();
+
+ auto result = MakeAvroNodeWithFieldIds(avro_schema.root(), *name_mapping);
+ ASSERT_THAT(result, IsOk());
+
+ const auto& node = *result;
+ EXPECT_EQ(node->type(), ::avro::AVRO_RECORD);
+ EXPECT_EQ(node->names(), 3);
+ EXPECT_EQ(node->leaves(), 3);
+
+ // Check that field IDs are properly applied
+ ASSERT_EQ(node->customAttributes(), 3);
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(node, /*index=*/0, /*field_id=*/1));
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(node, /*index=*/1, /*field_id=*/2));
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(node, /*index=*/2, /*field_id=*/3));
+}
+
+TEST_F(NameMappingAvroSchemaTest, ApplyNameMappingToNestedRecord) {
+ // Create a nested Avro record schema without field IDs
+ std::string avro_schema_json = R"({
+ "type": "record",
+ "name": "test_record",
+ "fields": [
+ {"name": "id", "type": "int"},
+ {"name": "address", "type": {
+ "type": "record",
+ "name": "address",
+ "fields": [
+ {"name": "street", "type": "string"},
+ {"name": "city", "type": "string"},
+ {"name": "zip", "type": "string"}
+ ]
+ }}
+ ]
+ })";
+ auto avro_schema = ::avro::compileJsonSchemaFromString(avro_schema_json);
+
+ auto name_mapping = CreateNestedNameMapping();
+
+ auto result = MakeAvroNodeWithFieldIds(avro_schema.root(), *name_mapping);
+ ASSERT_THAT(result, IsOk());
+
+ const auto& node = *result;
+ EXPECT_EQ(node->type(), ::avro::AVRO_RECORD);
+ EXPECT_EQ(node->names(), 2);
+ EXPECT_EQ(node->leaves(), 2);
+
+ // Check that field IDs are properly applied to top-level fields
+ ASSERT_EQ(node->customAttributes(), 2);
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(node, /*index=*/0, /*field_id=*/1));
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(node, /*index=*/1, /*field_id=*/2));
+
+ // Check nested record
+ const auto& address_node = node->leafAt(1);
+ EXPECT_EQ(address_node->type(), ::avro::AVRO_RECORD);
+ EXPECT_EQ(address_node->names(), 3);
+ EXPECT_EQ(address_node->leaves(), 3);
+
+ // Check that field IDs are properly applied to nested fields
+ ASSERT_EQ(address_node->customAttributes(), 3);
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(address_node, /*index=*/0,
/*field_id=*/10));
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(address_node, /*index=*/1,
/*field_id=*/11));
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(address_node, /*index=*/2,
/*field_id=*/12));
+}
+
+TEST_F(NameMappingAvroSchemaTest, ApplyNameMappingToArray) {
+ // Create an Avro array schema without field IDs
+ std::string avro_schema_json = R"({
+ "type": "record",
+ "name": "test_record",
+ "fields": [
+ {"name": "id", "type": "int"},
+ {"name": "items", "type": {
+ "type": "array",
+ "items": "string"
+ }}
+ ]
+ })";
+ auto avro_schema = ::avro::compileJsonSchemaFromString(avro_schema_json);
+
+ auto name_mapping = CreateArrayNameMapping();
+
+ auto result = MakeAvroNodeWithFieldIds(avro_schema.root(), *name_mapping);
+ ASSERT_THAT(result, IsOk());
+
+ const auto& new_node = *result;
+ EXPECT_EQ(new_node->type(), ::avro::AVRO_RECORD);
+ EXPECT_EQ(new_node->names(), 2);
+ EXPECT_EQ(new_node->leaves(), 2);
+
+ // Check that field IDs are properly applied to top-level fields
+ ASSERT_EQ(new_node->customAttributes(), 2);
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(new_node, /*index=*/0,
/*field_id=*/1));
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(new_node, /*index=*/1,
/*field_id=*/2));
+
+ // Check array field structure and element field ID
+ const auto& array_node = new_node->leafAt(1);
+ EXPECT_EQ(array_node->type(), ::avro::AVRO_ARRAY);
+ EXPECT_EQ(array_node->leaves(), 1);
+
+ // Check that array element has field ID applied
+ const auto& element_node = array_node->leafAt(0);
+ EXPECT_EQ(element_node->type(), ::avro::AVRO_STRING);
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(array_node, /*index=*/0,
/*field_id=*/20,
+ /*key=*/"element-id"));
+}
+
+TEST_F(NameMappingAvroSchemaTest, ApplyNameMappingToMap) {
+ // Create an Avro map schema without field IDs
+ std::string avro_schema_json = R"({
+ "type": "record",
+ "name": "test_record",
+ "fields": [
+ {"name": "id", "type": "int"},
+ {"name": "properties", "type": {
+ "type": "map",
+ "values": "string"
+ }}
+ ]
+ })";
+ auto avro_schema = ::avro::compileJsonSchemaFromString(avro_schema_json);
+
+ auto name_mapping = CreateMapNameMapping();
+
+ auto result = MakeAvroNodeWithFieldIds(avro_schema.root(), *name_mapping);
+ ASSERT_THAT(result, IsOk());
+
+ const auto& new_node = *result;
+ EXPECT_EQ(new_node->type(), ::avro::AVRO_RECORD);
+ EXPECT_EQ(new_node->names(), 2);
+ EXPECT_EQ(new_node->leaves(), 2);
+
+ // Check that field IDs are properly applied to top-level fields
+ ASSERT_EQ(new_node->customAttributes(), 2);
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(new_node, /*index=*/0,
/*field_id=*/1));
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(new_node, /*index=*/1,
/*field_id=*/2));
+
+ // Check map field structure and key-value field IDs
+ const auto& map_node = new_node->leafAt(1);
+ EXPECT_EQ(map_node->type(), ::avro::AVRO_MAP);
+ ASSERT_GE(map_node->leaves(), 2);
+ EXPECT_EQ(map_node->leafAt(0)->type(), ::avro::AVRO_STRING);
+ EXPECT_EQ(map_node->leafAt(1)->type(), ::avro::AVRO_STRING);
+ ASSERT_EQ(map_node->customAttributes(), 2);
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(map_node, /*index=*/0,
/*field_id=*/30,
+ /*key=*/"key-id"));
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(map_node, /*index=*/1,
/*field_id=*/31,
+ /*key=*/"value-id"));
+}
+
+TEST_F(NameMappingAvroSchemaTest, ApplyNameMappingToComplexMap) {
+ // Create an Avro schema for complex map (array<struct<key,value>>) without
field IDs
+ // This represents a map where key is not a string type
+ std::string avro_schema_json = R"({
+ "type": "record",
+ "name": "test_record",
+ "fields": [
+ {"name": "id", "type": "int"},
+ {"name": "entries", "type": {
+ "type": "array",
+ "items": {
+ "type": "record",
+ "name": "entry",
+ "fields": [
+ {"name": "key", "type": "int"},
+ {"name": "value", "type": "string"}
+ ]
+ }
+ }}
+ ]
+ })";
+ auto avro_schema = ::avro::compileJsonSchemaFromString(avro_schema_json);
+
+ auto name_mapping = CreateComplexMapNameMapping();
+
+ auto result = MakeAvroNodeWithFieldIds(avro_schema.root(), *name_mapping);
+ ASSERT_THAT(result, IsOk());
+
+ const auto& new_node = *result;
+ EXPECT_EQ(new_node->type(), ::avro::AVRO_RECORD);
+ EXPECT_EQ(new_node->names(), 2);
+ EXPECT_EQ(new_node->leaves(), 2);
+
+ // Check that field IDs are properly applied to top-level fields
+ ASSERT_EQ(new_node->customAttributes(), 2);
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(new_node, /*index=*/0,
/*field_id=*/1));
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(new_node, /*index=*/1,
/*field_id=*/2));
+
+ // Check array field structure (representing the map)
+ const auto& array_node = new_node->leafAt(1);
+ EXPECT_EQ(array_node->type(), ::avro::AVRO_ARRAY);
+ EXPECT_EQ(array_node->leaves(), 1);
+
+ // Check array element (struct<key,value>)
+ const auto& element_node = array_node->leafAt(0);
+ EXPECT_EQ(element_node->type(), ::avro::AVRO_RECORD);
+ EXPECT_EQ(element_node->names(), 2);
+ EXPECT_EQ(element_node->leaves(), 2);
+
+ // Check that field IDs are properly applied to struct fields
+ ASSERT_EQ(element_node->customAttributes(), 2);
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(element_node, /*index=*/0,
/*field_id=*/40));
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(element_node, /*index=*/1,
/*field_id=*/41));
+
+ // Check key and value types
+ EXPECT_EQ(element_node->leafAt(0)->type(), ::avro::AVRO_INT);
+ EXPECT_EQ(element_node->leafAt(1)->type(), ::avro::AVRO_STRING);
+}
+
+TEST_F(NameMappingAvroSchemaTest, ApplyNameMappingToUnion) {
+ // Create an Avro union schema without field IDs
+ std::string avro_schema_json = R"({
+ "type": "record",
+ "name": "test_record",
+ "fields": [
+ {"name": "id", "type": "int"},
+ {"name": "data", "type": ["null", "string"]}
+ ]
+ })";
+ auto avro_schema = ::avro::compileJsonSchemaFromString(avro_schema_json);
+
+ auto name_mapping = CreateUnionNameMapping();
+
+ auto result = MakeAvroNodeWithFieldIds(avro_schema.root(), *name_mapping);
+ ASSERT_THAT(result, IsOk());
+
+ const auto& new_node = *result;
+ EXPECT_EQ(new_node->type(), ::avro::AVRO_RECORD);
+ EXPECT_EQ(new_node->names(), 2);
+ EXPECT_EQ(new_node->leaves(), 2);
+
+ // Check that field IDs are properly applied to top-level fields
+ ASSERT_EQ(new_node->customAttributes(), 2);
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(new_node, /*index=*/0,
/*field_id=*/1));
+ ASSERT_NO_FATAL_FAILURE(CheckFieldIdAt(new_node, /*index=*/1,
/*field_id=*/2));
+
+ // Check union field
+ const auto& union_node = new_node->leafAt(1);
+ EXPECT_EQ(union_node->type(), ::avro::AVRO_UNION);
+ EXPECT_EQ(union_node->leaves(), 2);
+
+ const auto& non_null_branch = union_node->leafAt(1);
+ EXPECT_EQ(non_null_branch->type(), ::avro::AVRO_STRING);
+}
+
+TEST_F(NameMappingAvroSchemaTest, MissingFieldIdError) {
+ // Create a name mapping with missing field ID
+ std::vector<MappedField> fields;
+ fields.emplace_back(MappedField{.names = {"id"}, .field_id = 1});
+ fields.emplace_back(MappedField{.names = {"name"}}); // Missing field_id
+ auto name_mapping = NameMapping::Make(std::move(fields));
+
+ // Create a simple Avro record schema
+ std::string avro_schema_json = R"({
+ "type": "record",
+ "name": "test_record",
+ "fields": [
+ {"name": "id", "type": "int"},
+ {"name": "name", "type": "string"}
+ ]
+ })";
+ auto avro_schema = ::avro::compileJsonSchemaFromString(avro_schema_json);
+
+ auto result = MakeAvroNodeWithFieldIds(avro_schema.root(), *name_mapping);
+ ASSERT_THAT(result, IsError(ErrorKind::kInvalidSchema));
+}
+
} // namespace iceberg::avro
diff --git a/test/name_mapping_test.cc b/test/name_mapping_test.cc
index 53c9fe0..70f7156 100644
--- a/test/name_mapping_test.cc
+++ b/test/name_mapping_test.cc
@@ -137,6 +137,58 @@ TEST_F(NameMappingTest, FindByNameParts) {
}
}
+TEST_F(NameMappingTest, FindMethodsOnConstObject) {
+ auto mapping = MakeNameMapping();
+ const NameMapping& const_mapping = *mapping;
+
+ // Test Find by ID on const object
+ auto field_by_id = const_mapping.Find(1);
+ ASSERT_TRUE(field_by_id.has_value());
+ EXPECT_EQ(field_by_id->get().field_id, 1);
+ EXPECT_THAT(field_by_id->get().names, testing::UnorderedElementsAre("foo",
"bar"));
+
+ // Test Find by name on const object
+ auto field_by_name = const_mapping.Find("baz");
+ ASSERT_TRUE(field_by_name.has_value());
+ EXPECT_EQ(field_by_name->get().field_id, 2);
+ EXPECT_THAT(field_by_name->get().names,
testing::UnorderedElementsAre("baz"));
+
+ // Test Find by name parts on const object
+ auto field_by_parts = const_mapping.Find(std::vector<std::string>{"qux",
"hello"});
+ ASSERT_TRUE(field_by_parts.has_value());
+ EXPECT_EQ(field_by_parts->get().field_id, 4);
+ EXPECT_THAT(field_by_parts->get().names,
testing::UnorderedElementsAre("hello"));
+
+ // Test Find non-existent field on const object
+ auto non_existent = const_mapping.Find(999);
+ EXPECT_FALSE(non_existent.has_value());
+
+ auto non_existent_name = const_mapping.Find("non_existent");
+ EXPECT_FALSE(non_existent_name.has_value());
+
+ auto non_existent_parts =
+ const_mapping.Find(std::vector<std::string>{"non", "existent"});
+ EXPECT_FALSE(non_existent_parts.has_value());
+}
+
+TEST_F(NameMappingTest, FindMethodsOnConstEmptyMapping) {
+ auto empty_mapping = NameMapping::MakeEmpty();
+ const NameMapping& const_empty_mapping = *empty_mapping;
+
+ // Test Find by ID on const empty mapping
+ auto field_by_id = const_empty_mapping.Find(1);
+ EXPECT_FALSE(field_by_id.has_value());
+
+ // Test Find by name on const empty mapping
+ auto field_by_name = const_empty_mapping.Find("test");
+ EXPECT_FALSE(field_by_name.has_value());
+
+ // Test Find by name parts on const empty mapping
+ auto field_by_parts =
+ const_empty_mapping.Find(std::vector<std::string>{"test", "field"});
+ EXPECT_FALSE(field_by_parts.has_value());
+}
+
TEST_F(NameMappingTest, Equality) {
auto mapping1 = MakeNameMapping();
auto mapping2 = MakeNameMapping();