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 713dad1a3f [#12827] fix(doris): handle missing index deletes (#12828)
713dad1a3f is described below

commit 713dad1a3f06ae71a99c484e95584989fa86a539
Author: StormSpirit <[email protected]>
AuthorDate: Fri Sep 18 23:19:19 2026 +0800

    [#12827] fix(doris): handle missing index deletes (#12828)
    
    ### What changes were proposed in this pull request?
    
    Update the Doris JDBC catalog so `TableChange.deleteIndex(name, true)`
    treats a missing index as a no-op without emitting a DROP fragment,
    while preserving strict missing-index validation and existing-index
    deletion.
    
    Validate index changes before metadata loading or SQL generation so
    duplicate DeleteIndex changes and same-name AddIndex/DeleteIndex changes
    fail fast in either request order with deterministic errors.
    
    Filter empty fragments from the combined Doris `ALTER TABLE` statement
    and add unit, operation, and Doris 3.x/4.x integration coverage for the
    missing/existing index matrix, mixed no-op requests, conflict requests,
    and missing-table error propagation.
    
    ### Why are the changes needed?
    
    The Doris implementation currently skips its local existence check when
    `ifExists=true` but still generates `DROP INDEX` inside the combined
    `ALTER TABLE` statement. Doris then rejects a missing index, which
    violates the public `TableChange.DeleteIndex` contract and prevents
    idempotent cleanup operations.
    
    The change keeps the connector's existing batched ALTER model and does
    not assume undocumented `ALTER TABLE ... DROP INDEX IF EXISTS` syntax.
    It also prevents a no-op delete from masking same-name conflicting index
    changes or unrelated validation failures.
    
    Fix: #12827
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. Deleting a missing Doris index with `ifExists=true` now succeeds as
    a no-op. Existing-index deletion and `ifExists=false` strict behavior
    remain unchanged. Duplicate deletes and same-name AddIndex/DeleteIndex
    requests are rejected before DDL execution. No public API, OpenAPI
    field, or property key is added or removed.
    
    ### How was this patch tested?
    
    - `./gradlew :catalogs:catalog-jdbc-doris:test -PskipITs` — passed; 38
    tests, 0 skipped, 0 failures, 0 errors.
    - `./gradlew :catalogs:catalog-jdbc-doris:test --tests
    "org.apache.gravitino.catalog.doris.operation.TestDorisTableOperations"
    -PskipDockerTests=false` — passed; 9 tests, 0 skipped, 0 failures, 0
    errors.
    - `env NEED_CREATE_DOCKER_NETWORK=false ./gradlew
    :catalogs:catalog-jdbc-doris:test --tests
    "org.apache.gravitino.catalog.doris.integration.test.CatalogDoris3xIT"
    -PskipDockerTests=false -PdorisMultiVersionTest` — passed on Doris
    3.0.6.2; 15 tests, 0 skipped, 0 failures, 0 errors.
    - `env NEED_CREATE_DOCKER_NETWORK=false ./gradlew
    :catalogs:catalog-jdbc-doris:test --tests
    "org.apache.gravitino.catalog.doris.integration.test.CatalogDoris4xIT"
    -PskipDockerTests=false -PdorisMultiVersionTest` — passed on Doris
    4.0.6; 15 tests, 0 skipped, 0 failures, 0 errors.
    - `./gradlew :catalogs:catalog-jdbc-doris:spotlessCheck` — passed.
    - `./gradlew rat` — passed.
    - `./gradlew :catalogs:catalog-jdbc-doris:build -x test` — passed.
    - The local-only Gravitino precheck passed.
    - The first Doris 3.x attempt was blocked during test-network
    initialization by an unrelated active endpoint; the recovery run used
    the isolated network environment shown above and passed.
    
    ---------
    
    Signed-off-by: jiangxt2 <[email protected]>
---
 .../doris/operation/DorisTableOperations.java      |  48 ++++++-
 .../doris/integration/test/CatalogDoris3xIT.java   | 133 +++++++++++++++++++
 .../doris/integration/test/CatalogDoris4xIT.java   | 132 +++++++++++++++++++
 .../doris/operation/TestDorisTableOperations.java  |  12 +-
 .../TestDorisTableOperationsSqlGeneration.java     | 141 ++++++++++++++++++++-
 5 files changed, 451 insertions(+), 15 deletions(-)

diff --git 
a/catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/operation/DorisTableOperations.java
 
b/catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/operation/DorisTableOperations.java
index af8e19749e..40b31f3588 100644
--- 
a/catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/operation/DorisTableOperations.java
+++ 
b/catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/operation/DorisTableOperations.java
@@ -39,6 +39,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -746,6 +747,7 @@ public class DorisTableOperations extends 
JdbcTableOperations {
      * */
 
     // Not all operations require the original table information, so lazy 
loading is used here
+    validateIndexChangeConflicts(changes);
     JdbcTable lazyLoadTable = null;
     TableChange.UpdateComment updateComment = null;
     List<TableChange.SetProperty> setProperties = new ArrayList<>();
@@ -819,15 +821,44 @@ public class DorisTableOperations extends 
JdbcTableOperations {
       alterSql.add("MODIFY COMMENT \"" + escapeSqlLiteral(newComment, '"') + 
"\"");
     }
 
-    if (CollectionUtils.isEmpty(alterSql)) {
+    List<String> nonEmptyAlterSql =
+        
alterSql.stream().filter(StringUtils::isNotEmpty).collect(Collectors.toList());
+    if (CollectionUtils.isEmpty(nonEmptyAlterSql)) {
       return "";
     }
     // Return the generated SQL statement
-    String result = "ALTER TABLE `" + tableName + "`\n" + String.join(",\n", 
alterSql) + ";";
+    String result =
+        "ALTER TABLE `" + tableName + "`\n" + String.join(",\n", 
nonEmptyAlterSql) + ";";
     LOG.info("Generated alter table:{}.{} sql: {}", databaseName, tableName, 
result);
     return result;
   }
 
+  private static void validateIndexChangeConflicts(TableChange... changes) {
+    Set<String> deleteIndexNames = new HashSet<>();
+    Set<String> addIndexNames = new HashSet<>();
+
+    for (TableChange change : changes) {
+      if (change instanceof TableChange.DeleteIndex) {
+        String indexName = ((TableChange.DeleteIndex) change).getName();
+        Preconditions.checkArgument(
+            deleteIndexNames.add(indexName),
+            "Index '%s' cannot be deleted more than once in the same request",
+            indexName);
+        Preconditions.checkArgument(
+            !addIndexNames.contains(indexName),
+            "Index '%s' cannot be added and deleted in the same request",
+            indexName);
+      } else if (change instanceof TableChange.AddIndex) {
+        String indexName = ((TableChange.AddIndex) change).getName();
+        Preconditions.checkArgument(
+            !deleteIndexNames.contains(indexName),
+            "Index '%s' cannot be added and deleted in the same request",
+            indexName);
+        addIndexNames.add(indexName);
+      }
+    }
+  }
+
   private String updateColumnNullabilityDefinition(
       TableChange.UpdateColumnNullability change, JdbcTable table) {
     validateUpdateColumnNullable(change, table);
@@ -1035,11 +1066,14 @@ public class DorisTableOperations extends 
JdbcTableOperations {
 
   static String deleteIndexDefinition(
       JdbcTable lazyLoadTable, TableChange.DeleteIndex deleteIndex) {
-    if (!deleteIndex.isIfExists()) {
-      Preconditions.checkArgument(
-          Arrays.stream(lazyLoadTable.index())
-              .anyMatch(index -> index.name().equals(deleteIndex.getName())),
-          "Index does not exist");
+    boolean indexExists =
+        Arrays.stream(lazyLoadTable.index())
+            .anyMatch(index -> index.name().equals(deleteIndex.getName()));
+    if (!indexExists) {
+      if (deleteIndex.isIfExists()) {
+        return "";
+      }
+      throw new IllegalArgumentException("Index does not exist: " + 
deleteIndex.getName());
     }
     return "DROP INDEX `" + deleteIndex.getName() + "`";
   }
diff --git 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDoris3xIT.java
 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDoris3xIT.java
index 97da5a1c03..8c5a6ba0ba 100644
--- 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDoris3xIT.java
+++ 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDoris3xIT.java
@@ -26,6 +26,7 @@ import static 
org.apache.gravitino.integration.test.util.ITUtils.assertPartition
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import com.google.common.collect.Maps;
@@ -45,6 +46,7 @@ import org.apache.gravitino.Catalog;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.catalog.jdbc.config.JdbcConfig;
 import org.apache.gravitino.client.GravitinoMetalake;
+import org.apache.gravitino.exceptions.NoSuchTableException;
 import org.apache.gravitino.integration.test.container.ContainerSuite;
 import org.apache.gravitino.integration.test.container.DorisContainer;
 import org.apache.gravitino.integration.test.container.DorisImageName;
@@ -305,6 +307,137 @@ public class CatalogDoris3xIT extends BaseIT {
         .untilAsserted(() -> assertEquals(0, 
tc.loadTable(tid).index().length));
   }
 
+  @Test
+  void testDeleteMissingIndexIfExists() {
+    TableCatalog tc = catalog.asTableCatalog();
+    NameIdentifier tid =
+        NameIdentifier.of(schemaName, 
GravitinoITUtils.genRandomName("t_missing_idx"));
+
+    tc.createTable(
+        tid,
+        basicColumns(),
+        tableComment,
+        Collections.emptyMap(),
+        Transforms.EMPTY_TRANSFORM,
+        hashDist(),
+        null,
+        null);
+
+    tc.alterTable(tid, TableChange.deleteIndex("idx_missing", true));
+    assertEquals(0, tc.loadTable(tid).index().length);
+
+    IllegalArgumentException exception =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> tc.alterTable(tid, TableChange.deleteIndex("idx_missing", 
false)));
+    assertTrue(exception.getMessage().contains("Index does not exist"), 
exception.getMessage());
+  }
+
+  @Test
+  void testDeleteExistingInvertedIndexWithIfExistsFalse() {
+    TableCatalog tc = catalog.asTableCatalog();
+    NameIdentifier tid =
+        NameIdentifier.of(schemaName, 
GravitinoITUtils.genRandomName("t_existing_idx_strict"));
+
+    tc.createTable(
+        tid,
+        basicColumns(),
+        tableComment,
+        Collections.emptyMap(),
+        Transforms.EMPTY_TRANSFORM,
+        hashDist(),
+        null,
+        null);
+    tc.alterTable(
+        tid,
+        TableChange.addIndex(Index.IndexType.INVERTED, "idx_strict", new 
String[][] {{colName2}}));
+
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(() -> assertEquals(1, 
tc.loadTable(tid).index().length));
+
+    tc.alterTable(tid, TableChange.deleteIndex("idx_strict", false));
+
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(() -> assertEquals(0, 
tc.loadTable(tid).index().length));
+  }
+
+  @Test
+  void testNoOpDeleteComposesWithUnrelatedChanges() {
+    TableCatalog tc = catalog.asTableCatalog();
+    NameIdentifier addColumnFirst =
+        NameIdentifier.of(schemaName, 
GravitinoITUtils.genRandomName("t_noop_column_first"));
+    NameIdentifier addColumnLast =
+        NameIdentifier.of(schemaName, 
GravitinoITUtils.genRandomName("t_noop_column_last"));
+    NameIdentifier addIndexFirst =
+        NameIdentifier.of(schemaName, 
GravitinoITUtils.genRandomName("t_noop_index_first"));
+    NameIdentifier addIndexLast =
+        NameIdentifier.of(schemaName, 
GravitinoITUtils.genRandomName("t_noop_index_last"));
+
+    for (NameIdentifier tid :
+        new NameIdentifier[] {addColumnFirst, addColumnLast, addIndexFirst, 
addIndexLast}) {
+      tc.createTable(
+          tid,
+          basicColumns(),
+          tableComment,
+          Collections.emptyMap(),
+          Transforms.EMPTY_TRANSFORM,
+          hashDist(),
+          null,
+          null);
+    }
+
+    tc.alterTable(
+        addColumnFirst,
+        TableChange.deleteIndex("idx_missing", true),
+        TableChange.addColumn(new String[] {"col_extra"}, 
Types.VarCharType.of(100)));
+    tc.alterTable(
+        addColumnLast,
+        TableChange.addColumn(new String[] {"col_extra"}, 
Types.VarCharType.of(100)),
+        TableChange.deleteIndex("idx_missing", true));
+
+    for (NameIdentifier tid : new NameIdentifier[] {addColumnFirst, 
addColumnLast}) {
+      Awaitility.await()
+          .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+          .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+          .untilAsserted(() -> assertEquals(3, 
tc.loadTable(tid).columns().length));
+    }
+
+    tc.alterTable(
+        addIndexFirst,
+        TableChange.deleteIndex("idx_missing", true),
+        TableChange.addIndex(
+            Index.IndexType.INVERTED, "idx_unrelated", new String[][] 
{{colName2}}));
+    tc.alterTable(
+        addIndexLast,
+        TableChange.addIndex(
+            Index.IndexType.INVERTED, "idx_unrelated", new String[][] 
{{colName2}}),
+        TableChange.deleteIndex("idx_missing", true));
+
+    for (NameIdentifier tid : new NameIdentifier[] {addIndexFirst, 
addIndexLast}) {
+      Awaitility.await()
+          .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+          .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+          .untilAsserted(() -> assertEquals(1, 
tc.loadTable(tid).index().length));
+    }
+  }
+
+  @Test
+  void testDeleteIndexDoesNotSuppressMissingTable() {
+    TableCatalog tc = catalog.asTableCatalog();
+    NameIdentifier tid =
+        NameIdentifier.of(schemaName, 
GravitinoITUtils.genRandomName("t_missing_table"));
+
+    NoSuchTableException exception =
+        assertThrows(
+            NoSuchTableException.class,
+            () -> tc.alterTable(tid, TableChange.deleteIndex("idx_missing", 
true)));
+    assertTrue(exception.getMessage().contains(tid.name()), 
exception.getMessage());
+  }
+
   @Test
   void testCreateTableWithAutoIncrement() {
     TableCatalog tc = catalog.asTableCatalog();
diff --git 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDoris4xIT.java
 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDoris4xIT.java
index 6c5863dc98..e838a8274d 100644
--- 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDoris4xIT.java
+++ 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDoris4xIT.java
@@ -46,6 +46,7 @@ import org.apache.gravitino.Catalog;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.catalog.jdbc.config.JdbcConfig;
 import org.apache.gravitino.client.GravitinoMetalake;
+import org.apache.gravitino.exceptions.NoSuchTableException;
 import org.apache.gravitino.integration.test.container.ContainerSuite;
 import org.apache.gravitino.integration.test.container.DorisContainer;
 import org.apache.gravitino.integration.test.container.DorisImageName;
@@ -324,6 +325,137 @@ public class CatalogDoris4xIT extends BaseIT {
         .untilAsserted(() -> assertEquals(0, 
tc.loadTable(tid).index().length));
   }
 
+  @Test
+  void testDeleteMissingIndexIfExists() {
+    TableCatalog tc = catalog.asTableCatalog();
+    NameIdentifier tid =
+        NameIdentifier.of(schemaName, 
GravitinoITUtils.genRandomName("t_missing_idx"));
+
+    tc.createTable(
+        tid,
+        basicColumns(),
+        tableComment,
+        Collections.emptyMap(),
+        Transforms.EMPTY_TRANSFORM,
+        hashDist(),
+        null,
+        null);
+
+    tc.alterTable(tid, TableChange.deleteIndex("idx_missing", true));
+    assertEquals(0, tc.loadTable(tid).index().length);
+
+    IllegalArgumentException exception =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> tc.alterTable(tid, TableChange.deleteIndex("idx_missing", 
false)));
+    assertTrue(exception.getMessage().contains("Index does not exist"), 
exception.getMessage());
+  }
+
+  @Test
+  void testDeleteExistingInvertedIndexWithIfExistsFalse() {
+    TableCatalog tc = catalog.asTableCatalog();
+    NameIdentifier tid =
+        NameIdentifier.of(schemaName, 
GravitinoITUtils.genRandomName("t_existing_idx_strict"));
+
+    tc.createTable(
+        tid,
+        basicColumns(),
+        tableComment,
+        Collections.emptyMap(),
+        Transforms.EMPTY_TRANSFORM,
+        hashDist(),
+        null,
+        null);
+    tc.alterTable(
+        tid,
+        TableChange.addIndex(Index.IndexType.INVERTED, "idx_strict", new 
String[][] {{colName2}}));
+
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(() -> assertEquals(1, 
tc.loadTable(tid).index().length));
+
+    tc.alterTable(tid, TableChange.deleteIndex("idx_strict", false));
+
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(() -> assertEquals(0, 
tc.loadTable(tid).index().length));
+  }
+
+  @Test
+  void testNoOpDeleteComposesWithUnrelatedChanges() {
+    TableCatalog tc = catalog.asTableCatalog();
+    NameIdentifier addColumnFirst =
+        NameIdentifier.of(schemaName, 
GravitinoITUtils.genRandomName("t_noop_column_first"));
+    NameIdentifier addColumnLast =
+        NameIdentifier.of(schemaName, 
GravitinoITUtils.genRandomName("t_noop_column_last"));
+    NameIdentifier addIndexFirst =
+        NameIdentifier.of(schemaName, 
GravitinoITUtils.genRandomName("t_noop_index_first"));
+    NameIdentifier addIndexLast =
+        NameIdentifier.of(schemaName, 
GravitinoITUtils.genRandomName("t_noop_index_last"));
+
+    for (NameIdentifier tid :
+        new NameIdentifier[] {addColumnFirst, addColumnLast, addIndexFirst, 
addIndexLast}) {
+      tc.createTable(
+          tid,
+          basicColumns(),
+          tableComment,
+          Collections.emptyMap(),
+          Transforms.EMPTY_TRANSFORM,
+          hashDist(),
+          null,
+          null);
+    }
+
+    tc.alterTable(
+        addColumnFirst,
+        TableChange.deleteIndex("idx_missing", true),
+        TableChange.addColumn(new String[] {"col_extra"}, 
Types.VarCharType.of(100)));
+    tc.alterTable(
+        addColumnLast,
+        TableChange.addColumn(new String[] {"col_extra"}, 
Types.VarCharType.of(100)),
+        TableChange.deleteIndex("idx_missing", true));
+
+    for (NameIdentifier tid : new NameIdentifier[] {addColumnFirst, 
addColumnLast}) {
+      Awaitility.await()
+          .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+          .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+          .untilAsserted(() -> assertEquals(3, 
tc.loadTable(tid).columns().length));
+    }
+
+    tc.alterTable(
+        addIndexFirst,
+        TableChange.deleteIndex("idx_missing", true),
+        TableChange.addIndex(
+            Index.IndexType.INVERTED, "idx_unrelated", new String[][] 
{{colName2}}));
+    tc.alterTable(
+        addIndexLast,
+        TableChange.addIndex(
+            Index.IndexType.INVERTED, "idx_unrelated", new String[][] 
{{colName2}}),
+        TableChange.deleteIndex("idx_missing", true));
+
+    for (NameIdentifier tid : new NameIdentifier[] {addIndexFirst, 
addIndexLast}) {
+      Awaitility.await()
+          .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+          .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+          .untilAsserted(() -> assertEquals(1, 
tc.loadTable(tid).index().length));
+    }
+  }
+
+  @Test
+  void testDeleteIndexDoesNotSuppressMissingTable() {
+    TableCatalog tc = catalog.asTableCatalog();
+    NameIdentifier tid =
+        NameIdentifier.of(schemaName, 
GravitinoITUtils.genRandomName("t_missing_table"));
+
+    NoSuchTableException exception =
+        assertThrows(
+            NoSuchTableException.class,
+            () -> tc.alterTable(tid, TableChange.deleteIndex("idx_missing", 
true)));
+    assertTrue(exception.getMessage().contains(tid.name()), 
exception.getMessage());
+  }
+
   @Test
   void testCreateTableWithAutoIncrement() {
     TableCatalog tc = catalog.asTableCatalog();
diff --git 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/operation/TestDorisTableOperations.java
 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/operation/TestDorisTableOperations.java
index a503eb3c5e..93fc33d12f 100644
--- 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/operation/TestDorisTableOperations.java
+++ 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/operation/TestDorisTableOperations.java
@@ -669,20 +669,18 @@ public class TestDorisTableOperations extends TestDoris {
         indexes);
     JdbcTable load = TABLE_OPERATIONS.load(databaseName, tableName);
 
-    // If ifExists is set to true then the code should not throw an exception 
if the index doesn't
-    // exist.
+    // If ifExists is set to true then the missing index should be a 
metadata-aware no-op.
     TableChange.DeleteIndex deleteIndex = new TableChange.DeleteIndex("uk_1", 
true);
-    String sql = DorisTableOperations.deleteIndexDefinition(null, deleteIndex);
-    Assertions.assertEquals("DROP INDEX `uk_1`", sql);
+    String sql = DorisTableOperations.deleteIndexDefinition(load, deleteIndex);
+    Assertions.assertEquals("", sql);
 
-    // The index existence check should only verify existence when ifExists is 
false, preventing
-    // failures when dropping non-existent indexes.
+    // Strict mode should continue to fail when dropping a non-existent index.
     TableChange.DeleteIndex deleteIndex2 = new TableChange.DeleteIndex("uk_1", 
false);
     IllegalArgumentException thrown =
         Assertions.assertThrows(
             IllegalArgumentException.class,
             () -> DorisTableOperations.deleteIndexDefinition(load, 
deleteIndex2));
-    Assertions.assertEquals("Index does not exist", thrown.getMessage());
+    Assertions.assertEquals("Index does not exist: uk_1", thrown.getMessage());
 
     TableChange.DeleteIndex deleteIndex3 = new TableChange.DeleteIndex("uk_2", 
false);
     sql = DorisTableOperations.deleteIndexDefinition(load, deleteIndex3);
diff --git 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/operation/TestDorisTableOperationsSqlGeneration.java
 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/operation/TestDorisTableOperationsSqlGeneration.java
index 105549ff7c..9b9b7de262 100644
--- 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/operation/TestDorisTableOperationsSqlGeneration.java
+++ 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/operation/TestDorisTableOperationsSqlGeneration.java
@@ -54,6 +54,9 @@ import org.mockito.Mockito;
 public class TestDorisTableOperationsSqlGeneration {
 
   private static class TestableDorisTableOperations extends 
DorisTableOperations {
+    private JdbcTable tableForAlter =
+        
JdbcTable.builder().withName("test_table").withIndexes(Indexes.EMPTY_INDEXES).build();
+
     public TestableDorisTableOperations() {
       super.exceptionMapper = new JdbcExceptionConverter();
       super.typeConverter = new DorisTypeConverter();
@@ -105,10 +108,14 @@ public class TestDorisTableOperationsSqlGeneration {
       return generateAlterTableSql("database", tableName, changes);
     }
 
+    void setTableForAlter(JdbcTable table) {
+      this.tableForAlter = table;
+    }
+
     @Override
     protected JdbcTable getOrCreateTable(
         String databaseName, String tableName, JdbcTable lazyLoadCreateTable) {
-      return JdbcTable.builder().withName(tableName).build();
+      return tableForAlter;
     }
 
     public String createTableSqlWithIndexes(
@@ -777,6 +784,114 @@ public class TestDorisTableOperationsSqlGeneration {
     Assertions.assertEquals("DROP INDEX `idx_name`", sql);
   }
 
+  @Test
+  public void testDeleteIndexDefinitionReturnsEmptyFragmentForMissingIndex() {
+    JdbcTable table = tableWithIndexes("idx_existing");
+
+    TableChange.DeleteIndex deleteIndex =
+        (TableChange.DeleteIndex) TableChange.deleteIndex("idx_missing", true);
+    Assertions.assertEquals("", 
DorisTableOperations.deleteIndexDefinition(table, deleteIndex));
+
+    TableChange.DeleteIndex strictDeleteIndex =
+        (TableChange.DeleteIndex) TableChange.deleteIndex("idx_missing", 
false);
+    IllegalArgumentException exception =
+        Assertions.assertThrows(
+            IllegalArgumentException.class,
+            () -> DorisTableOperations.deleteIndexDefinition(table, 
strictDeleteIndex));
+    Assertions.assertEquals("Index does not exist: idx_missing", 
exception.getMessage());
+  }
+
+  @Test
+  public void testNoOpDeleteIsFilteredFromAlterSql() {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    ops.setTableForAlter(tableWithIndexes());
+
+    Assertions.assertEquals(
+        "", ops.alterTableSql("test_table", 
TableChange.deleteIndex("idx_missing", true)));
+
+    TableChange[] unrelatedChanges =
+        new TableChange[] {
+          TableChange.addColumn(new String[] {"col2"}, 
Types.IntegerType.get()),
+          TableChange.updateColumnComment(new String[] {"col1"}, "updated 
comment"),
+          TableChange.setProperty(REPLICATION_FACTOR, "1"),
+          TableChange.addIndex(Index.IndexType.INVERTED, "idx_new", new 
String[][] {{"col1"}})
+        };
+    String[] unrelatedFragments =
+        new String[] {
+          "ADD COLUMN `col2`",
+          "MODIFY COLUMN `col1` COMMENT 'updated comment'",
+          "set (",
+          "ADD INDEX `idx_new`"
+        };
+    for (int i = 0; i < unrelatedChanges.length; i++) {
+      TableChange unrelatedChange = unrelatedChanges[i];
+      String noOpFirstSql =
+          ops.alterTableSql(
+              "test_table", TableChange.deleteIndex("idx_missing", true), 
unrelatedChange);
+      String noOpLastSql =
+          ops.alterTableSql(
+              "test_table", unrelatedChange, 
TableChange.deleteIndex("idx_missing", true));
+
+      Assertions.assertFalse(noOpFirstSql.contains("DROP INDEX 
`idx_missing`"), noOpFirstSql);
+      Assertions.assertFalse(noOpLastSql.contains("DROP INDEX `idx_missing`"), 
noOpLastSql);
+      Assertions.assertTrue(noOpFirstSql.contains(unrelatedFragments[i]), 
noOpFirstSql);
+      Assertions.assertTrue(noOpLastSql.contains(unrelatedFragments[i]), 
noOpLastSql);
+    }
+  }
+
+  @Test
+  public void testIndexChangeConflictsFailFastBeforeJdbc() throws Exception {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    DataSource dataSource = Mockito.mock(DataSource.class);
+    Connection connection = Mockito.mock(Connection.class);
+    Mockito.when(dataSource.getConnection()).thenReturn(connection);
+    ops.setDataSource(dataSource);
+
+    TableChange[][] duplicateDeleteChanges =
+        new TableChange[][] {
+          {TableChange.deleteIndex("idx", true), 
TableChange.deleteIndex("idx", true)},
+          {TableChange.deleteIndex("idx", true), 
TableChange.deleteIndex("idx", false)},
+          {TableChange.deleteIndex("idx", false), 
TableChange.deleteIndex("idx", true)},
+          {TableChange.deleteIndex("idx", false), 
TableChange.deleteIndex("idx", false)}
+        };
+    ops.setTableForAlter(tableWithIndexes("idx"));
+    for (TableChange[] changes : duplicateDeleteChanges) {
+      assertIndexChangeConflict(
+          ops,
+          connection,
+          "Index 'idx' cannot be deleted more than once in the same request",
+          changes);
+    }
+
+    TableChange.AddIndex addIndex =
+        (TableChange.AddIndex)
+            TableChange.addIndex(Index.IndexType.INVERTED, "idx", new 
String[][] {{"col1"}});
+    TableChange[][] addDeleteChanges =
+        new TableChange[][] {
+          {addIndex, TableChange.deleteIndex("idx", true)},
+          {TableChange.deleteIndex("idx", true), addIndex},
+          {addIndex, TableChange.deleteIndex("idx", false)},
+          {TableChange.deleteIndex("idx", false), addIndex}
+        };
+    JdbcTable[] addDeleteTables =
+        new JdbcTable[] {
+          tableWithIndexes(), tableWithIndexes(), tableWithIndexes("idx"), 
tableWithIndexes("idx")
+        };
+    for (int i = 0; i < addDeleteChanges.length; i++) {
+      ops.setTableForAlter(addDeleteTables[i]);
+      assertIndexChangeConflict(
+          ops,
+          connection,
+          "Index 'idx' cannot be added and deleted in the same request",
+          addDeleteChanges[i]);
+    }
+
+    ops.setTableForAlter(tableWithIndexes());
+    ops.alterTable("database", "test_table", 
TableChange.deleteIndex("idx_missing", true));
+    Mockito.verify(connection, Mockito.never()).createStatement();
+    Mockito.verify(connection, 
Mockito.never()).prepareStatement(Mockito.anyString());
+  }
+
   @Test
   public void testIsVersionAtLeast() {
     // Exact match
@@ -839,6 +954,30 @@ public class TestDorisTableOperationsSqlGeneration {
         exception.getMessage());
   }
 
+  private static void assertIndexChangeConflict(
+      TestableDorisTableOperations ops,
+      Connection connection,
+      String expectedMessage,
+      TableChange[] changes)
+      throws Exception {
+    IllegalArgumentException exception =
+        Assertions.assertThrows(
+            IllegalArgumentException.class,
+            () -> ops.alterTable("database", "test_table", changes));
+
+    Assertions.assertEquals(expectedMessage, exception.getMessage());
+    Mockito.verify(connection, Mockito.never()).createStatement();
+    Mockito.verify(connection, 
Mockito.never()).prepareStatement(Mockito.anyString());
+  }
+
+  private static JdbcTable tableWithIndexes(String... indexNames) {
+    Index[] indexes = new Index[indexNames.length];
+    for (int i = 0; i < indexNames.length; i++) {
+      indexes[i] = Indexes.of(Index.IndexType.INVERTED, indexNames[i], new 
String[][] {{"col1"}});
+    }
+    return 
JdbcTable.builder().withName("test_table").withIndexes(indexes).build();
+  }
+
   private static void assertInvalidAddIndex(String[][] fields, String 
expectedMessage) {
     TableChange.AddIndex addIndex =
         (TableChange.AddIndex) TableChange.addIndex(Index.IndexType.INVERTED, 
"idx_name", fields);

Reply via email to