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 3704b7a5bf [#12736] fix(doris): validate ADD INDEX fields (#12737)
3704b7a5bf is described below

commit 3704b7a5bfe978421e43d53d5472f6c52bc15073
Author: StormSpirit <[email protected]>
AuthorDate: Mon Aug 31 20:57:27 2026 +0800

    [#12736] fix(doris): validate ADD INDEX fields (#12737)
    
    ### What changes were proposed in this pull request?
    
    This pull request adds a Doris-specific field-shape validator shared by
    CREATE TABLE index generation and ALTER TABLE ADD INDEX. The validator
    requires exactly one non-blank top-level field and returns that
    validated field for SQL generation.
    
    It also adds focused tests for valid index SQL, malformed field shapes,
    consistent CREATE and ALTER validation, the no-JDBC-statement failure
    path, and the real Doris 4.x ADD/DROP INDEX lifecycle.
    
    ### Why are the changes needed?
    
    The Doris ALTER ADD INDEX path currently reads only
    `getFieldNames()[0][0]`. When a request contains multiple fields or a
    nested field path, Gravitino silently discards the remaining components
    and can create a valid single-column Doris index that does not match the
    request.
    
    Rejecting unsupported field shapes before DDL generation prevents this
    semantic truncation and makes CREATE and ALTER enforce the same Doris
    connector contract. Valid single-field index SQL and existing key-model
    behavior remain unchanged.
    
    Fix: #12736
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. Doris CREATE TABLE and ALTER TABLE ADD INDEX requests containing
    multiple fields, nested field paths, empty field shapes, or blank field
    names now fail with an explicit `IllegalArgumentException` instead of
    being truncated or producing invalid SQL. Valid single-field index
    operations are unchanged, and no public API or property key is added or
    removed.
    
    ### How was this patch tested?
    
    - `./gradlew :catalogs:catalog-jdbc-doris:spotlessCheck`
    - `./gradlew rat`
    - `./gradlew :catalogs:catalog-jdbc-doris:test -PskipITs`
    - `./gradlew :catalogs:catalog-jdbc-doris:test --tests
    
'org.apache.gravitino.catalog.doris.integration.test.CatalogDoris4xIT.testAddAndDropInvertedIndex'
    -PskipDockerTests=false -PdorisMultiVersionTest`
    - `./gradlew :catalogs:catalog-jdbc-doris:build -x test`
    
    Signed-off-by: jiangxt2 <[email protected]>
---
 .../doris/operation/DorisTableOperations.java      | 44 +++++++----
 .../doris/integration/test/CatalogDoris4xIT.java   | 18 +++++
 .../TestDorisTableOperationsSqlGeneration.java     | 90 ++++++++++++++++++++++
 3 files changed, 136 insertions(+), 16 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 657dc04c12..b002af801b 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
@@ -344,25 +344,18 @@ public class DorisTableOperations extends 
JdbcTableOperations {
       return;
     }
 
-    nonKeyIndexes.forEach(
-        index -> {
-          if (index.fieldNames().length > 1) {
-            throw new IllegalArgumentException(
-                "Index '" + index.name() + "' does not support multi fields in 
Doris");
-          }
-        });
-
     String indexSql =
         nonKeyIndexes.stream()
             .map(
                 index -> {
+                  String fieldName =
+                      requireSingleTopLevelIndexField(index.name(), 
index.fieldNames());
                   String usingClause = mapIndexTypeToUsingClause(index.type());
                   if (usingClause.isEmpty()) {
-                    return String.format(
-                        "INDEX `%s` (`%s`)", index.name(), 
index.fieldNames()[0][0]);
+                    return String.format("INDEX `%s` (`%s`)", index.name(), 
fieldName);
                   }
                   return String.format(
-                      "INDEX `%s` (`%s`) %s", index.name(), 
index.fieldNames()[0][0], usingClause);
+                      "INDEX `%s` (`%s`) %s", index.name(), fieldName, 
usingClause);
                 })
             .collect(Collectors.joining(",\n"));
 
@@ -1012,14 +1005,13 @@ public class DorisTableOperations extends 
JdbcTableOperations {
       throw new UnsupportedOperationException(
           "PRIMARY_KEY and UNIQUE_KEY cannot be added via ALTER TABLE ADD 
INDEX in Doris");
     }
+    String fieldName =
+        requireSingleTopLevelIndexField(addIndex.getName(), 
addIndex.getFieldNames());
     String usingClause = mapIndexTypeToUsingClause(addIndex.getType());
     if (usingClause.isEmpty()) {
-      return String.format(
-          "ADD INDEX `%s` (`%s`)", addIndex.getName(), 
addIndex.getFieldNames()[0][0]);
+      return String.format("ADD INDEX `%s` (`%s`)", addIndex.getName(), 
fieldName);
     }
-    return String.format(
-        "ADD INDEX `%s` (`%s`) %s",
-        addIndex.getName(), addIndex.getFieldNames()[0][0], usingClause);
+    return String.format("ADD INDEX `%s` (`%s`) %s", addIndex.getName(), 
fieldName, usingClause);
   }
 
   static String deleteIndexDefinition(
@@ -1082,4 +1074,24 @@ public class DorisTableOperations extends 
JdbcTableOperations {
 
     return null;
   }
+
+  private static String requireSingleTopLevelIndexField(String indexName, 
String[][] fieldNames) {
+    Preconditions.checkArgument(
+        fieldNames != null && fieldNames.length == 1,
+        "Index '%s' supports exactly one top-level field in Doris, but got %s",
+        indexName,
+        fieldNames == null ? "null" : fieldNames.length);
+
+    String[] fieldPath = fieldNames[0];
+    Preconditions.checkArgument(
+        fieldPath != null && fieldPath.length == 1,
+        "Index '%s' supports exactly one top-level field in Doris, but got 
path %s",
+        indexName,
+        Arrays.toString(fieldPath));
+    Preconditions.checkArgument(
+        StringUtils.isNotBlank(fieldPath[0]),
+        "Index '%s' requires a non-blank top-level field in Doris",
+        indexName);
+    return fieldPath[0];
+  }
 }
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 39af05faf7..d438a0e8d8 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
@@ -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;
@@ -197,6 +198,23 @@ public class CatalogDoris4xIT extends BaseIT {
         null,
         null);
 
+    IllegalArgumentException exception =
+        assertThrows(
+            IllegalArgumentException.class,
+            () ->
+                tc.alterTable(
+                    tid,
+                    TableChange.addIndex(
+                        Index.IndexType.INVERTED,
+                        "idx_invalid",
+                        new String[][] {{colName1}, {colName2}})));
+    assertTrue(
+        exception
+            .getMessage()
+            .contains(
+                "Index 'idx_invalid' supports exactly one top-level field in 
Doris, but got 2"));
+    assertEquals(0, tc.loadTable(tid).index().length);
+
     // Add INVERTED index
     tc.alterTable(
         tid,
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 ba7b665c01..af1bc0dc47 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
@@ -447,6 +447,84 @@ public class TestDorisTableOperationsSqlGeneration {
     Assertions.assertEquals("ADD INDEX `idx_vec` (`embedding`) USING ANN", 
sql);
   }
 
+  @Test
+  public void testAddIndexDefinitionRejectsUnsupportedFieldShapes() {
+    assertInvalidAddIndex(
+        new String[][] {{"col1"}, {"col2"}},
+        "Index 'idx_name' supports exactly one top-level field in Doris, but 
got 2");
+    assertInvalidAddIndex(
+        new String[][] {{"payload", "nested"}},
+        "Index 'idx_name' supports exactly one top-level field in Doris, but 
got path "
+            + "[payload, nested]");
+    assertInvalidAddIndex(
+        new String[0][],
+        "Index 'idx_name' supports exactly one top-level field in Doris, but 
got 0");
+    assertInvalidAddIndex(
+        new String[][] {new String[0]},
+        "Index 'idx_name' supports exactly one top-level field in Doris, but 
got path []");
+    assertInvalidAddIndex(
+        new String[][] {null},
+        "Index 'idx_name' supports exactly one top-level field in Doris, but 
got path null");
+    assertInvalidAddIndex(
+        new String[][] {{" "}}, "Index 'idx_name' requires a non-blank 
top-level field in Doris");
+    assertInvalidAddIndex(
+        null, "Index 'idx_name' supports exactly one top-level field in Doris, 
but got null");
+  }
+
+  @Test
+  public void testCreateAndAlterIndexUseSameFieldShapeValidation() {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    TestableDorisTableOperations mockOps = Mockito.spy(ops);
+    Mockito.doAnswer(a -> a.getArgument(0))
+        .when(mockOps)
+        .appendNecessaryProperties(Mockito.anyMap());
+    JdbcColumn idColumn =
+        JdbcColumn.builder()
+            .withName("id")
+            .withType(Types.IntegerType.get())
+            .withNullable(false)
+            .build();
+    Distribution distribution = Distributions.hash(1, 
NamedReference.field("id"));
+    String[][] fields = {{"id"}, {"other"}};
+    Index[] indexes = {Indexes.of(Index.IndexType.INVERTED, "idx_name", 
fields)};
+
+    IllegalArgumentException createException =
+        Assertions.assertThrows(
+            IllegalArgumentException.class,
+            () ->
+                mockOps.createTableSqlWithIndexes(
+                    "test_table", new JdbcColumn[] {idColumn}, distribution, 
indexes));
+    IllegalArgumentException alterException =
+        Assertions.assertThrows(
+            IllegalArgumentException.class,
+            () ->
+                mockOps.alterTableSql(
+                    "test_table",
+                    TableChange.addIndex(Index.IndexType.INVERTED, "idx_name", 
fields)));
+
+    Assertions.assertEquals(createException.getMessage(), 
alterException.getMessage());
+  }
+
+  @Test
+  public void testInvalidAddIndexDoesNotExecuteJdbcStatement() 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);
+
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ops.alterTable(
+                "database",
+                "test_table",
+                TableChange.addIndex(
+                    Index.IndexType.INVERTED, "idx_name", new String[][] 
{{"col1"}, {"col2"}})));
+
+    Mockito.verify(connection, Mockito.never()).createStatement();
+  }
+
   @Test
   public void testAddPrimaryKeyIndexDefinitionThrows() {
     // PRIMARY_KEY cannot be added via ALTER TABLE ADD INDEX in Doris
@@ -546,6 +624,18 @@ public class TestDorisTableOperationsSqlGeneration {
         exception.getMessage());
   }
 
+  private static void assertInvalidAddIndex(String[][] fields, String 
expectedMessage) {
+    TableChange.AddIndex addIndex =
+        (TableChange.AddIndex) TableChange.addIndex(Index.IndexType.INVERTED, 
"idx_name", fields);
+
+    IllegalArgumentException exception =
+        Assertions.assertThrows(
+            IllegalArgumentException.class,
+            () -> DorisTableOperations.addIndexDefinition(addIndex));
+
+    Assertions.assertEquals(expectedMessage, exception.getMessage());
+  }
+
   private static DataSource mockBackendDataSource(int aliveBackendCount) 
throws Exception {
     DataSource dataSource = Mockito.mock(DataSource.class);
     Connection connection = Mockito.mock(Connection.class);

Reply via email to