-
Notifications
You must be signed in to change notification settings - Fork 40
feat: avro support applying field-ids based on name mapping #127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7ca5a53
5466efc
96ddbd9
38c7bb2
1f8eef7
9e96946
0643070
2a44b9e
9d9f521
f7ef8f2
fdec322
a19781e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -31,7 +31,9 @@ | |||||||||||||||||||||||||||||||
#include <avro/ValidSchema.hh> | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
#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" | ||||||||||||||||||||||||||||||||
|
@@ -783,4 +785,225 @@ Result<SchemaProjection> Project(const Schema& expected_schema, | |||||||||||||||||||||||||||||||
return SchemaProjection{std::move(field_projection.children)}; | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
namespace { | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
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); | ||||||||||||||||||||||||||||||||
MisterRaindrop marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||
if (i >= original_node->leaves()) { | ||||||||||||||||||||||||||||||||
return InvalidSchema("Index {} is out of bounds for leaves (size: {})", i, | ||||||||||||||||||||||||||||||||
original_node->leaves()); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
::avro::NodePtr field_node = original_node->leafAt(i); | ||||||||||||||||||||||||||||||||
MisterRaindrop marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
// 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; | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
Comment on lines
+809
to
+818
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This is not a required suggestion but I just found that the refactoring above does the same thing. Perhaps we should extend |
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
if (nested_field) { | ||||||||||||||||||||||||||||||||
// Check if field_id is present | ||||||||||||||||||||||||||||||||
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 | ||||||||||||||||||||||||||||||||
const auto& original_attrs = original_node->customAttributesAt(i); | ||||||||||||||||||||||||||||||||
const auto& existing_attrs = original_attrs.attributes(); | ||||||||||||||||||||||||||||||||
for (const auto& attr_pair : existing_attrs) { | ||||||||||||||||||||||||||||||||
Comment on lines
+831
to
+833
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||
attributes.addAttribute(attr_pair.first, attr_pair.second, false); | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For better readability. |
||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
// Add field ID attribute to the new node (preserving existing attributes) | ||||||||||||||||||||||||||||||||
attributes.addAttribute(std::string(kFieldIdProp), | ||||||||||||||||||||||||||||||||
std::to_string(nested_field->field_id.value()), false); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
new_record_node->addCustomAttributesForField(attributes); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
// Recursively apply field IDs to nested fields | ||||||||||||||||||||||||||||||||
ICEBERG_ASSIGN_OR_RAISE(auto new_nested_node, | ||||||||||||||||||||||||||||||||
CreateAvroNodeWithFieldIds(field_node, *nested_field)); | ||||||||||||||||||||||||||||||||
new_record_node->addName(field_name); | ||||||||||||||||||||||||||||||||
new_record_node->addLeaf(new_nested_node); | ||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||
// If no nested field found, this is an error | ||||||||||||||||||||||||||||||||
return InvalidSchema("Field '{}' not found in nested mapping", field_name); | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is much better to return error immediately after we fail to find the field id. Then we don't need the large if branch at line 817. |
||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
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, | ||||||||||||||||||||||||||||||||
CreateAvroNodeWithFieldIds(original_node->leafAt(0), field)); | ||||||||||||||||||||||||||||||||
new_array_node->addLeaf(new_element_node); | ||||||||||||||||||||||||||||||||
return new_array_node; | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
// For regular arrays, try to find element field ID from nested mapping | ||||||||||||||||||||||||||||||||
const MappedField* element_field = nullptr; | ||||||||||||||||||||||||||||||||
if (field.nested_mapping) { | ||||||||||||||||||||||||||||||||
auto fields_span = field.nested_mapping->fields(); | ||||||||||||||||||||||||||||||||
for (const auto& f : fields_span) { | ||||||||||||||||||||||||||||||||
if (f.names.find(std::string(kElement)) != f.names.end()) { | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we actually need to find the field name of a map or array? IIUC, the field name mapping should only apply to struct fields. We can blindly use the child nested_mapping. |
||||||||||||||||||||||||||||||||
element_field = &f; | ||||||||||||||||||||||||||||||||
break; | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
if (element_field) { | ||||||||||||||||||||||||||||||||
// Check if field_id is present | ||||||||||||||||||||||||||||||||
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, | ||||||||||||||||||||||||||||||||
CreateAvroNodeWithFieldIds(original_node->leafAt(0), *element_field)); | ||||||||||||||||||||||||||||||||
new_array_node->addLeaf(new_element_node); | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we add |
||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||
// If no element field found, this is an error | ||||||||||||||||||||||||||||||||
return InvalidSchema("Element field not found in nested mapping for array"); | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto |
||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
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 use fixed field IDs for key and value | ||||||||||||||||||||||||||||||||
// Key field gets field ID 0, value field gets field ID 1 | ||||||||||||||||||||||||||||||||
constexpr int32_t kMapKeyFieldId = 0; | ||||||||||||||||||||||||||||||||
constexpr int32_t kMapValueFieldId = 1; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
// Create key field with fixed field ID | ||||||||||||||||||||||||||||||||
MappedField key_field; | ||||||||||||||||||||||||||||||||
key_field.field_id = kMapKeyFieldId; | ||||||||||||||||||||||||||||||||
key_field.nested_mapping = | ||||||||||||||||||||||||||||||||
field.nested_mapping; // Pass through nested mapping for complex key types | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
// Create value field with fixed field ID | ||||||||||||||||||||||||||||||||
MappedField value_field; | ||||||||||||||||||||||||||||||||
value_field.field_id = kMapValueFieldId; | ||||||||||||||||||||||||||||||||
value_field.nested_mapping = | ||||||||||||||||||||||||||||||||
field.nested_mapping; // Pass through nested mapping for complex value types | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
// Add key and value nodes | ||||||||||||||||||||||||||||||||
ICEBERG_ASSIGN_OR_RAISE( | ||||||||||||||||||||||||||||||||
auto new_key_node, CreateAvroNodeWithFieldIds(original_node->leafAt(0), key_field)); | ||||||||||||||||||||||||||||||||
ICEBERG_ASSIGN_OR_RAISE( | ||||||||||||||||||||||||||||||||
auto new_value_node, | ||||||||||||||||||||||||||||||||
CreateAvroNodeWithFieldIds(original_node->leafAt(1), value_field)); | ||||||||||||||||||||||||||||||||
new_map_node->addLeaf(new_key_node); | ||||||||||||||||||||||||||||||||
new_map_node->addLeaf(new_value_node); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
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, | ||||||||||||||||||||||||||||||||
CreateAvroNodeWithFieldIds(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, | ||||||||||||||||||||||||||||||||
CreateAvroNodeWithFieldIds(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> CreateAvroNodeWithFieldIds(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)); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
} // namespace iceberg::avro |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Uh oh!
There was an error while loading. Please reload this page.