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

yuqi1129 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 436a6b592a [#14606] fix(core): normalize table alter changes exactly 
once (#14607)
436a6b592a is described below

commit 436a6b592a4e62e7108ea46ac05ad0609f45c666
Author: Qi Yu <[email protected]>
AuthorDate: Wed Nov 4 22:53:09 2026 +0800

    [#14606] fix(core): normalize table alter changes exactly once (#14607)
    
    ### What changes were proposed in this pull request?
    
    Remove duplicate alter-change normalization from the operation
    dispatcher. Keep normalization in TableNormalizeDispatcher and use the
    same normalized changes for provider dispatch and stored column name
    changes.
    
    ### Why are the changes needed?
    
    Quote-aware normalization is not idempotent: the second pass folds
    already unquoted names or rejects valid names containing spaces.
    
    Fixes #14606.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Quoted alter names retain their intended spelling through provider
    dispatch.
    
    ### How was this patch tested?
    
    TestTableNormalizeDispatcher and TestTableOperationDispatcher pass (61
    tests). Reverting the production change reproduces the new regression
    failure.
    
    Co-authored-by: Jerry Shao <[email protected]>
---
 .../catalog/TableOperationDispatcher.java          | 10 ++--
 .../catalog/TestTableNormalizeDispatcher.java      | 53 ++++++++++++++++++++++
 2 files changed, 57 insertions(+), 6 deletions(-)

diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java 
b/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
index 4439b31145..b63df43356 100644
--- 
a/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
@@ -20,7 +20,6 @@ package org.apache.gravitino.catalog;
 
 import static org.apache.gravitino.Entity.EntityType.COLUMN;
 import static org.apache.gravitino.Entity.EntityType.TABLE;
-import static org.apache.gravitino.catalog.CapabilityHelpers.applyCapabilities;
 import static 
org.apache.gravitino.catalog.PropertiesMetadataHelpers.validatePropertyForCreate;
 import static 
org.apache.gravitino.rel.expressions.transforms.Transforms.EMPTY_TRANSFORM;
 import static 
org.apache.gravitino.utils.NameIdentifierUtil.getCatalogIdentifier;
@@ -339,15 +338,14 @@ public class TableOperationDispatcher extends 
OperationDispatcher implements Tab
                         isRenameTable && !managed
                             ? getTableEntityBeforeRename(ident)
                             : Optional.empty();
-                    TableChange[] normalizedChanges =
-                        applyCapabilities(catalog.capabilities(), changes);
+                    // TableNormalizeDispatcher already normalized the 
changes. Normalization can
+                    // depend on quoting and must not be repeated on canonical 
physical names.
                     Table table =
-                        catalog.doWithTableOps(
-                            tableOps -> tableOps.alterTable(ident, 
normalizedChanges));
+                        catalog.doWithTableOps(tableOps -> 
tableOps.alterTable(ident, changes));
                     return new AlterTableCatalogResult(
                         snapshotTable(catalog, table, managed),
                         tableEntityBeforeRename,
-                        resolveColumnNameChanges(normalizedChanges));
+                        resolveColumnNameChanges(changes));
                   },
                   NoSuchTableException.class,
                   IllegalArgumentException.class);
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestTableNormalizeDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestTableNormalizeDispatcher.java
index 879c448d1c..6bd257e2cb 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestTableNormalizeDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestTableNormalizeDispatcher.java
@@ -37,6 +37,7 @@ import org.apache.gravitino.exceptions.NoSuchCatalogException;
 import org.apache.gravitino.exceptions.TableAlreadyExistsException;
 import org.apache.gravitino.rel.Column;
 import org.apache.gravitino.rel.Table;
+import org.apache.gravitino.rel.TableCatalog;
 import org.apache.gravitino.rel.TableChange;
 import org.apache.gravitino.rel.expressions.NamedReference;
 import org.apache.gravitino.rel.expressions.distributions.Distribution;
@@ -52,6 +53,7 @@ import org.apache.gravitino.rel.indexes.Indexes;
 import org.apache.gravitino.rel.partitions.Partitions;
 import org.apache.gravitino.rel.partitions.RangePartition;
 import org.apache.gravitino.rel.types.Types;
+import org.apache.gravitino.utils.ThrowableFunction;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
@@ -73,6 +75,57 @@ public class TestTableNormalizeDispatcher extends 
TestOperationDispatcher {
             TestTableOperationDispatcher.schemaOperationDispatcher, 
catalogManager);
   }
 
+  @Test
+  void testAlterChangesAreNormalizedOnlyOnceBeforeReachingCatalog() throws 
Exception {
+    CatalogManager manager = Mockito.mock(CatalogManager.class);
+    BaseCatalog<?> catalog = Mockito.mock(BaseCatalog.class);
+    
Mockito.when(catalog.capability()).thenReturn(TestCapabilityHelpers.QUOTE_AWARE_CAPABILITY);
+    CatalogTestUtils.mockDoWithCatalog(manager, catalog);
+    CatalogManager.CatalogWrapper wrapper = 
Mockito.mock(CatalogManager.CatalogWrapper.class);
+    
Mockito.when(wrapper.capabilities()).thenReturn(TestCapabilityHelpers.QUOTE_AWARE_CAPABILITY);
+    Mockito.when(wrapper.doWithCatalog(Mockito.any())).thenReturn(true);
+    Mockito.doAnswer(
+            invocation -> {
+              ThrowableFunction<CatalogManager.CatalogWrapper, Object> 
operation =
+                  invocation.getArgument(1);
+              return operation.apply(wrapper);
+            })
+        .when(manager)
+        .doWithCatalogWrapper(Mockito.any(), Mockito.any());
+    TableCatalog tables = Mockito.mock(TableCatalog.class);
+    IllegalArgumentException stop =
+        new IllegalArgumentException("Stop after observing provider changes");
+    Mockito.doAnswer(
+            invocation -> {
+              ThrowableFunction<TableCatalog, Object> operation = 
invocation.getArgument(0);
+              return operation.apply(tables);
+            })
+        .when(wrapper)
+        .doWithTableOps(Mockito.any());
+    Mockito.when(tables.alterTable(Mockito.any(), 
Mockito.<TableChange[]>any())).thenThrow(stop);
+    TableNormalizeDispatcher dispatcher =
+        new TableNormalizeDispatcher(
+            new TableOperationDispatcher(manager, entityStore, idGenerator, 
secretManager),
+            manager);
+    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "schema", 
"table");
+    TableChange[] requests = {
+      TableChange.renameColumn(new String[] {"\"MixedOld\""}, "\"MixedNew\""),
+      TableChange.addColumn(new String[] {"\"Added Col\""}, 
Types.IntegerType.get()),
+      TableChange.rename("\"RenamedTable\"")
+    };
+    Assertions.assertSame(
+        stop,
+        Assertions.assertThrows(
+            IllegalArgumentException.class, () -> dispatcher.alterTable(ident, 
requests)));
+    ArgumentCaptor<TableChange[]> captor = 
ArgumentCaptor.forClass(TableChange[].class);
+    Mockito.verify(tables).alterTable(Mockito.any(), captor.capture());
+    TableChange[] actual = captor.getValue();
+    Assertions.assertEquals("MixedOld", ((TableChange.RenameColumn) 
actual[0]).fieldName()[0]);
+    Assertions.assertEquals("MixedNew", ((TableChange.RenameColumn) 
actual[0]).getNewName());
+    Assertions.assertEquals("Added Col", ((TableChange.AddColumn) 
actual[1]).fieldName()[0]);
+    Assertions.assertEquals("RenamedTable", ((TableChange.RenameTable) 
actual[2]).getNewName());
+  }
+
   @Test
   void testDeletionCanUseProviderValidatedNativeNamesWithoutRelaxingCreation() 
throws Exception {
     TableDispatcher delegate = Mockito.mock(TableDispatcher.class);

Reply via email to