This is an automated email from the ASF dual-hosted git repository.
gangwu 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 d43455d7 feat: implement UpdateMapping and apply meta change to
UpdateSchema (#561)
d43455d7 is described below
commit d43455d71e8a6cd1fd9e070c94f250f9fa73ae57
Author: Junwang Zhao <[email protected]>
AuthorDate: Fri Mar 13 16:24:18 2026 +0800
feat: implement UpdateMapping and apply meta change to UpdateSchema (#561)
---
src/iceberg/json_serde.cc | 11 ++
src/iceberg/json_serde_internal.h | 14 ++
src/iceberg/name_mapping.cc | 179 ++++++++++++++++++--
src/iceberg/name_mapping.h | 10 +-
src/iceberg/test/CMakeLists.txt | 1 +
src/iceberg/test/name_mapping_update_test.cc | 236 +++++++++++++++++++++++++++
src/iceberg/transaction.cc | 4 +
src/iceberg/update/update_schema.cc | 25 ++-
src/iceberg/update/update_schema.h | 1 +
9 files changed, 465 insertions(+), 16 deletions(-)
diff --git a/src/iceberg/json_serde.cc b/src/iceberg/json_serde.cc
index 7d6c9ee2..2d8c2225 100644
--- a/src/iceberg/json_serde.cc
+++ b/src/iceberg/json_serde.cc
@@ -1270,6 +1270,17 @@ Result<std::unique_ptr<NameMapping>>
NameMappingFromJson(const nlohmann::json& j
return NameMapping::Make(std::move(mapped_fields));
}
+Result<std::string> UpdateMappingFromJsonString(
+ std::string_view mapping_json,
+ const std::unordered_map<int32_t, std::shared_ptr<SchemaField>>& updates,
+ const std::multimap<int32_t, int32_t>& adds) {
+ ICEBERG_ASSIGN_OR_RAISE(auto json,
FromJsonString(std::string(mapping_json)));
+ ICEBERG_ASSIGN_OR_RAISE(auto current_mapping, NameMappingFromJson(json));
+ ICEBERG_ASSIGN_OR_RAISE(auto updated_mapping,
+ UpdateMapping(*current_mapping, updates, adds));
+ return ToJsonString(ToJson(*updated_mapping));
+}
+
nlohmann::json ToJson(const TableIdentifier& identifier) {
nlohmann::json json;
json[kNamespace] = identifier.ns.levels;
diff --git a/src/iceberg/json_serde_internal.h
b/src/iceberg/json_serde_internal.h
index 7b09acdb..8699e3dd 100644
--- a/src/iceberg/json_serde_internal.h
+++ b/src/iceberg/json_serde_internal.h
@@ -19,7 +19,12 @@
#pragma once
+#include <map>
#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <unordered_map>
#include <nlohmann/json_fwd.hpp>
@@ -347,6 +352,15 @@ ICEBERG_EXPORT nlohmann::json ToJson(const NameMapping&
name_mapping);
ICEBERG_EXPORT Result<std::unique_ptr<NameMapping>> NameMappingFromJson(
const nlohmann::json& json);
+/// \brief Update a name mapping from its JSON string and return updated JSON.
+///
+/// Parses the JSON, calls UpdateMapping, and serializes the result.
+/// Returns an error if parsing, mapping update, or serialization fails.
+ICEBERG_EXPORT Result<std::string> UpdateMappingFromJsonString(
+ std::string_view mapping_json,
+ const std::unordered_map<int32_t, std::shared_ptr<SchemaField>>& updates,
+ const std::multimap<int32_t, int32_t>& adds);
+
/// \brief Serializes a `TableIdentifier` object to JSON.
///
/// \param identifier The `TableIdentifier` object to be serialized.
diff --git a/src/iceberg/name_mapping.cc b/src/iceberg/name_mapping.cc
index eaf6199e..ff2d3250 100644
--- a/src/iceberg/name_mapping.cc
+++ b/src/iceberg/name_mapping.cc
@@ -310,29 +310,188 @@ class CreateMappingVisitor {
private:
Status AddMappedField(std::vector<MappedField>& fields, const std::string&
name,
const SchemaField& field) const {
- auto visit_result =
- VisitType(*field.type(), [this](const auto& type) { return
this->Visit(type); });
- ICEBERG_RETURN_UNEXPECTED(visit_result);
+ ICEBERG_ASSIGN_OR_RAISE(
+ auto visit_result,
+ VisitType(*field.type(), [this](const auto& type) { return
this->Visit(type); }));
fields.emplace_back(MappedField{
.names = {name},
.field_id = field.field_id(),
- .nested_mapping = std::move(visit_result.value()),
+ .nested_mapping = std::move(visit_result),
});
return {};
}
};
+// Visitor class for updating name mappings with schema changes
+class UpdateMappingVisitor {
+ public:
+ UpdateMappingVisitor(
+ const std::unordered_map<int32_t, std::shared_ptr<SchemaField>>& updates,
+ const std::multimap<int32_t, int32_t>& adds)
+ : updates_(updates), adds_(adds) {}
+
+ Result<std::unique_ptr<MappedFields>> VisitMapping(const NameMapping&
mapping) {
+ ICEBERG_ASSIGN_OR_RAISE(auto fields_result,
VisitFields(mapping.AsMappedFields()));
+ return AddNewFields(std::move(fields_result), kRootId);
+ }
+
+ private:
+ static constexpr int32_t kRootId = -1;
+
+ Result<std::unique_ptr<MappedFields>> VisitFields(const MappedFields&
fields) {
+ // Recursively visit all fields
+ std::vector<MappedField> field_results;
+ field_results.reserve(fields.Size());
+
+ for (const auto& field : fields.fields()) {
+ ICEBERG_ASSIGN_OR_RAISE(auto field_result, VisitField(field));
+ field_results.push_back(std::move(field_result));
+ }
+
+ // Build update assignments map for removing reassigned names
+ std::unordered_map<std::string, int32_t> update_assignments;
+ for (const auto& field : field_results) {
+ if (field.field_id.has_value()) {
+ auto update_it = updates_.find(field.field_id.value());
+ if (update_it != updates_.end()) {
+ update_assignments.emplace(std::string(update_it->second->name()),
+ field.field_id.value());
+ }
+ }
+ }
+
+ // Remove reassigned names from all fields
+ for (auto& field : field_results) {
+ field = RemoveReassignedNames(field, update_assignments);
+ }
+
+ return MappedFields::Make(std::move(field_results));
+ }
+
+ Result<MappedField> VisitField(const MappedField& field) {
+ // Update this field's names
+ std::unordered_set<std::string> field_names = field.names;
+ if (field.field_id.has_value()) {
+ auto update_it = updates_.find(field.field_id.value());
+ if (update_it != updates_.end()) {
+ field_names.insert(std::string(update_it->second->name()));
+ }
+ }
+
+ std::unique_ptr<MappedFields> nested_mapping = nullptr;
+ if (field.nested_mapping != nullptr) {
+ ICEBERG_ASSIGN_OR_RAISE(nested_mapping,
VisitFields(*field.nested_mapping));
+ }
+
+ // Add a new mapping for any new nested fields
+ if (field.field_id.has_value()) {
+ ICEBERG_ASSIGN_OR_RAISE(nested_mapping,
AddNewFields(std::move(nested_mapping),
+
field.field_id.value()));
+ }
+
+ return MappedField{
+ .names = std::move(field_names),
+ .field_id = field.field_id,
+ .nested_mapping = std::move(nested_mapping),
+ };
+ }
+
+ Result<std::unique_ptr<MappedFields>> AddNewFields(
+ std::unique_ptr<MappedFields> mapping, int32_t parent_id) {
+ auto range = adds_.equal_range(parent_id);
+ std::vector<const SchemaField*> fields_to_add;
+ for (auto it = range.first; it != range.second; ++it) {
+ auto update_it = updates_.find(it->second);
+ if (update_it != updates_.end()) {
+ fields_to_add.push_back(update_it->second.get());
+ }
+ }
+
+ if (fields_to_add.empty()) {
+ return std::move(mapping);
+ }
+
+ std::vector<MappedField> new_fields;
+ CreateMappingVisitor create_visitor;
+ for (const auto* field_to_add : fields_to_add) {
+ ICEBERG_ASSIGN_OR_RAISE(
+ auto nested_result,
+ VisitType(*field_to_add->type(), [&create_visitor](const auto& type)
{
+ return create_visitor.Visit(type);
+ }));
+
+ new_fields.emplace_back(MappedField{
+ .names = {std::string(field_to_add->name())},
+ .field_id = field_to_add->field_id(),
+ .nested_mapping = std::move(nested_result),
+ });
+ }
+
+ if (mapping == nullptr || mapping->Size() == 0) {
+ return MappedFields::Make(std::move(new_fields));
+ }
+
+ // Build assignments map for removing reassigned names
+ std::unordered_map<std::string, int32_t> assignments;
+ for (const auto* field_to_add : fields_to_add) {
+ assignments.emplace(std::string(field_to_add->name()),
field_to_add->field_id());
+ }
+
+ // create a copy of fields that can be updated (append new fields, replace
existing
+ // for reassignment)
+ std::vector<MappedField> fields;
+ fields.reserve(mapping->Size() + new_fields.size());
+ for (const auto& field : mapping->fields()) {
+ fields.push_back(RemoveReassignedNames(field, assignments));
+ }
+
+ fields.insert(fields.end(), std::make_move_iterator(new_fields.begin()),
+ std::make_move_iterator(new_fields.end()));
+
+ return MappedFields::Make(std::move(fields));
+ }
+
+ static MappedField RemoveReassignedNames(
+ const MappedField& field,
+ const std::unordered_map<std::string, int32_t>& assignments) {
+ std::unordered_set<std::string> updated_names = field.names;
+ std::erase_if(updated_names, [&](const std::string& name) {
+ auto assign_it = assignments.find(name);
+ return assign_it != assignments.end() &&
+ (!field.field_id.has_value() || assign_it->second !=
field.field_id.value());
+ });
+ return MappedField{
+ .names = std::move(updated_names),
+ .field_id = field.field_id,
+ .nested_mapping = field.nested_mapping,
+ };
+ }
+
+ const std::unordered_map<int32_t, std::shared_ptr<SchemaField>>& updates_;
+ const std::multimap<int32_t, int32_t>& adds_;
+};
+
} // namespace
Result<std::unique_ptr<NameMapping>> CreateMapping(const Schema& schema) {
CreateMappingVisitor visitor;
- auto result = VisitType(
- schema, [&visitor](const auto& type) ->
Result<std::unique_ptr<MappedFields>> {
- return visitor.Visit(type);
- });
- ICEBERG_RETURN_UNEXPECTED(result);
- return NameMapping::Make(std::move(*result));
+ ICEBERG_ASSIGN_OR_RAISE(
+ auto result,
+ VisitType(schema,
+ [&visitor](const auto& type) ->
Result<std::unique_ptr<MappedFields>> {
+ return visitor.Visit(type);
+ }));
+ return NameMapping::Make(std::move(result));
+}
+
+Result<std::unique_ptr<NameMapping>> UpdateMapping(
+ const NameMapping& mapping,
+ const std::unordered_map<int32_t, std::shared_ptr<SchemaField>>& updates,
+ const std::multimap<int32_t, int32_t>& adds) {
+ UpdateMappingVisitor visitor(updates, adds);
+ ICEBERG_ASSIGN_OR_RAISE(auto result, visitor.VisitMapping(mapping));
+ return NameMapping::Make(std::move(result));
}
} // namespace iceberg
diff --git a/src/iceberg/name_mapping.h b/src/iceberg/name_mapping.h
index 41ff2d14..392b573e 100644
--- a/src/iceberg/name_mapping.h
+++ b/src/iceberg/name_mapping.h
@@ -20,6 +20,7 @@
#pragma once
#include <functional>
+#include <map>
#include <memory>
#include <optional>
#include <span>
@@ -143,16 +144,15 @@ ICEBERG_EXPORT std::string ToString(const NameMapping&
mapping);
/// \return A new NameMapping instance initialized with the schema's fields
and names.
ICEBERG_EXPORT Result<std::unique_ptr<NameMapping>> CreateMapping(const
Schema& schema);
-/// TODO(gangwu): implement this function once SchemaUpdate is supported
-///
/// \brief Update a name-based mapping using changes to a schema.
/// \param mapping a name-based mapping
/// \param updates a map from field ID to updated field definitions
/// \param adds a map from parent field ID to nested fields to be added
/// \return an updated mapping with names added to renamed fields and the
mapping extended
/// for new fields
-// ICEBERG_EXPORT Result<std::unique_ptr<NameMapping>> UpdateMapping(
-// const NameMapping& mapping, const std::map<int32_t, SchemaField>&
updates,
-// const std::multimap<int32_t, int32_t>& adds);
+ICEBERG_EXPORT Result<std::unique_ptr<NameMapping>> UpdateMapping(
+ const NameMapping& mapping,
+ const std::unordered_map<int32_t, std::shared_ptr<SchemaField>>& updates,
+ const std::multimap<int32_t, int32_t>& adds);
} // namespace iceberg
diff --git a/src/iceberg/test/CMakeLists.txt b/src/iceberg/test/CMakeLists.txt
index fdd88888..c4ec29c4 100644
--- a/src/iceberg/test/CMakeLists.txt
+++ b/src/iceberg/test/CMakeLists.txt
@@ -179,6 +179,7 @@ if(ICEBERG_BUILD_BUNDLE)
SOURCES
expire_snapshots_test.cc
fast_append_test.cc
+ name_mapping_update_test.cc
snapshot_manager_test.cc
transaction_test.cc
update_location_test.cc
diff --git a/src/iceberg/test/name_mapping_update_test.cc
b/src/iceberg/test/name_mapping_update_test.cc
new file mode 100644
index 00000000..f63a4c6d
--- /dev/null
+++ b/src/iceberg/test/name_mapping_update_test.cc
@@ -0,0 +1,236 @@
+/*
+ * 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 <memory>
+#include <string>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "iceberg/json_serde_internal.h"
+#include "iceberg/name_mapping.h"
+#include "iceberg/schema.h"
+#include "iceberg/table_properties.h"
+#include "iceberg/test/matchers.h"
+#include "iceberg/test/update_test_base.h"
+#include "iceberg/type.h"
+#include "iceberg/update/update_properties.h"
+#include "iceberg/update/update_schema.h"
+
+namespace iceberg {
+
+class UpdateMappingTest : public UpdateTestBase {};
+
+TEST_F(UpdateMappingTest, AddColumnMappingUpdate) {
+ // Set initial name mapping to match current schema (x, y, z)
+ ICEBERG_UNWRAP_OR_FAIL(auto schema, table_->metadata()->Schema());
+ auto initial_mapping = CreateMapping(*schema);
+ ASSERT_TRUE(initial_mapping.has_value());
+ ICEBERG_UNWRAP_OR_FAIL(auto mapping_json,
ToJsonString(ToJson(**initial_mapping)));
+
+ ICEBERG_UNWRAP_OR_FAIL(auto props_update, table_->NewUpdateProperties());
+ props_update->Set(std::string(TableProperties::kDefaultNameMapping),
+ std::move(mapping_json));
+ EXPECT_THAT(props_update->Commit(), IsOk());
+
+ ICEBERG_UNWRAP_OR_FAIL(auto reloaded, catalog_->LoadTable(table_ident_));
+
+ // Add column ts
+ ICEBERG_UNWRAP_OR_FAIL(auto schema_update, reloaded->NewUpdateSchema());
+ schema_update->AddColumn("ts", timestamp_tz(), "Timestamp");
+ EXPECT_THAT(schema_update->Commit(), IsOk());
+
+ ICEBERG_UNWRAP_OR_FAIL(auto updated_table,
catalog_->LoadTable(table_ident_));
+ auto updated_mapping_str =
updated_table->metadata()->properties.configs().at(
+ std::string(TableProperties::kDefaultNameMapping));
+ ICEBERG_UNWRAP_OR_FAIL(auto json, FromJsonString(updated_mapping_str));
+ ICEBERG_UNWRAP_OR_FAIL(auto updated_mapping, NameMappingFromJson(json));
+
+ auto expected = MappedFields::Make({
+ MappedField{.names = {"x"}, .field_id = 1},
+ MappedField{.names = {"y"}, .field_id = 2},
+ MappedField{.names = {"z"}, .field_id = 3},
+ MappedField{.names = {"ts"}, .field_id = 4},
+ });
+ EXPECT_EQ(updated_mapping->AsMappedFields(), *expected);
+}
+
+TEST_F(UpdateMappingTest, AddNestedColumnMappingUpdate) {
+ // Set initial name mapping (schema has x, y, z)
+ ICEBERG_UNWRAP_OR_FAIL(auto schema, table_->metadata()->Schema());
+ auto initial_mapping = CreateMapping(*schema);
+ ASSERT_TRUE(initial_mapping.has_value());
+ ICEBERG_UNWRAP_OR_FAIL(auto mapping_json,
ToJsonString(ToJson(**initial_mapping)));
+
+ ICEBERG_UNWRAP_OR_FAIL(auto props_update, table_->NewUpdateProperties());
+ props_update->Set(std::string(TableProperties::kDefaultNameMapping),
+ std::move(mapping_json));
+ EXPECT_THAT(props_update->Commit(), IsOk());
+
+ ICEBERG_UNWRAP_OR_FAIL(auto reloaded, catalog_->LoadTable(table_ident_));
+
+ // Add point struct with x, y - mapping updated automatically on commit
+ auto point_struct = std::make_shared<StructType>(std::vector<SchemaField>{
+ SchemaField::MakeRequired(4, "x", float64()),
+ SchemaField::MakeRequired(5, "y", float64()),
+ });
+ ICEBERG_UNWRAP_OR_FAIL(auto add_point, reloaded->NewUpdateSchema());
+ add_point->AddColumn("point", point_struct, "Point struct");
+ EXPECT_THAT(add_point->Commit(), IsOk());
+
+ ICEBERG_UNWRAP_OR_FAIL(auto with_point, catalog_->LoadTable(table_ident_));
+
+ // Add point.z - mapping updated automatically on commit
+ ICEBERG_UNWRAP_OR_FAIL(auto add_z, with_point->NewUpdateSchema());
+ add_z->AddColumn("point", "z", float64(), "Z coordinate");
+ EXPECT_THAT(add_z->Commit(), IsOk());
+
+ ICEBERG_UNWRAP_OR_FAIL(auto with_z, catalog_->LoadTable(table_ident_));
+ auto mapping_after_z = with_z->metadata()->properties.configs().at(
+ std::string(TableProperties::kDefaultNameMapping));
+ ICEBERG_UNWRAP_OR_FAIL(auto json2, FromJsonString(mapping_after_z));
+ ICEBERG_UNWRAP_OR_FAIL(auto mapping2, NameMappingFromJson(json2));
+
+ auto expected_after_z = MappedFields::Make({
+ MappedField{.names = {"x"}, .field_id = 1},
+ MappedField{.names = {"y"}, .field_id = 2},
+ MappedField{.names = {"z"}, .field_id = 3},
+ MappedField{.names = {"point"},
+ .field_id = 4,
+ .nested_mapping = MappedFields::Make({
+ MappedField{.names = {"x"}, .field_id = 5},
+ MappedField{.names = {"y"}, .field_id = 6},
+ MappedField{.names = {"z"}, .field_id = 7},
+ })},
+ });
+ EXPECT_EQ(mapping2->AsMappedFields(), *expected_after_z);
+}
+
+TEST_F(UpdateMappingTest, RenameMappingUpdate) {
+ ICEBERG_UNWRAP_OR_FAIL(auto schema, table_->metadata()->Schema());
+ auto initial_mapping = CreateMapping(*schema);
+ ASSERT_TRUE(initial_mapping.has_value());
+ ICEBERG_UNWRAP_OR_FAIL(auto mapping_json,
ToJsonString(ToJson(**initial_mapping)));
+
+ ICEBERG_UNWRAP_OR_FAIL(auto props_update, table_->NewUpdateProperties());
+ props_update->Set(std::string(TableProperties::kDefaultNameMapping),
+ std::move(mapping_json));
+ EXPECT_THAT(props_update->Commit(), IsOk());
+
+ ICEBERG_UNWRAP_OR_FAIL(auto reloaded, catalog_->LoadTable(table_ident_));
+
+ // Rename x -> x_renamed
+ ICEBERG_UNWRAP_OR_FAIL(auto rename_update, reloaded->NewUpdateSchema());
+ rename_update->RenameColumn("x", "x_renamed");
+ EXPECT_THAT(rename_update->Commit(), IsOk());
+
+ ICEBERG_UNWRAP_OR_FAIL(auto updated_table,
catalog_->LoadTable(table_ident_));
+ auto updated_mapping_str =
updated_table->metadata()->properties.configs().at(
+ std::string(TableProperties::kDefaultNameMapping));
+ ICEBERG_UNWRAP_OR_FAIL(auto json, FromJsonString(updated_mapping_str));
+ ICEBERG_UNWRAP_OR_FAIL(auto updated_mapping, NameMappingFromJson(json));
+
+ // Field 1 should have both names
+ EXPECT_THAT(updated_mapping->Find(1)->get().names,
+ testing::UnorderedElementsAre("x", "x_renamed"));
+}
+
+TEST_F(UpdateMappingTest, RenameNestedFieldMappingUpdate) {
+ auto point_struct = std::make_shared<StructType>(std::vector<SchemaField>{
+ SchemaField::MakeRequired(4, "x", float64()),
+ SchemaField::MakeRequired(5, "y", float64()),
+ });
+ ICEBERG_UNWRAP_OR_FAIL(auto add_point, table_->NewUpdateSchema());
+ add_point->AddColumn("point", point_struct, "Point struct");
+ EXPECT_THAT(add_point->Commit(), IsOk());
+
+ ICEBERG_UNWRAP_OR_FAIL(auto with_point, catalog_->LoadTable(table_ident_));
+ ICEBERG_UNWRAP_OR_FAIL(auto schema, with_point->metadata()->Schema());
+ auto initial_mapping = CreateMapping(*schema);
+ ASSERT_TRUE(initial_mapping.has_value());
+ ICEBERG_UNWRAP_OR_FAIL(auto mapping_json,
ToJsonString(ToJson(**initial_mapping)));
+
+ ICEBERG_UNWRAP_OR_FAIL(auto props_update, with_point->NewUpdateProperties());
+ props_update->Set(std::string(TableProperties::kDefaultNameMapping),
+ std::move(mapping_json));
+ EXPECT_THAT(props_update->Commit(), IsOk());
+
+ ICEBERG_UNWRAP_OR_FAIL(auto with_mapping, catalog_->LoadTable(table_ident_));
+
+ // Rename point.x -> X, point.y -> Y
+ ICEBERG_UNWRAP_OR_FAIL(auto rename_update, with_mapping->NewUpdateSchema());
+ rename_update->RenameColumn("point.x", "X").RenameColumn("point.y", "Y");
+ EXPECT_THAT(rename_update->Commit(), IsOk());
+
+ ICEBERG_UNWRAP_OR_FAIL(auto updated_table,
catalog_->LoadTable(table_ident_));
+ auto updated_mapping_str =
updated_table->metadata()->properties.configs().at(
+ std::string(TableProperties::kDefaultNameMapping));
+ ICEBERG_UNWRAP_OR_FAIL(auto json, FromJsonString(updated_mapping_str));
+ ICEBERG_UNWRAP_OR_FAIL(auto updated_mapping, NameMappingFromJson(json));
+
+ auto point_field = updated_mapping->Find("point");
+ ASSERT_TRUE(point_field.has_value());
+ auto x_field = updated_mapping->Find("point.X");
+ ASSERT_TRUE(x_field.has_value());
+ auto y_field = updated_mapping->Find("point.Y");
+ ASSERT_TRUE(y_field.has_value());
+ EXPECT_THAT(x_field->get().names, testing::UnorderedElementsAre("x", "X"));
+ EXPECT_THAT(y_field->get().names, testing::UnorderedElementsAre("y", "Y"));
+}
+
+TEST_F(UpdateMappingTest, RenameComplexFieldMappingUpdate) {
+ auto point_struct = std::make_shared<StructType>(std::vector<SchemaField>{
+ SchemaField::MakeRequired(4, "x", float64()),
+ SchemaField::MakeRequired(5, "y", float64()),
+ });
+ ICEBERG_UNWRAP_OR_FAIL(auto add_point, table_->NewUpdateSchema());
+ add_point->AddColumn("point", point_struct, "Point struct");
+ EXPECT_THAT(add_point->Commit(), IsOk());
+
+ ICEBERG_UNWRAP_OR_FAIL(auto with_point, catalog_->LoadTable(table_ident_));
+ ICEBERG_UNWRAP_OR_FAIL(auto schema, with_point->metadata()->Schema());
+ auto initial_mapping = CreateMapping(*schema);
+ ASSERT_TRUE(initial_mapping.has_value());
+ ICEBERG_UNWRAP_OR_FAIL(auto mapping_json,
ToJsonString(ToJson(**initial_mapping)));
+
+ ICEBERG_UNWRAP_OR_FAIL(auto props_update, with_point->NewUpdateProperties());
+ props_update->Set(std::string(TableProperties::kDefaultNameMapping),
+ std::move(mapping_json));
+ EXPECT_THAT(props_update->Commit(), IsOk());
+
+ ICEBERG_UNWRAP_OR_FAIL(auto with_mapping, catalog_->LoadTable(table_ident_));
+
+ // Rename point -> p2
+ ICEBERG_UNWRAP_OR_FAIL(auto rename_update, with_mapping->NewUpdateSchema());
+ rename_update->RenameColumn("point", "p2");
+ EXPECT_THAT(rename_update->Commit(), IsOk());
+
+ ICEBERG_UNWRAP_OR_FAIL(auto updated_table,
catalog_->LoadTable(table_ident_));
+ auto updated_mapping_str =
updated_table->metadata()->properties.configs().at(
+ std::string(TableProperties::kDefaultNameMapping));
+ ICEBERG_UNWRAP_OR_FAIL(auto json, FromJsonString(updated_mapping_str));
+ ICEBERG_UNWRAP_OR_FAIL(auto updated_mapping, NameMappingFromJson(json));
+
+ // Field 4 (point) should have both names
+ auto point_field = updated_mapping->Find(4);
+ ASSERT_TRUE(point_field.has_value());
+ EXPECT_THAT(point_field->get().names, testing::UnorderedElementsAre("point",
"p2"));
+}
+
+} // namespace iceberg
diff --git a/src/iceberg/transaction.cc b/src/iceberg/transaction.cc
index 6df45b30..58b0daf9 100644
--- a/src/iceberg/transaction.cc
+++ b/src/iceberg/transaction.cc
@@ -221,6 +221,10 @@ Status Transaction::ApplyUpdateSchema(UpdateSchema&
update) {
ICEBERG_ASSIGN_OR_RAISE(auto result, update.Apply());
metadata_builder_->SetCurrentSchema(std::move(result.schema),
result.new_last_column_id);
+ if (!result.updated_props.empty()) {
+ metadata_builder_->SetProperties(result.updated_props);
+ }
+
return {};
}
diff --git a/src/iceberg/update/update_schema.cc
b/src/iceberg/update/update_schema.cc
index a4c45349..3fdce409 100644
--- a/src/iceberg/update/update_schema.cc
+++ b/src/iceberg/update/update_schema.cc
@@ -29,9 +29,12 @@
#include <utility>
#include <vector>
+#include "iceberg/json_serde_internal.h"
+#include "iceberg/name_mapping.h"
#include "iceberg/schema.h"
#include "iceberg/schema_field.h"
#include "iceberg/table_metadata.h"
+#include "iceberg/table_properties.h"
#include "iceberg/transaction.h"
#include "iceberg/type.h"
#include "iceberg/util/checked_cast.h"
@@ -592,8 +595,28 @@ Result<UpdateSchema::ApplyResult> UpdateSchema::Apply() {
auto new_schema,
Schema::Make(std::move(new_fields), schema_->schema_id(),
fresh_identifier_ids));
+ std::unordered_map<std::string, std::string> updated_props;
+ const auto& base_metadata = base();
+ const auto& properties = base_metadata.properties.configs();
+
+ auto mapping_it =
properties.find(std::string(TableProperties::kDefaultNameMapping));
+ if (mapping_it != properties.end() && !mapping_it->second.empty()) {
+ std::multimap<int32_t, int32_t> adds;
+ for (const auto& [parent_id, child_ids] : parent_to_added_ids_) {
+ std::ranges::for_each(child_ids, [&adds, parent_id](int32_t child_id) {
+ adds.emplace(parent_id, child_id);
+ });
+ }
+ ICEBERG_ASSIGN_OR_RAISE(
+ auto updated_mapping_json,
+ UpdateMappingFromJsonString(mapping_it->second, updates_, adds));
+ updated_props[std::string(TableProperties::kDefaultNameMapping)] =
+ std::move(updated_mapping_json);
+ }
+
return ApplyResult{.schema = std::move(new_schema),
- .new_last_column_id = last_column_id_};
+ .new_last_column_id = last_column_id_,
+ .updated_props = std::move(updated_props)};
}
// TODO(Guotao Yu): v3 default value is not yet supported
diff --git a/src/iceberg/update/update_schema.h
b/src/iceberg/update/update_schema.h
index a1c3e92d..2223c0b8 100644
--- a/src/iceberg/update/update_schema.h
+++ b/src/iceberg/update/update_schema.h
@@ -337,6 +337,7 @@ class ICEBERG_EXPORT UpdateSchema : public PendingUpdate {
struct ApplyResult {
std::shared_ptr<Schema> schema;
int32_t new_last_column_id;
+ std::unordered_map<std::string, std::string> updated_props;
};
/// \brief Apply the pending changes to the original schema and return the
result.