This is an automated email from the ASF dual-hosted git repository.

blue pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-iceberg.git


The following commit(s) were added to refs/heads/master by this push:
     new 1dda1ad  Add mapping to Iceberg for external name-based schemas (#338)
1dda1ad is described below

commit 1dda1ada9c5b338fd0383b7f528707b8506c49a6
Author: Ryan Blue <[email protected]>
AuthorDate: Wed Jul 31 14:44:05 2019 -0700

    Add mapping to Iceberg for external name-based schemas (#338)
---
 .../main/java/org/apache/iceberg/SchemaUpdate.java |  32 ++-
 .../java/org/apache/iceberg/TableProperties.java   |   2 +
 .../org/apache/iceberg/mapping/MappedField.java    |  96 ++++++++
 .../org/apache/iceberg/mapping/MappedFields.java   | 110 +++++++++
 .../org/apache/iceberg/mapping/MappingUtil.java    | 261 +++++++++++++++++++++
 .../org/apache/iceberg/mapping/NameMapping.java    |  66 ++++++
 .../apache/iceberg/mapping/NameMappingParser.java  | 144 ++++++++++++
 .../java/org/apache/iceberg/util/JsonUtil.java     |  10 +
 .../java/org/apache/iceberg/TableTestBase.java     |   6 +-
 .../test/java/org/apache/iceberg/TestTables.java   |   8 +-
 .../apache/iceberg/mapping/TestMappingUpdates.java | 249 ++++++++++++++++++++
 .../apache/iceberg/mapping/TestNameMapping.java    | 250 ++++++++++++++++++++
 12 files changed, 1226 insertions(+), 8 deletions(-)

diff --git a/core/src/main/java/org/apache/iceberg/SchemaUpdate.java 
b/core/src/main/java/org/apache/iceberg/SchemaUpdate.java
index 48cbb04..d94f449 100644
--- a/core/src/main/java/org/apache/iceberg/SchemaUpdate.java
+++ b/core/src/main/java/org/apache/iceberg/SchemaUpdate.java
@@ -28,9 +28,14 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import org.apache.iceberg.mapping.MappingUtil;
+import org.apache.iceberg.mapping.NameMapping;
+import org.apache.iceberg.mapping.NameMappingParser;
 import org.apache.iceberg.types.Type;
 import org.apache.iceberg.types.TypeUtil;
 import org.apache.iceberg.types.Types;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import static org.apache.iceberg.types.Types.NestedField.optional;
 import static org.apache.iceberg.types.Types.NestedField.required;
@@ -39,6 +44,7 @@ import static 
org.apache.iceberg.types.Types.NestedField.required;
  * Schema evolution API implementation.
  */
 class SchemaUpdate implements UpdateSchema {
+  private static final Logger LOG = 
LoggerFactory.getLogger(SchemaUpdate.class);
   private static final int TABLE_ROOT_ID = -1;
 
   private final TableOperations ops;
@@ -200,7 +206,7 @@ class SchemaUpdate implements UpdateSchema {
 
   @Override
   public void commit() {
-    TableMetadata update = base.updateSchema(apply(), lastColumnId);
+    TableMetadata update = applyChangesToMapping(base.updateSchema(apply(), 
lastColumnId));
     ops.commit(base, update);
   }
 
@@ -210,6 +216,30 @@ class SchemaUpdate implements UpdateSchema {
     return next;
   }
 
+  private TableMetadata applyChangesToMapping(TableMetadata metadata) {
+    String mappingJson = 
metadata.property(TableProperties.DEFAULT_NAME_MAPPING, null);
+    if (mappingJson != null) {
+      try {
+        // parse and update the mapping
+        NameMapping mapping = NameMappingParser.fromJson(mappingJson);
+        NameMapping updated = MappingUtil.update(mapping, updates, adds);
+
+        // replace the table property
+        Map<String, String> updatedProperties = Maps.newHashMap();
+        updatedProperties.putAll(metadata.properties());
+        updatedProperties.put(TableProperties.DEFAULT_NAME_MAPPING, 
NameMappingParser.toJson(updated));
+
+        return metadata.replaceProperties(updatedProperties);
+
+      } catch (RuntimeException e) {
+        // log the error, but do not fail the update
+        LOG.warn("Failed to update external schema mapping: {}", mappingJson, 
e);
+      }
+    }
+
+    return metadata;
+  }
+
   private static Schema applyChanges(Schema schema, List<Integer> deletes,
                                      Map<Integer, Types.NestedField> updates,
                                      Multimap<Integer, Types.NestedField> 
adds) {
diff --git a/core/src/main/java/org/apache/iceberg/TableProperties.java 
b/core/src/main/java/org/apache/iceberg/TableProperties.java
index d426dd3..4dd778d 100644
--- a/core/src/main/java/org/apache/iceberg/TableProperties.java
+++ b/core/src/main/java/org/apache/iceberg/TableProperties.java
@@ -91,4 +91,6 @@ public class TableProperties {
 
   public static final String DEFAULT_WRITE_METRICS_MODE = 
"write.metadata.metrics.default";
   public static final String DEFAULT_WRITE_METRICS_MODE_DEFAULT = 
"truncate(16)";
+
+  public static final String DEFAULT_NAME_MAPPING = 
"schema.name-mapping.default";
 }
diff --git a/core/src/main/java/org/apache/iceberg/mapping/MappedField.java 
b/core/src/main/java/org/apache/iceberg/mapping/MappedField.java
new file mode 100644
index 0000000..2a56afa
--- /dev/null
+++ b/core/src/main/java/org/apache/iceberg/mapping/MappedField.java
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+
+package org.apache.iceberg.mapping;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * An immutable mapping between a field ID and a set of names.
+ */
+public class MappedField {
+
+  static MappedField of(Integer id, String name) {
+    return new MappedField(id, ImmutableSet.of(name), null);
+  }
+
+  static MappedField of(Integer id, Iterable<String> names) {
+    return new MappedField(id, names, null);
+  }
+
+  static MappedField of(Integer id, String name, MappedFields nestedMapping) {
+    return new MappedField(id, ImmutableSet.of(name), nestedMapping);
+  }
+
+  static MappedField of(Integer id, Iterable<String> names, MappedFields 
nestedMapping) {
+    return new MappedField(id, names, nestedMapping);
+  }
+
+  private final Set<String> names;
+  private Integer id;
+  private MappedFields nestedMapping;
+
+  private MappedField(Integer id, Iterable<String> names, MappedFields nested) 
{
+    this.id = id;
+    this.names = ImmutableSet.copyOf(names);
+    this.nestedMapping = nested;
+  }
+
+  public Integer id() {
+    return id;
+  }
+
+  public Set<String> names() {
+    return names;
+  }
+
+  public MappedFields nestedMapping() {
+    return nestedMapping;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (this == other) {
+      return true;
+    }
+
+    if (other == null || getClass() != other.getClass()) {
+      return false;
+    }
+
+    MappedField that = (MappedField) other;
+    return names.equals(that.names) &&
+        Objects.equals(id, that.id) &&
+        Objects.equals(nestedMapping, that.nestedMapping);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(names, id, nestedMapping);
+  }
+
+  @Override
+  public String toString() {
+    return "([" + Joiner.on(", ").join(names) + "] -> " + (id != null ? id : 
"?") +
+        (nestedMapping != null ? ", " + nestedMapping + ")" : ")");
+  }
+}
diff --git a/core/src/main/java/org/apache/iceberg/mapping/MappedFields.java 
b/core/src/main/java/org/apache/iceberg/mapping/MappedFields.java
new file mode 100644
index 0000000..824257b
--- /dev/null
+++ b/core/src/main/java/org/apache/iceberg/mapping/MappedFields.java
@@ -0,0 +1,110 @@
+/*
+ * 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.
+ */
+
+package org.apache.iceberg.mapping;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class MappedFields {
+
+  static MappedFields of(MappedField... fields) {
+    return new MappedFields(ImmutableList.copyOf(fields));
+  }
+
+  static MappedFields of(List<MappedField> fields) {
+    return new MappedFields(fields);
+  }
+
+  private final List<MappedField> fields;
+  private final Map<String, Integer> nameToId;
+  private final Map<Integer, MappedField> idToField;
+
+  private MappedFields(List<MappedField> fields) {
+    this.fields = ImmutableList.copyOf(fields);
+    this.nameToId = indexIds(fields);
+    this.idToField = indexFields(fields);
+  }
+
+  public MappedField field(int id) {
+    return idToField.get(id);
+  }
+
+  public Integer id(String name) {
+    return nameToId.get(name);
+  }
+
+  public int size() {
+    return fields.size();
+  }
+
+  private static Map<String, Integer> indexIds(List<MappedField> fields) {
+    ImmutableMap.Builder<String, Integer> builder = ImmutableMap.builder();
+    fields.forEach(field ->
+        field.names().forEach(name -> {
+          Integer id = field.id();
+          if (id != null) {
+            builder.put(name, id);
+          }
+        }));
+    return builder.build();
+  }
+
+  private static Map<Integer, MappedField> indexFields(List<MappedField> 
fields) {
+    ImmutableMap.Builder<Integer, MappedField> builder = 
ImmutableMap.builder();
+    fields.forEach(field -> {
+      Integer id = field.id();
+      if (id != null) {
+        builder.put(id, field);
+      }
+    });
+    return builder.build();
+  }
+
+  public List<MappedField> fields() {
+    return fields;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (this == other) {
+      return true;
+    }
+
+    if (other == null || getClass() != other.getClass()) {
+      return false;
+    }
+
+    return fields.equals(((MappedFields) other).fields);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(fields);
+  }
+
+  @Override
+  public String toString() {
+    return "[ " + Joiner.on(", ").join(fields) + " ]";
+  }
+}
diff --git a/core/src/main/java/org/apache/iceberg/mapping/MappingUtil.java 
b/core/src/main/java/org/apache/iceberg/mapping/MappingUtil.java
new file mode 100644
index 0000000..95d8b4e
--- /dev/null
+++ b/core/src/main/java/org/apache/iceberg/mapping/MappingUtil.java
@@ -0,0 +1,261 @@
+/*
+ * 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.
+ */
+
+package org.apache.iceberg.mapping;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.types.Type;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.types.Types;
+
+public class MappingUtil {
+  private static final Joiner DOT = Joiner.on('.');
+
+  private MappingUtil() {
+  }
+
+  /**
+   * Create a name-based mapping for a schema.
+   * <p>
+   * The mapping returned by this method will use the schema's name for each 
field.
+   *
+   * @param schema a {@link Schema}
+   * @return a {@link NameMapping} initialized with the schema's fields and 
names
+   */
+  public static NameMapping create(Schema schema) {
+    return new NameMapping(TypeUtil.visit(schema, CreateMapping.INSTANCE));
+  }
+
+  /**
+   * 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
+   */
+  public static NameMapping update(NameMapping mapping,
+                            Map<Integer, Types.NestedField> updates,
+                            Multimap<Integer, Types.NestedField> adds) {
+    return new NameMapping(visit(mapping, new UpdateMapping(updates, adds)));
+  }
+
+  static Map<Integer, MappedField> indexById(MappedFields mapping) {
+    return visit(mapping, new IndexById());
+  }
+
+  static Map<String, MappedField> indexByName(MappedFields mapping) {
+    return visit(mapping, IndexByName.INSTANCE);
+  }
+
+  private static class UpdateMapping implements Visitor<MappedFields, 
MappedField> {
+    private final Map<Integer, Types.NestedField> updates;
+    private final Multimap<Integer, Types.NestedField> adds;
+
+    private UpdateMapping(Map<Integer, Types.NestedField> updates, 
Multimap<Integer, Types.NestedField> adds) {
+      this.updates = updates;
+      this.adds = adds;
+    }
+
+    @Override
+    public MappedFields mapping(NameMapping mapping, MappedFields result) {
+      return addNewFields(result, -1 /* parent ID used to add top-level fields 
*/);
+    }
+
+    @Override
+    public MappedFields fields(MappedFields fields, List<MappedField> 
fieldResults) {
+      return MappedFields.of(fieldResults);
+    }
+
+    @Override
+    public MappedField field(MappedField field, MappedFields fieldResult) {
+      // update this field's names
+      Set<String> fieldNames = Sets.newHashSet(field.names());
+      Types.NestedField update = updates.get(field.id());
+      if (update != null) {
+        fieldNames.add(update.name());
+      }
+
+      // add a new mapping for any new nested fields
+      MappedFields nestedMapping = addNewFields(fieldResult, field.id());
+      return MappedField.of(field.id(), fieldNames, nestedMapping);
+    }
+
+    private MappedFields addNewFields(MappedFields mapping, int parentId) {
+      Collection<Types.NestedField> fieldsToAdd = adds.get(parentId);
+      if (fieldsToAdd == null || fieldsToAdd.isEmpty()) {
+        return mapping;
+      }
+
+      List<MappedField> fields = Lists.newArrayList();
+      if (mapping != null) {
+        fields.addAll(mapping.fields());
+      }
+
+      for (Types.NestedField add : fieldsToAdd) {
+        MappedFields nestedMapping = TypeUtil.visit(add.type(), 
CreateMapping.INSTANCE);
+        fields.add(MappedField.of(add.fieldId(), add.name(), nestedMapping));
+      }
+
+      return MappedFields.of(fields);
+    }
+  }
+
+  private static class IndexByName implements Visitor<Map<String, 
MappedField>, Map<String, MappedField>> {
+    static final IndexByName INSTANCE = new IndexByName();
+
+    @Override
+    public Map<String, MappedField> mapping(NameMapping mapping, Map<String, 
MappedField> result) {
+      return result;
+    }
+
+    @Override
+    public Map<String, MappedField> fields(MappedFields fields, 
List<Map<String, MappedField>> fieldResults) {
+      // merge the results of each field
+      ImmutableMap.Builder<String, MappedField> builder = 
ImmutableMap.builder();
+      for (Map<String, MappedField> results : fieldResults) {
+        builder.putAll(results);
+      }
+      return builder.build();
+    }
+
+    @Override
+    public Map<String, MappedField> field(MappedField field, Map<String, 
MappedField> fieldResult) {
+      ImmutableMap.Builder<String, MappedField> builder = 
ImmutableMap.builder();
+
+      if (fieldResult != null) {
+        for (String name : field.names()) {
+          for (Map.Entry<String, MappedField> entry : fieldResult.entrySet()) {
+            String fullName = DOT.join(name, entry.getKey());
+            builder.put(fullName, entry.getValue());
+          }
+        }
+      }
+
+      for (String name : field.names()) {
+        builder.put(name, field);
+      }
+
+      return builder.build();
+    }
+  }
+
+  private static class IndexById implements Visitor<Map<Integer, MappedField>, 
Map<Integer, MappedField>> {
+    private final Map<Integer, MappedField> result = Maps.newHashMap();
+
+    @Override
+    public Map<Integer, MappedField> mapping(NameMapping mapping, Map<Integer, 
MappedField> fieldsResult) {
+      return fieldsResult;
+    }
+
+    @Override
+    public Map<Integer, MappedField> fields(MappedFields fields, 
List<Map<Integer, MappedField>> fieldResults) {
+      return result;
+    }
+
+    @Override
+    public Map<Integer, MappedField> field(MappedField field, Map<Integer, 
MappedField> fieldResult) {
+      Preconditions.checkState(!result.containsKey(field.id()), "Invalid 
mapping: ID %s is not unique", field.id());
+      result.put(field.id(), field);
+      return result;
+    }
+  }
+
+  private interface Visitor<S, T> {
+    S mapping(NameMapping mapping, S result);
+    S fields(MappedFields fields, List<T> fieldResults);
+    T field(MappedField field, S fieldResult);
+  }
+
+  private static <S, T> S visit(NameMapping mapping, Visitor<S, T> visitor) {
+    return visitor.mapping(mapping, visit(mapping.asMappedFields(), visitor));
+  }
+
+  private static <S, T> S visit(MappedFields mapping, Visitor<S, T> visitor) {
+    if (mapping == null) {
+      return null;
+    }
+
+    List<T> fieldResults = Lists.newArrayList();
+    for (MappedField field : mapping.fields()) {
+      fieldResults.add(visitor.field(field, visit(field.nestedMapping(), 
visitor)));
+    }
+
+    return visitor.fields(mapping, fieldResults);
+  }
+
+  private static class CreateMapping extends 
TypeUtil.SchemaVisitor<MappedFields> {
+    private static final CreateMapping INSTANCE = new CreateMapping();
+
+    private CreateMapping() {
+    }
+
+    @Override
+    public MappedFields schema(Schema schema, MappedFields structResult) {
+      return structResult;
+    }
+
+    @Override
+    public MappedFields struct(Types.StructType struct, List<MappedFields> 
fieldResults) {
+      List<MappedField> fields = 
Lists.newArrayListWithExpectedSize(fieldResults.size());
+
+      for (int i = 0; i < fieldResults.size(); i += 1) {
+        Types.NestedField field = struct.fields().get(i);
+        MappedFields result = fieldResults.get(i);
+        fields.add(MappedField.of(field.fieldId(), field.name(), result));
+      }
+
+      return MappedFields.of(fields);
+    }
+
+    @Override
+    public MappedFields field(Types.NestedField field, MappedFields 
fieldResult) {
+      return fieldResult;
+    }
+
+    @Override
+    public MappedFields list(Types.ListType list, MappedFields elementResult) {
+      return MappedFields.of(MappedField.of(list.elementId(), "element", 
elementResult));
+    }
+
+    @Override
+    public MappedFields map(Types.MapType map, MappedFields keyResult, 
MappedFields valueResult) {
+      return MappedFields.of(
+          MappedField.of(map.keyId(), "key", keyResult),
+          MappedField.of(map.valueId(), "value", valueResult)
+      );
+    }
+
+    @Override
+    public MappedFields primitive(Type.PrimitiveType primitive) {
+      return null; // no mapping because primitives have no nested fields
+    }
+  }
+}
diff --git a/core/src/main/java/org/apache/iceberg/mapping/NameMapping.java 
b/core/src/main/java/org/apache/iceberg/mapping/NameMapping.java
new file mode 100644
index 0000000..2d8d4d8
--- /dev/null
+++ b/core/src/main/java/org/apache/iceberg/mapping/NameMapping.java
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+
+package org.apache.iceberg.mapping;
+
+import com.google.common.base.Joiner;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a mapping from external schema names to Iceberg type IDs.
+ */
+public class NameMapping {
+  private static final Joiner DOT = Joiner.on('.');
+
+  private final MappedFields mapping;
+  private final Map<Integer, MappedField> fieldsById;
+  private final Map<String, MappedField> fieldsByName;
+
+  NameMapping(MappedFields mapping) {
+    this.mapping = mapping;
+    this.fieldsById = MappingUtil.indexById(mapping);
+    this.fieldsByName = MappingUtil.indexByName(mapping);
+  }
+
+  public MappedField find(int id) {
+    return fieldsById.get(id);
+  }
+
+  public MappedField find(String... names) {
+    return fieldsByName.get(DOT.join(names));
+  }
+
+  public MappedField find(List<String> names) {
+    return fieldsByName.get(DOT.join(names));
+  }
+
+  public MappedFields asMappedFields() {
+    return mapping;
+  }
+
+  @Override
+  public String toString() {
+    if (mapping.fields().isEmpty()) {
+      return "[]";
+    } else {
+      return "[\n  " + Joiner.on("\n  ").join(mapping.fields()) + "\n]";
+    }
+  }
+}
diff --git 
a/core/src/main/java/org/apache/iceberg/mapping/NameMappingParser.java 
b/core/src/main/java/org/apache/iceberg/mapping/NameMappingParser.java
new file mode 100644
index 0000000..453165c
--- /dev/null
+++ b/core/src/main/java/org/apache/iceberg/mapping/NameMappingParser.java
@@ -0,0 +1,144 @@
+/*
+ * 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.
+ */
+
+package org.apache.iceberg.mapping;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.List;
+import java.util.Set;
+import org.apache.iceberg.exceptions.RuntimeIOException;
+import org.apache.iceberg.util.JsonUtil;
+
+/**
+ * Parses external name mappings from a JSON representation.
+ * <pre>
+ * [ { "field-id": 1, "names": ["id", "record_id"] },
+ *   { "field-id": 2, "names": ["data"] },
+ *   { "field-id": 3, "names": ["location"], "fields": [
+ *       { "field-id": 4, "names": ["latitude", "lat"] },
+ *       { "field-id": 5, "names": ["longitude", "long"] }
+ *     ] } ]
+ * </pre>
+ */
+public class NameMappingParser {
+
+  private NameMappingParser() {
+  }
+
+  private static final String FIELD_ID = "field-id";
+  private static final String NAMES = "names";
+  private static final String FIELDS = "fields";
+
+  public static String toJson(NameMapping mapping) {
+    try {
+      StringWriter writer = new StringWriter();
+      JsonGenerator generator = JsonUtil.factory().createGenerator(writer);
+      generator.useDefaultPrettyPrinter();
+      toJson(mapping, generator);
+      generator.flush();
+      return writer.toString();
+    } catch (IOException e) {
+      throw new RuntimeIOException(e, "Failed to write json for: %s", mapping);
+    }
+  }
+
+  static void toJson(NameMapping nameMapping, JsonGenerator generator) throws 
IOException {
+    toJson(nameMapping.asMappedFields(), generator);
+  }
+
+  private static void toJson(MappedFields mapping, JsonGenerator generator) 
throws IOException {
+    generator.writeStartArray();
+
+    for (MappedField field : mapping.fields()) {
+      toJson(field, generator);
+    }
+
+    generator.writeEndArray();
+  }
+
+  private static void toJson(MappedField field, JsonGenerator generator) 
throws IOException {
+    generator.writeStartObject();
+
+    generator.writeNumberField(FIELD_ID, field.id());
+
+    generator.writeArrayFieldStart(NAMES);
+    for (String name : field.names()) {
+      generator.writeString(name);
+    }
+    generator.writeEndArray();
+
+    MappedFields nested = field.nestedMapping();
+    if (nested != null) {
+      generator.writeFieldName(FIELDS);
+      toJson(nested, generator);
+    }
+
+    generator.writeEndObject();
+  }
+
+  public static NameMapping fromJson(String json) {
+    try {
+      return fromJson(JsonUtil.mapper().readValue(json, JsonNode.class));
+    } catch (IOException e) {
+      throw new RuntimeIOException(e, "Failed to convert version from json: 
%s", json);
+    }
+  }
+
+  static NameMapping fromJson(JsonNode node) {
+    return new NameMapping(fieldsFromJson(node));
+  }
+
+  private static MappedFields fieldsFromJson(JsonNode node) {
+    Preconditions.checkArgument(node.isArray(), "Cannot parse non-array 
mapping fields: %s", node);
+
+    List<MappedField> fields = Lists.newArrayList();
+    node.elements().forEachRemaining(fieldNode -> 
fields.add(fieldFromJson(fieldNode)));
+
+    return MappedFields.of(fields);
+  }
+
+  private static MappedField fieldFromJson(JsonNode node) {
+    Preconditions.checkArgument(node != null && !node.isNull() && 
node.isObject(),
+        "Cannot parse non-object mapping field: %s", node);
+
+    Integer id = JsonUtil.getIntOrNull(FIELD_ID, node);
+
+    Set<String> names;
+    if (node.has(NAMES)) {
+      names = ImmutableSet.copyOf(JsonUtil.getStringList(NAMES, node));
+    } else {
+      names = ImmutableSet.of();
+    }
+
+    MappedFields nested;
+    if (node.has(FIELDS)) {
+      nested = fieldsFromJson(node.get(FIELDS));
+    } else {
+      nested = null;
+    }
+
+    return MappedField.of(id, names, nested);
+  }
+}
diff --git a/core/src/main/java/org/apache/iceberg/util/JsonUtil.java 
b/core/src/main/java/org/apache/iceberg/util/JsonUtil.java
index 09a5466..976a1cc 100644
--- a/core/src/main/java/org/apache/iceberg/util/JsonUtil.java
+++ b/core/src/main/java/org/apache/iceberg/util/JsonUtil.java
@@ -52,6 +52,16 @@ public class JsonUtil {
     return pNode.asInt();
   }
 
+  public static Integer getIntOrNull(String property, JsonNode node) {
+    if (!node.has(property)) {
+      return null;
+    }
+    JsonNode pNode = node.get(property);
+    Preconditions.checkArgument(pNode != null && !pNode.isNull() && 
pNode.isIntegralNumber() && pNode.canConvertToInt(),
+        "Cannot parse %s from non-string value: %s", property, pNode);
+    return pNode.asInt();
+  }
+
   public static long getLong(String property, JsonNode node) {
     Preconditions.checkArgument(node.has(property), "Cannot parse missing int 
%s", property);
     JsonNode pNode = node.get(property);
diff --git a/core/src/test/java/org/apache/iceberg/TableTestBase.java 
b/core/src/test/java/org/apache/iceberg/TableTestBase.java
index 6c4f682..c55c07d 100644
--- a/core/src/test/java/org/apache/iceberg/TableTestBase.java
+++ b/core/src/test/java/org/apache/iceberg/TableTestBase.java
@@ -40,7 +40,7 @@ import static 
org.apache.iceberg.types.Types.NestedField.required;
 
 public class TableTestBase {
   // Schema passed to create tables
-  static final Schema SCHEMA = new Schema(
+  public static final Schema SCHEMA = new Schema(
       required(3, "id", Types.IntegerType.get()),
       required(4, "data", Types.StringType.get())
   );
@@ -80,7 +80,7 @@ public class TableTestBase {
 
   File tableDir = null;
   File metadataDir = null;
-  TestTables.TestTable table = null;
+  public TestTables.TestTable table = null;
 
   @Before
   public void setupTable() throws Exception {
@@ -117,7 +117,7 @@ public class TableTestBase {
     return TestTables.metadataVersion("test");
   }
 
-  TableMetadata readMetadata() {
+  public TableMetadata readMetadata() {
     return TestTables.readMetadata("test");
   }
 
diff --git a/core/src/test/java/org/apache/iceberg/TestTables.java 
b/core/src/test/java/org/apache/iceberg/TestTables.java
index 4275205..13e336e 100644
--- a/core/src/test/java/org/apache/iceberg/TestTables.java
+++ b/core/src/test/java/org/apache/iceberg/TestTables.java
@@ -38,7 +38,7 @@ public class TestTables {
 
   private TestTables() {}
 
-  static TestTable create(File temp, String name, Schema schema, PartitionSpec 
spec) {
+  public static TestTable create(File temp, String name, Schema schema, 
PartitionSpec spec) {
     TestTableOperations ops = new TestTableOperations(name, temp);
     if (ops.current() != null) {
       throw new AlreadyExistsException("Table %s already exists at location: 
%s", name, temp);
@@ -47,7 +47,7 @@ public class TestTables {
     return new TestTable(ops, name);
   }
 
-  static Transaction beginCreate(File temp, String name, Schema schema, 
PartitionSpec spec) {
+  public static Transaction beginCreate(File temp, String name, Schema schema, 
PartitionSpec spec) {
     TableOperations ops = new TestTableOperations(name, temp);
     if (ops.current() != null) {
       throw new AlreadyExistsException("Table %s already exists at location: 
%s", name, temp);
@@ -77,12 +77,12 @@ public class TestTables {
     }
   }
 
-  static TestTable load(File temp, String name) {
+  public static TestTable load(File temp, String name) {
     TestTableOperations ops = new TestTableOperations(name, temp);
     return new TestTable(ops, name);
   }
 
-  static class TestTable extends BaseTable {
+  public static class TestTable extends BaseTable {
     private final TestTableOperations ops;
 
     private TestTable(TestTableOperations ops, String name) {
diff --git 
a/core/src/test/java/org/apache/iceberg/mapping/TestMappingUpdates.java 
b/core/src/test/java/org/apache/iceberg/mapping/TestMappingUpdates.java
new file mode 100644
index 0000000..bd39cb0
--- /dev/null
+++ b/core/src/test/java/org/apache/iceberg/mapping/TestMappingUpdates.java
@@ -0,0 +1,249 @@
+/*
+ * 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.
+ */
+
+package org.apache.iceberg.mapping;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TableTestBase;
+import org.apache.iceberg.types.Types;
+import org.junit.Assert;
+import org.junit.Test;
+
+import static org.apache.iceberg.types.Types.NestedField.required;
+
+public class TestMappingUpdates extends TableTestBase {
+  @Test
+  public void testAddColumnMappingUpdate() {
+    NameMapping mapping = MappingUtil.create(table.schema());
+    table.updateProperties()
+        .set(TableProperties.DEFAULT_NAME_MAPPING, 
NameMappingParser.toJson(mapping))
+        .commit();
+
+    Assert.assertEquals(
+        MappedFields.of(
+            MappedField.of(1, "id"),
+            MappedField.of(2, "data")),
+        mapping.asMappedFields());
+
+    table.updateSchema()
+        .addColumn("ts", Types.TimestampType.withZone())
+        .commit();
+
+    NameMapping updated = 
NameMappingParser.fromJson(table.properties().get(TableProperties.DEFAULT_NAME_MAPPING));
+
+    Assert.assertEquals(
+        MappedFields.of(
+            MappedField.of(1, "id"),
+            MappedField.of(2, "data"),
+            MappedField.of(3, "ts")),
+        updated.asMappedFields());
+  }
+
+  @Test
+  public void testAddNestedColumnMappingUpdate() {
+    NameMapping mapping = MappingUtil.create(table.schema());
+    table.updateProperties()
+        .set(TableProperties.DEFAULT_NAME_MAPPING, 
NameMappingParser.toJson(mapping))
+        .commit();
+
+    Assert.assertEquals(
+        MappedFields.of(
+            MappedField.of(1, "id"),
+            MappedField.of(2, "data")),
+        mapping.asMappedFields());
+
+    table.updateSchema()
+        .addColumn("point", Types.StructType.of(
+            required(1, "x", Types.DoubleType.get()),
+            required(2, "y", Types.DoubleType.get())))
+        .commit();
+
+    NameMapping updated = 
NameMappingParser.fromJson(table.properties().get(TableProperties.DEFAULT_NAME_MAPPING));
+
+    Assert.assertEquals(
+        MappedFields.of(
+            MappedField.of(1, "id"),
+            MappedField.of(2, "data"),
+            MappedField.of(3, "point", MappedFields.of(
+                MappedField.of(4, "x"),
+                MappedField.of(5, "y")
+            ))),
+        updated.asMappedFields());
+
+    table.updateSchema()
+        .addColumn("point", "z", Types.DoubleType.get())
+        .commit();
+
+    NameMapping pointUpdated = 
NameMappingParser.fromJson(table.properties().get(TableProperties.DEFAULT_NAME_MAPPING));
+
+    Assert.assertEquals(
+        MappedFields.of(
+            MappedField.of(1, "id"),
+            MappedField.of(2, "data"),
+            MappedField.of(3, "point", MappedFields.of(
+                MappedField.of(4, "x"),
+                MappedField.of(5, "y"),
+                MappedField.of(6, "z")
+            ))),
+        pointUpdated.asMappedFields());
+  }
+
+  @Test
+  public void testRenameMappingUpdate() {
+    NameMapping mapping = MappingUtil.create(table.schema());
+    table.updateProperties()
+        .set(TableProperties.DEFAULT_NAME_MAPPING, 
NameMappingParser.toJson(mapping))
+        .commit();
+
+    Assert.assertEquals(
+        MappedFields.of(
+            MappedField.of(1, "id"),
+            MappedField.of(2, "data")),
+        mapping.asMappedFields());
+
+    table.updateSchema()
+        .renameColumn("id", "object_id")
+        .commit();
+
+    NameMapping updated = 
NameMappingParser.fromJson(table.properties().get(TableProperties.DEFAULT_NAME_MAPPING));
+
+    Assert.assertEquals(
+        MappedFields.of(
+            MappedField.of(1, ImmutableList.of("id", "object_id")),
+            MappedField.of(2, "data")),
+        updated.asMappedFields());
+  }
+
+  @Test
+  public void testRenameNestedFieldMappingUpdate() {
+    NameMapping mapping = MappingUtil.create(table.schema());
+    table.updateProperties()
+        .set(TableProperties.DEFAULT_NAME_MAPPING, 
NameMappingParser.toJson(mapping))
+        .commit();
+
+    table.updateSchema()
+        .addColumn("point", Types.StructType.of(
+            required(1, "x", Types.DoubleType.get()),
+            required(2, "y", Types.DoubleType.get())))
+        .commit();
+
+    NameMapping updated = 
NameMappingParser.fromJson(table.properties().get(TableProperties.DEFAULT_NAME_MAPPING));
+
+    Assert.assertEquals(
+        MappedFields.of(
+            MappedField.of(1, "id"),
+            MappedField.of(2, "data"),
+            MappedField.of(3, "point", MappedFields.of(
+                MappedField.of(4, "x"),
+                MappedField.of(5, "y")
+            ))),
+        updated.asMappedFields());
+
+    table.updateSchema()
+        .renameColumn("point.x", "X")
+        .renameColumn("point.y", "Y")
+        .commit();
+
+    NameMapping pointUpdated = 
NameMappingParser.fromJson(table.properties().get(TableProperties.DEFAULT_NAME_MAPPING));
+
+    Assert.assertEquals(
+        MappedFields.of(
+            MappedField.of(1, "id"),
+            MappedField.of(2, "data"),
+            MappedField.of(3, "point", MappedFields.of(
+                MappedField.of(4, ImmutableList.of("x", "X")),
+                MappedField.of(5, ImmutableList.of("y", "Y"))
+            ))),
+        pointUpdated.asMappedFields());
+  }
+
+
+  @Test
+  public void testRenameComplexFieldMappingUpdate() {
+    NameMapping mapping = MappingUtil.create(table.schema());
+    table.updateProperties()
+        .set(TableProperties.DEFAULT_NAME_MAPPING, 
NameMappingParser.toJson(mapping))
+        .commit();
+
+    table.updateSchema()
+        .addColumn("point", Types.StructType.of(
+            required(1, "x", Types.DoubleType.get()),
+            required(2, "y", Types.DoubleType.get())))
+        .commit();
+
+    NameMapping updated = 
NameMappingParser.fromJson(table.properties().get(TableProperties.DEFAULT_NAME_MAPPING));
+
+    Assert.assertEquals(
+        MappedFields.of(
+            MappedField.of(1, "id"),
+            MappedField.of(2, "data"),
+            MappedField.of(3, "point", MappedFields.of(
+                MappedField.of(4, "x"),
+                MappedField.of(5, "y")
+            ))),
+        updated.asMappedFields());
+
+    table.updateSchema()
+        .renameColumn("point", "p2")
+        .commit();
+
+    NameMapping pointUpdated = 
NameMappingParser.fromJson(table.properties().get(TableProperties.DEFAULT_NAME_MAPPING));
+
+    Assert.assertEquals(
+        MappedFields.of(
+            MappedField.of(1, "id"),
+            MappedField.of(2, "data"),
+            MappedField.of(3, ImmutableList.of("point", "p2"), MappedFields.of(
+                MappedField.of(4, "x"),
+                MappedField.of(5, "y")
+            ))),
+        pointUpdated.asMappedFields());
+  }
+
+  @Test
+  public void testMappingUpdateFailureSkipsMappingUpdate() {
+    NameMapping mapping = MappingUtil.create(table.schema());
+    table.updateProperties()
+        .set(TableProperties.DEFAULT_NAME_MAPPING, 
NameMappingParser.toJson(mapping))
+        .commit();
+
+    table.updateSchema()
+        .renameColumn("id", "object_id")
+        .commit();
+
+    String updatedJson = 
table.properties().get(TableProperties.DEFAULT_NAME_MAPPING);
+    NameMapping updated = NameMappingParser.fromJson(updatedJson);
+
+    Assert.assertEquals(
+        MappedFields.of(
+            MappedField.of(1, ImmutableList.of("id", "object_id")),
+            MappedField.of(2, "data")),
+        updated.asMappedFields());
+
+    // rename data to id, which conflicts in the mapping above
+    // this update should succeed, even though the mapping update fails
+    table.updateSchema()
+        .renameColumn("data", "id")
+        .commit();
+
+    Assert.assertEquals("Mapping JSON should not change",
+        updatedJson, 
table.properties().get(TableProperties.DEFAULT_NAME_MAPPING));
+  }
+}
diff --git a/core/src/test/java/org/apache/iceberg/mapping/TestNameMapping.java 
b/core/src/test/java/org/apache/iceberg/mapping/TestNameMapping.java
new file mode 100644
index 0000000..7a49764
--- /dev/null
+++ b/core/src/test/java/org/apache/iceberg/mapping/TestNameMapping.java
@@ -0,0 +1,250 @@
+/*
+ * 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.
+ */
+
+package org.apache.iceberg.mapping;
+
+import org.apache.iceberg.AssertHelpers;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.types.Types;
+import org.junit.Assert;
+import org.junit.Test;
+
+import static org.apache.iceberg.types.Types.NestedField.required;
+
+public class TestNameMapping {
+  @Test
+  public void testFlatSchemaToMapping() {
+    Schema schema = new Schema(
+        required(1, "id", Types.LongType.get()),
+        required(2, "data", Types.StringType.get()));
+
+    MappedFields expected = MappedFields.of(
+        MappedField.of(1, "id"),
+        MappedField.of(2, "data"));
+
+    NameMapping mapping = MappingUtil.create(schema);
+    Assert.assertEquals(expected, mapping.asMappedFields());
+  }
+
+  @Test
+  public void testNestedStructSchemaToMapping() {
+    Schema schema = new Schema(
+        required(1, "id", Types.LongType.get()),
+        required(2, "data", Types.StringType.get()),
+        required(3, "location", Types.StructType.of(
+            required(4, "latitude", Types.FloatType.get()),
+            required(5, "longitude", Types.FloatType.get())
+        )));
+
+    MappedFields expected = MappedFields.of(
+        MappedField.of(1, "id"),
+        MappedField.of(2, "data"),
+        MappedField.of(3, "location", MappedFields.of(
+            MappedField.of(4, "latitude"),
+            MappedField.of(5, "longitude")
+        )));
+
+    NameMapping mapping = MappingUtil.create(schema);
+    Assert.assertEquals(expected, mapping.asMappedFields());
+  }
+
+  @Test
+  public void testMapSchemaToMapping() {
+    Schema schema = new Schema(
+        required(1, "id", Types.LongType.get()),
+        required(2, "data", Types.StringType.get()),
+        required(3, "map", Types.MapType.ofRequired(4, 5,
+            Types.StringType.get(),
+            Types.DoubleType.get())));
+
+    MappedFields expected = MappedFields.of(
+        MappedField.of(1, "id"),
+        MappedField.of(2, "data"),
+        MappedField.of(3, "map", MappedFields.of(
+            MappedField.of(4, "key"),
+            MappedField.of(5, "value")
+        )));
+
+    NameMapping mapping = MappingUtil.create(schema);
+    Assert.assertEquals(expected, mapping.asMappedFields());
+  }
+
+  @Test
+  public void testComplexKeyMapSchemaToMapping() {
+    Schema schema = new Schema(
+        required(1, "id", Types.LongType.get()),
+        required(2, "data", Types.StringType.get()),
+        required(3, "map", Types.MapType.ofRequired(4, 5,
+            Types.StructType.of(
+                required(6, "x", Types.DoubleType.get()),
+                required(7, "y", Types.DoubleType.get())),
+            Types.DoubleType.get())));
+
+    MappedFields expected = MappedFields.of(
+        MappedField.of(1, "id"),
+        MappedField.of(2, "data"),
+        MappedField.of(3, "map", MappedFields.of(
+            MappedField.of(4, "key", MappedFields.of(
+                MappedField.of(6, "x"),
+                MappedField.of(7, "y")
+            )),
+            MappedField.of(5, "value")
+        )));
+
+    NameMapping mapping = MappingUtil.create(schema);
+    Assert.assertEquals(expected, mapping.asMappedFields());
+  }
+
+  @Test
+  public void testComplexValueMapSchemaToMapping() {
+    Schema schema = new Schema(
+        required(1, "id", Types.LongType.get()),
+        required(2, "data", Types.StringType.get()),
+        required(3, "map", Types.MapType.ofRequired(4, 5,
+            Types.DoubleType.get(),
+            Types.StructType.of(
+                required(6, "x", Types.DoubleType.get()),
+                required(7, "y", Types.DoubleType.get()))
+        )));
+
+    MappedFields expected = MappedFields.of(
+        MappedField.of(1, "id"),
+        MappedField.of(2, "data"),
+        MappedField.of(3, "map", MappedFields.of(
+            MappedField.of(4, "key"),
+            MappedField.of(5, "value", MappedFields.of(
+                MappedField.of(6, "x"),
+                MappedField.of(7, "y")
+            ))
+        )));
+
+    NameMapping mapping = MappingUtil.create(schema);
+    Assert.assertEquals(expected, mapping.asMappedFields());
+  }
+
+  @Test
+  public void testListSchemaToMapping() {
+    Schema schema = new Schema(
+        required(1, "id", Types.LongType.get()),
+        required(2, "data", Types.StringType.get()),
+        required(3, "list", Types.ListType.ofRequired(4, 
Types.StringType.get())));
+
+    MappedFields expected = MappedFields.of(
+        MappedField.of(1, "id"),
+        MappedField.of(2, "data"),
+        MappedField.of(3, "list", MappedFields.of(
+            MappedField.of(4, "element")
+        )));
+
+    NameMapping mapping = MappingUtil.create(schema);
+    Assert.assertEquals(expected, mapping.asMappedFields());
+  }
+
+  @Test
+  public void testFailsDuplicateId() {
+    // the schema can be created because ID indexing is lazy
+    Schema schema = new Schema(
+        required(1, "id", Types.LongType.get()),
+        required(1, "data", Types.StringType.get()));
+
+    AssertHelpers.assertThrows("Should fail if IDs are reused",
+        IllegalArgumentException.class, "Multiple entries with same key",
+        () -> MappingUtil.create(schema));
+  }
+
+  @Test
+  public void testFailsDuplicateName() {
+    AssertHelpers.assertThrows("Should fail if names are reused",
+        IllegalArgumentException.class, "Multiple entries with same key",
+        () -> new NameMapping(MappedFields.of(MappedField.of(1, "x"), 
MappedField.of(2, "x"))));
+  }
+
+  @Test
+  public void testAllowsDuplicateNamesInSeparateContexts() {
+    new NameMapping(MappedFields.of(
+        MappedField.of(1, "x", MappedFields.of(MappedField.of(3, "x"))),
+        MappedField.of(2, "y", MappedFields.of(MappedField.of(4, "x")))
+    ));
+  }
+
+  @Test
+  public void testMappingFindById() {
+    Schema schema = new Schema(
+        required(1, "id", Types.LongType.get()),
+        required(2, "data", Types.StringType.get()),
+        required(3, "map", Types.MapType.ofRequired(4, 5,
+            Types.DoubleType.get(),
+            Types.StructType.of(
+                required(6, "x", Types.DoubleType.get()),
+                required(7, "y", Types.DoubleType.get())))),
+        required(8, "list", Types.ListType.ofRequired(9,
+            Types.StringType.get())),
+        required(10, "location", Types.StructType.of(
+            required(11, "latitude", Types.FloatType.get()),
+            required(12, "longitude", Types.FloatType.get())
+        )));
+
+    NameMapping mapping = MappingUtil.create(schema);
+
+    Assert.assertNull("Should not return a field mapping for a missing ID", 
mapping.find(100));
+    Assert.assertEquals(MappedField.of(2, "data"), mapping.find(2));
+    Assert.assertEquals(MappedField.of(6, "x"), mapping.find(6));
+    Assert.assertEquals(MappedField.of(9, "element"), mapping.find(9));
+    Assert.assertEquals(MappedField.of(11, "latitude"), mapping.find(11));
+    Assert.assertEquals(
+        MappedField.of(10, "location", MappedFields.of(
+            MappedField.of(11, "latitude"),
+            MappedField.of(12, "longitude"))),
+        mapping.find(10));
+  }
+
+  @Test
+  public void testMappingFindByName() {
+    Schema schema = new Schema(
+        required(1, "id", Types.LongType.get()),
+        required(2, "data", Types.StringType.get()),
+        required(3, "map", Types.MapType.ofRequired(4, 5,
+            Types.DoubleType.get(),
+            Types.StructType.of(
+                required(6, "x", Types.DoubleType.get()),
+                required(7, "y", Types.DoubleType.get())))),
+        required(8, "list", Types.ListType.ofRequired(9,
+            Types.StringType.get())),
+        required(10, "location", Types.StructType.of(
+            required(11, "latitude", Types.FloatType.get()),
+            required(12, "longitude", Types.FloatType.get())
+        )));
+
+    NameMapping mapping = MappingUtil.create(schema);
+
+    Assert.assertNull("Should not return a field mapping for a nested name", 
mapping.find("element"));
+    Assert.assertNull("Should not return a field mapping for a nested name", 
mapping.find("x"));
+    Assert.assertNull("Should not return a field mapping for a nested name", 
mapping.find("key"));
+    Assert.assertNull("Should not return a field mapping for a nested name", 
mapping.find("value"));
+    Assert.assertEquals(MappedField.of(2, "data"), mapping.find("data"));
+    Assert.assertEquals(MappedField.of(6, "x"), mapping.find("map", "value", 
"x"));
+    Assert.assertEquals(MappedField.of(9, "element"), mapping.find("list", 
"element"));
+    Assert.assertEquals(MappedField.of(11, "latitude"), 
mapping.find("location", "latitude"));
+    Assert.assertEquals(
+        MappedField.of(10, "location", MappedFields.of(
+            MappedField.of(11, "latitude"),
+            MappedField.of(12, "longitude"))),
+        mapping.find("location"));
+  }
+}

Reply via email to