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

mchades pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new dc912f9754 [#11329] fix(catalog-glue): Support complex types for 
Iceberg tables in Glue catalog (#11429)
dc912f9754 is described below

commit dc912f97543e64ecfd4e44910dd14b001a057865
Author: Yuhui <[email protected]>
AuthorDate: Mon Jun 8 10:57:03 2026 +0800

    [#11329] fix(catalog-glue): Support complex types for Iceberg tables in 
Glue catalog (#11429)
    
    ### What changes were proposed in this pull request?
    
    Support `ListType`, `MapType`, and `StructType` columns when creating
    Iceberg
    tables via the Glue catalog.
    
    ### Why are the changes needed?
    
    Creating an Iceberg table with complex column types via the Glue catalog
    threw
    `UnsupportedOperationException`, while the standard Iceberg catalog
    worked fine.
    
    Fix: #11329
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. Users can now create Iceberg tables with `ListType`, `MapType`, and
    `StructType` columns via the Glue catalog.
    
    ### How was this patch tested?
    
    - Unit tests in `TestGlueIcebergTableHelper`
    - Integration test
    `AwsGlueCatalogIT#testCreateIcebergTableWithComplexTypes`
      against real AWS Glue + S3
---
 .../catalog/glue/GlueIcebergTableHelper.java       | 55 +++++++++++++++++--
 .../catalog/glue/TestGlueIcebergTableHelper.java   | 61 +++++++++++++++++++++
 .../glue/integration/test/AwsGlueCatalogIT.java    | 64 ++++++++++++++++++++++
 3 files changed, 174 insertions(+), 6 deletions(-)

diff --git 
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueIcebergTableHelper.java
 
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueIcebergTableHelper.java
index f0fe363875..8438e6754a 100644
--- 
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueIcebergTableHelper.java
+++ 
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueIcebergTableHelper.java
@@ -40,6 +40,7 @@ import static 
org.apache.gravitino.rel.types.Types.TimestampType;
 import static org.apache.gravitino.rel.types.Types.UUIDType;
 import static org.apache.gravitino.rel.types.Types.VarCharType;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -442,9 +443,13 @@ final class GlueIcebergTableHelper {
           TableChange.UpdateColumnType upd = (TableChange.UpdateColumnType) 
change;
           Preconditions.checkArgument(
               upd.fieldName().length == 1, "Nested column type updates are not 
supported");
+          org.apache.iceberg.types.Type newIcebergType = 
toIcebergType(upd.getNewDataType());
+          Preconditions.checkArgument(
+              newIcebergType instanceof 
org.apache.iceberg.types.Type.PrimitiveType,
+              "Iceberg only supports primitive type promotion via 
updateColumn, got: %s",
+              upd.getNewDataType().simpleString());
           update.updateColumn(
-              upd.fieldName()[0],
-              (org.apache.iceberg.types.Type.PrimitiveType) 
toIcebergType(upd.getNewDataType()));
+              upd.fieldName()[0], 
(org.apache.iceberg.types.Type.PrimitiveType) newIcebergType);
         } else if (change instanceof TableChange.UpdateColumnComment) {
           TableChange.UpdateColumnComment upd = 
(TableChange.UpdateColumnComment) change;
           Preconditions.checkArgument(
@@ -578,13 +583,15 @@ final class GlueIcebergTableHelper {
   // Type conversion (Gravitino -> Iceberg)
   // 
---------------------------------------------------------------------------
 
-  private static Schema toIcebergSchema(Column[] columns) {
+  @VisibleForTesting
+  static Schema toIcebergSchema(Column[] columns) {
     List<Types.NestedField> fields = new ArrayList<>();
-    // Field IDs are assigned sequentially starting from 0. This is the 
Iceberg convention
-    // for initial schema creation. Schema evolution reuses existing IDs from 
the table.
+    // Top-level columns use ordinal IDs (0, 1, ...). Nested field IDs start 
after that range
+    // so all IDs are unique within the schema, as required by the Schema 
constructor.
+    int[] nextId = {columns.length};
     for (int i = 0; i < columns.length; i++) {
       Column col = columns[i];
-      org.apache.iceberg.types.Type icebergType = 
toIcebergType(col.dataType());
+      org.apache.iceberg.types.Type icebergType = 
toIcebergType(col.dataType(), nextId);
       if (col.nullable()) {
         fields.add(Types.NestedField.optional(i, col.name(), icebergType, 
col.comment()));
       } else {
@@ -595,6 +602,42 @@ final class GlueIcebergTableHelper {
   }
 
   private static org.apache.iceberg.types.Type toIcebergType(Type type) {
+    int[] nextId = {0};
+    return toIcebergType(type, nextId);
+  }
+
+  private static org.apache.iceberg.types.Type toIcebergType(Type type, int[] 
nextId) {
+    if (type instanceof ListType) {
+      ListType listType = (ListType) type;
+      org.apache.iceberg.types.Type elementType = 
toIcebergType(listType.elementType(), nextId);
+      int elementId = nextId[0]++;
+      return listType.elementNullable()
+          ? Types.ListType.ofOptional(elementId, elementType)
+          : Types.ListType.ofRequired(elementId, elementType);
+    }
+    if (type instanceof MapType) {
+      MapType mapType = (MapType) type;
+      org.apache.iceberg.types.Type keyType = toIcebergType(mapType.keyType(), 
nextId);
+      org.apache.iceberg.types.Type valueType = 
toIcebergType(mapType.valueType(), nextId);
+      int keyId = nextId[0]++;
+      int valueId = nextId[0]++;
+      return mapType.valueNullable()
+          ? Types.MapType.ofOptional(keyId, valueId, keyType, valueType)
+          : Types.MapType.ofRequired(keyId, valueId, keyType, valueType);
+    }
+    if (type instanceof StructType) {
+      StructType structType = (StructType) type;
+      List<Types.NestedField> nestedFields = new ArrayList<>();
+      for (StructType.Field field : structType.fields()) {
+        org.apache.iceberg.types.Type fieldType = toIcebergType(field.type(), 
nextId);
+        int fieldId = nextId[0]++;
+        nestedFields.add(
+            field.nullable()
+                ? Types.NestedField.optional(fieldId, field.name(), fieldType, 
field.comment())
+                : Types.NestedField.required(fieldId, field.name(), fieldType, 
field.comment()));
+      }
+      return Types.StructType.of(nestedFields);
+    }
     if (type instanceof BooleanType) {
       return Types.BooleanType.get();
     }
diff --git 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueIcebergTableHelper.java
 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueIcebergTableHelper.java
index 5b99b63e2f..e3f3f3bb8b 100644
--- 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueIcebergTableHelper.java
+++ 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueIcebergTableHelper.java
@@ -20,6 +20,9 @@ package org.apache.gravitino.catalog.glue;
 
 import static 
org.apache.gravitino.catalog.glue.GlueIcebergTableHelper.fromIcebergType;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -130,6 +133,64 @@ class TestGlueIcebergTableHelper {
     assertEquals(false, structType.fields()[1].nullable());
   }
 
+  // -------------------------------------------------------------------------
+  // toIcebergSchema — complex types
+  // -------------------------------------------------------------------------
+
+  @Test
+  void testToIcebergSchemaWithListType() {
+    Column col =
+        GlueColumn.builder()
+            .withName("col_list")
+            .withType(Types.ListType.nullable(Types.StringType.get()))
+            .build();
+    Schema schema = GlueIcebergTableHelper.toIcebergSchema(new Column[] {col});
+
+    NestedField field = schema.findField("col_list");
+    assertInstanceOf(ListType.class, field.type());
+    ListType listType = (ListType) field.type();
+    assertEquals(StringType.get(), listType.elementType());
+    assertTrue(listType.isElementOptional());
+  }
+
+  @Test
+  void testToIcebergSchemaWithMapType() {
+    Column col =
+        GlueColumn.builder()
+            .withName("col_map")
+            .withType(Types.MapType.of(Types.StringType.get(), 
Types.LongType.get(), false))
+            .build();
+    Schema schema = GlueIcebergTableHelper.toIcebergSchema(new Column[] {col});
+
+    NestedField field = schema.findField("col_map");
+    assertInstanceOf(MapType.class, field.type());
+    MapType mapType = (MapType) field.type();
+    assertEquals(StringType.get(), mapType.keyType());
+    assertEquals(LongType.get(), mapType.valueType());
+    assertFalse(mapType.isValueOptional());
+  }
+
+  @Test
+  void testToIcebergSchemaWithStructType() {
+    Types.StructType gravitinoStruct =
+        Types.StructType.of(
+            Types.StructType.Field.of("name", Types.StringType.get(), true, 
"the name"),
+            Types.StructType.Field.of("age", Types.IntegerType.get(), false, 
null));
+    Column col = 
GlueColumn.builder().withName("col_struct").withType(gravitinoStruct).build();
+    Schema schema = GlueIcebergTableHelper.toIcebergSchema(new Column[] {col});
+
+    NestedField field = schema.findField("col_struct");
+    assertInstanceOf(StructType.class, field.type());
+    StructType structType = (StructType) field.type();
+    assertEquals(2, structType.fields().size());
+    assertEquals("name", structType.fields().get(0).name());
+    assertEquals(StringType.get(), structType.fields().get(0).type());
+    assertTrue(structType.fields().get(0).isOptional());
+    assertEquals("age", structType.fields().get(1).name());
+    assertEquals(IntegerType.get(), structType.fields().get(1).type());
+    assertFalse(structType.fields().get(1).isOptional());
+  }
+
   // -------------------------------------------------------------------------
   // loadTable — column type overwrite
   // -------------------------------------------------------------------------
diff --git 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/integration/test/AwsGlueCatalogIT.java
 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/integration/test/AwsGlueCatalogIT.java
index 8ac09cf389..eb1437e3a4 100644
--- 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/integration/test/AwsGlueCatalogIT.java
+++ 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/integration/test/AwsGlueCatalogIT.java
@@ -19,6 +19,8 @@
 package org.apache.gravitino.catalog.glue.integration.test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import com.google.common.base.Preconditions;
 import java.util.Collections;
@@ -89,6 +91,68 @@ class AwsGlueCatalogIT extends AbstractGlueCatalogIT {
     return config;
   }
 
+  @Test
+  void testCreateIcebergTableWithComplexTypes() {
+    String bucket = System.getenv("AWS_S3_TEST_BUCKET");
+    String schema = "glue_it_" + System.nanoTime();
+    ops.createSchema(NameIdentifier.of("ml", "cat", schema), null, 
Collections.emptyMap());
+
+    Map<String, String> props = new HashMap<>();
+    props.put(GlueConstants.TABLE_FORMAT, "ICEBERG");
+    props.put(GlueConstants.LOCATION, "s3://" + bucket + "/" + schema + "/");
+
+    Column[] cols = {
+      Column.of("tags", Types.ListType.nullable(Types.StringType.get()), "list 
col"),
+      Column.of(
+          "scores",
+          Types.MapType.of(Types.StringType.get(), Types.DoubleType.get(), 
false),
+          "map col"),
+      Column.of(
+          "info",
+          Types.StructType.of(
+              Types.StructType.Field.of("name", Types.StringType.get(), true, 
null),
+              Types.StructType.Field.of("age", Types.IntegerType.get(), false, 
null)),
+          "struct col"),
+    };
+
+    ops.createTable(
+        NameIdentifier.of("ml", "cat", schema, "ice_complex"),
+        cols,
+        null,
+        props,
+        new Transform[0],
+        Distributions.NONE,
+        new SortOrder[0],
+        new Index[0]);
+
+    try {
+      Table loaded = ops.loadTable(NameIdentifier.of("ml", "cat", schema, 
"ice_complex"));
+      assertEquals(3, loaded.columns().length);
+
+      assertEquals("list col", loaded.columns()[0].comment());
+      Types.ListType listType = (Types.ListType) 
loaded.columns()[0].dataType();
+      assertEquals(Types.StringType.get(), listType.elementType());
+      assertTrue(listType.elementNullable());
+
+      assertEquals("map col", loaded.columns()[1].comment());
+      Types.MapType mapType = (Types.MapType) loaded.columns()[1].dataType();
+      assertEquals(Types.StringType.get(), mapType.keyType());
+      assertEquals(Types.DoubleType.get(), mapType.valueType());
+      assertFalse(mapType.valueNullable());
+
+      assertEquals("struct col", loaded.columns()[2].comment());
+      Types.StructType structType = (Types.StructType) 
loaded.columns()[2].dataType();
+      assertEquals(2, structType.fields().length);
+      assertEquals("name", structType.fields()[0].name());
+      assertEquals(Types.StringType.get(), structType.fields()[0].type());
+      assertEquals("age", structType.fields()[1].name());
+      assertEquals(Types.IntegerType.get(), structType.fields()[1].type());
+    } finally {
+      ops.dropTable(NameIdentifier.of("ml", "cat", schema, "ice_complex"));
+      ops.dropSchema(NameIdentifier.of("ml", "cat", schema), false);
+    }
+  }
+
   @Test
   void testAlterIcebergMetadata() {
     String bucket = System.getenv("AWS_S3_TEST_BUCKET");

Reply via email to