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

roryqi 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 c3da460478 [#9666]fix(iceberg): Fix Iceberg migrate procedure by 
preserving stageCreate flag (#9667)
c3da460478 is described below

commit c3da4604783b836aee664eb97c53b49ed9ae4eac
Author: Bharath Krishna <[email protected]>
AuthorDate: Sat Jan 10 00:52:29 2026 -0800

    [#9666]fix(iceberg): Fix Iceberg migrate procedure by preserving 
stageCreate flag (#9667)
    
    ### What changes were proposed in this pull request?
    
    This PR fixes the Iceberg system.migrate() procedure for JDBC-backed
    catalogs by preserving stageCreate flag in IcebergTableOperationExecutor
    
    ### Why are the changes needed?
    
    The system.migrate() procedure fails when migrating external Iceberg
    tables to JDBC-backed catalogs (MySQL, PostgreSQL) with the error:
    ```
    org.apache.iceberg.exceptions.AlreadyExistsException: Table already exists 
in database
    ```
    
    Root Cause:
    The staging protocol requires 3 distinct phases to safely migrate tables
    with existing data:
    
    Stage: Create table metadata without registering in the catalog
    Write: Client writes/copies data files to the staged location
    Commit: Atomically register the table with assert-create to prevent race
    conditions
    JDBC catalogs skip phase 1 and immediately commit the table to the
    database, causing the migration to fail because the table already exists
    when phase 3 attempts the final commit.
    
    This is a critical bug that makes it impossible to migrate existing
    Iceberg tables to Gravitino JDBC catalogs.
    
    Fix: #9666
    
    ### Does this PR introduce _any_ user-facing change?
    
    No user-facing API changes. This fix enables the system.migrate()
    procedure to work correctly with JDBC catalogs, which was previously
    broken.
    
    ### How was this patch tested?
    - Added unit test
    
    Manual Testing:
    
    Configured Gravitino with JDBC catalog (MySQL backend)
    Created external Iceberg table in Hive catalog
    Successfully migrated table using CALL
    system.migrate('catalog.database.table')
    Verified table metadata and data files are correctly staged and
    committed
    Confirmed migration fails without the fix (table already exists error)
---
 .../dispatcher/IcebergTableOperationExecutor.java  | 12 ++++--
 .../TestIcebergTableOperationExecutor.java         | 44 ++++++++++++++++++++++
 2 files changed, 53 insertions(+), 3 deletions(-)

diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableOperationExecutor.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableOperationExecutor.java
index ad0ee7f077..8319a200aa 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableOperationExecutor.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableOperationExecutor.java
@@ -72,15 +72,21 @@ public class IcebergTableOperationExecutor implements 
IcebergTableOperationDispa
             authenticatedUser);
 
         // CreateTableRequest is immutable, so we need to rebuild it with 
modified properties
-        createTableRequest =
+        CreateTableRequest.Builder builder =
             CreateTableRequest.builder()
                 .withName(createTableRequest.name())
                 .withSchema(createTableRequest.schema())
                 .withPartitionSpec(createTableRequest.spec())
                 .withWriteOrder(createTableRequest.writeOrder())
                 .withLocation(createTableRequest.location())
-                .setProperties(properties)
-                .build();
+                .setProperties(properties);
+
+        // Preserve the stageCreate flag when rebuilding the request
+        if (createTableRequest.stageCreate()) {
+          builder.stageCreate();
+        }
+
+        createTableRequest = builder.build();
       }
     }
 
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergTableOperationExecutor.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergTableOperationExecutor.java
index 47c2ffb4d2..65e271731d 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergTableOperationExecutor.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergTableOperationExecutor.java
@@ -144,4 +144,48 @@ public class TestIcebergTableOperationExecutor {
     String actualOwner = 
requestCaptor.getValue().properties().get(IcebergConstants.OWNER);
     Assertions.assertEquals(clientProvidedOwner, actualOwner);
   }
+
+  @Test
+  public void testCreateTablePreservesStageCreateTrue() {
+    when(mockContext.userName()).thenReturn("[email protected]");
+    LoadTableResponse mockResponse = mock(LoadTableResponse.class);
+    when(mockCatalogWrapper.createTable(any(), any(), 
anyBoolean())).thenReturn(mockResponse);
+
+    CreateTableRequest stagedRequest =
+        CreateTableRequest.builder()
+            .withName("test_table")
+            .withSchema(TABLE_SCHEMA)
+            .stageCreate()
+            .build();
+
+    executor.createTable(mockContext, Namespace.of("test_namespace"), 
stagedRequest);
+
+    ArgumentCaptor<CreateTableRequest> requestCaptor =
+        ArgumentCaptor.forClass(CreateTableRequest.class);
+    verify(mockCatalogWrapper).createTable(any(), requestCaptor.capture(), 
anyBoolean());
+
+    Assertions.assertTrue(
+        requestCaptor.getValue().stageCreate(),
+        "stageCreate=true must be preserved when rebuilding request");
+  }
+
+  @Test
+  public void testCreateTablePreservesStageCreateFalse() {
+    when(mockContext.userName()).thenReturn("[email protected]");
+    LoadTableResponse mockResponse = mock(LoadTableResponse.class);
+    when(mockCatalogWrapper.createTable(any(), any(), 
anyBoolean())).thenReturn(mockResponse);
+
+    CreateTableRequest normalRequest =
+        
CreateTableRequest.builder().withName("test_table").withSchema(TABLE_SCHEMA).build();
+
+    executor.createTable(mockContext, Namespace.of("test_namespace"), 
normalRequest);
+
+    ArgumentCaptor<CreateTableRequest> requestCaptor =
+        ArgumentCaptor.forClass(CreateTableRequest.class);
+    verify(mockCatalogWrapper).createTable(any(), requestCaptor.capture(), 
anyBoolean());
+
+    Assertions.assertFalse(
+        requestCaptor.getValue().stageCreate(),
+        "stageCreate=false must remain false when rebuilding request");
+  }
 }

Reply via email to