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.

Reply via email to