Copilot commented on code in PR #11715:
URL: https://github.com/apache/gravitino/pull/11715#discussion_r3427939856


##########
catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/operation/DorisTableOperations.java:
##########
@@ -71,12 +73,300 @@ public class DorisTableOperations extends 
JdbcTableOperations {
   private static final String DORIS_AUTO_INCREMENT = "AUTO_INCREMENT";
   private static final String NEW_LINE = "\n";
 
+  // Cache the last SHOW CREATE TABLE result to avoid N+1 queries when loading 
columns.
+  // getColumnBuilder is called once per column for the same table, so caching 
by db+table
+  // eliminates redundant queries within a single listColumns/loadTable call.
+  //
+  // Thread safety: DorisTableOperations is a singleton shared across threads. 
The fields
+  // are wrapped in a single immutable holder and written via a volatile 
reference, so any thread
+  // that reads the reference sees a consistent snapshot — no partial writes.
+  // Two threads can both miss the cache simultaneously and each write the 
same value; this
+  // is a benign race (last-write wins, both writes produce identical content).
+  private static final class ShowCreateCache {
+    final String db;
+    final String table;
+    final String sql;
+    final Map<String, String> generatedColumns;
+
+    ShowCreateCache(String db, String table, String sql) {
+      this.db = db;
+      this.table = table;
+      this.sql = sql;
+      this.generatedColumns = extractGeneratedColumnExpressions(sql);
+    }
+  }
+
+  private volatile ShowCreateCache showCreateCache = null;
+
   @Override
   public JdbcTablePartitionOperations 
createJdbcTablePartitionOperations(JdbcTable loadedTable) {
     return new DorisTablePartitionOperations(
         dataSource, loadedTable, exceptionMapper, typeConverter);
   }
 
+  /**
+   * Override getColumnBuilder to get complete type information for complex 
types. Doris JDBC driver
+   * returns base types (e.g., TEXT, INT) for nested types (ARRAY, MAP, 
STRUCT) instead of full type
+   * strings. This method uses SHOW CREATE TABLE to get the complete type 
definition.
+   */
+  @Override
+  protected JdbcColumn.Builder getColumnBuilder(
+      ResultSet columnsResult, String databaseName, String tableName) throws 
SQLException {
+    String columnName = columnsResult.getString("COLUMN_NAME");
+    if (!Objects.equals(columnsResult.getString("TABLE_NAME"), tableName)) {
+      return null;
+    }
+
+    // Try to get full type from SHOW CREATE TABLE.
+    // The cache holds both the raw SQL and the pre-computed generated-column 
map, so neither
+    // parseColumnTypeFromCreateTable nor extractGeneratedColumnExpressions 
re-runs per column.
+    ShowCreateCache cache = getShowCreateCache(databaseName, tableName);
+    String fullTypeName =
+        cache != null ? parseColumnTypeFromCreateTable(cache.sql, columnName) 
: null;
+    if (fullTypeName != null) {
+      // Use the pre-computed generated-column map from the cache (computed 
once per table load,
+      // not once per column) to detect generated columns and retrieve their 
expressions.
+      Map<String, String> genExpressions = cache.generatedColumns;
+      boolean isExpression = genExpressions.containsKey(columnName);
+
+      JdbcTypeConverter.JdbcTypeBean typeBean = new 
JdbcTypeConverter.JdbcTypeBean(fullTypeName);
+      int columnSize = columnsResult.getInt("COLUMN_SIZE");
+      int scale = columnsResult.getInt("DECIMAL_DIGITS");
+      typeBean.setColumnSize(columnSize);
+      typeBean.setScale(scale);
+      Integer datetimePrecision = calculateDatetimePrecision(fullTypeName, 
columnSize, scale);
+      typeBean.setDatetimePrecision(datetimePrecision);

Review Comment:
   getColumnBuilder() feeds the full SHOW CREATE TABLE type string (e.g., 
`int(11)`, `varchar(100)`, `decimal(10,2)`) into DorisTypeConverter via 
JdbcTypeBean. DorisTypeConverter.toGravitino() only matches base type names 
(e.g., `int`, `varchar`, `decimal`) so parameterized scalar types will fall 
into the default branch and be treated as ExternalType, breaking schema/type 
loading.



##########
catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/operation/DorisTableOperations.java:
##########
@@ -87,7 +377,18 @@ protected String generateCreateTableSql(
       Distribution distribution,
       Index[] indexes) {
 
-    validateIncrementCol(columns);
+    // Log index information for debugging
+    LOG.debug("generateCreateTableSql: tableName={}, indexes.length={}", 
tableName, indexes.length);
+    for (int i = 0; i < indexes.length; i++) {
+      LOG.debug(
+          "  indexes[{}]: name={}, type={}, type.name()={}",
+          i,
+          indexes[i].name(),
+          indexes[i].type(),
+          indexes[i].type().name());
+    }

Review Comment:
   The debug logging here iterates over all indexes unconditionally. Even when 
debug logging is disabled, the loop still runs and calls into the index 
objects, adding overhead on every CREATE TABLE SQL generation. Wrap this block 
in an `if (LOG.isDebugEnabled())` guard (or remove it) to avoid unnecessary 
work in production.



##########
catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/utils/DorisUtils.java:
##########
@@ -98,23 +101,27 @@ public static Map<String, String> 
extractPropertiesFromSql(String createTableSql
 
   public static Optional<Transform> extractPartitionInfoFromSql(String 
createTableSql) {
     try {
-      String[] lines = createTableSql.split("\n");
-      for (String line : lines) {
-        Matcher matcher = PARTITION_INFO_PATTERN.matcher(line.trim());
-        if (matcher.matches()) {
-          String partitionType = matcher.group(1);
-          String partitionInfoString = matcher.group(2);
-          String[] columns =
-              Arrays.stream(partitionInfoString.split(", "))
-                  .map(s -> s.substring(1, s.length() - 1))
-                  .toArray(String[]::new);
-          if (LIST_PARTITION.equals(partitionType)) {
-            String[][] filedNames =
-                Arrays.stream(columns).map(s -> new String[] 
{s}).toArray(String[][]::new);
-            return Optional.of(Transforms.list(filedNames));
-          } else if (RANGE_PARTITION.equals(partitionType)) {
-            return Optional.of(Transforms.range(new String[] {columns[0]}));
+      // Merge all lines to handle multi-line partition definitions
+      String mergedSql = createTableSql.replaceAll("\\n", " ");
+      Matcher matcher = PARTITION_INFO_PATTERN.matcher(mergedSql);
+      if (matcher.find()) {
+        String partitionType = matcher.group(1);
+        String partitionInfoString = matcher.group(2);
+        String[] columns =
+            Arrays.stream(partitionInfoString.split(", "))
+                .map(s -> s.substring(1, s.length() - 1))
+                .toArray(String[]::new);

Review Comment:
   Partition column extraction splits on ", " and strips backticks via 
`substring(1, len-1)`, which is brittle (fails for `,` without spaces or extra 
whitespace). This can mis-parse LIST/RANGE partition columns from SHOW CREATE 
TABLE output.



##########
catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/utils/TestDorisUtils.java:
##########
@@ -100,42 +97,57 @@ public void testExtractPartitionInfoFromSql() {
     String createTableSql =
         "CREATE TABLE `testTable` (\n`col1` date NOT NULL\n) ENGINE=OLAP\n 
PARTITION BY RANGE(`col1`)\n()\n DISTRIBUTED BY HASH(`col1`) BUCKETS 2";
     Optional<Transform> transform = 
DorisUtils.extractPartitionInfoFromSql(createTableSql);
-    assertTrue(transform.isPresent());
-    assertEquals(Transforms.range(new String[] {"col1"}), transform.get());
+    Assertions.assertTrue(transform.isPresent());
+    Assertions.assertEquals(Transforms.range(new String[] {"col1"}), 
transform.get());
 
-    // test list partition
+    // test list partition (no space between LIST and parenthesis)
     createTableSql =
         "CREATE TABLE `testTable` (\n`col1` int(11) NOT NULL\n) ENGINE=OLAP\n 
PARTITION BY LIST(`col1`)\n()\n DISTRIBUTED BY HASH(`col1`) BUCKETS 2";
     transform = DorisUtils.extractPartitionInfoFromSql(createTableSql);
-    assertTrue(transform.isPresent());
-    assertEquals(Transforms.list(new String[][] {{"col1"}}), transform.get());
+    Assertions.assertTrue(transform.isPresent());
+    Assertions.assertEquals(Transforms.list(new String[][] {{"col1"}}), 
transform.get());
+
+    // test list partition with space (Doris 3.0+ format: "PARTITION BY LIST 
(`col1`)")
+    createTableSql =
+        "CREATE TABLE `testTable` (\n`col1` int(11) NOT NULL\n) ENGINE=OLAP\n 
PARTITION BY LIST (`col1`)\n()\n DISTRIBUTED BY HASH(`col1`) BUCKETS 2";
+    transform = DorisUtils.extractPartitionInfoFromSql(createTableSql);
+    Assertions.assertTrue(transform.isPresent());
+    Assertions.assertEquals(Transforms.list(new String[][] {{"col1"}}), 
transform.get());
+
+    // test range partition with space (Doris 3.0+ may also add space for 
RANGE)
+    createTableSql =
+        "CREATE TABLE `testTable` (\n`col1` date NOT NULL\n) ENGINE=OLAP\n 
PARTITION BY RANGE (`col1`)\n()\n DISTRIBUTED BY HASH(`col1`) BUCKETS 2";
+    transform = DorisUtils.extractPartitionInfoFromSql(createTableSql);
+    Assertions.assertTrue(transform.isPresent());
+    Assertions.assertEquals(Transforms.range(new String[] {"col1"}), 
transform.get());

Review Comment:
   New LIST partition assignment extraction logic in DorisUtils is not covered 
by unit tests. Adding a SHOW CREATE TABLE example with `PARTITION ... VALUES IN 
(...)` and asserting `Transforms.list(fieldNames, assignments)` would prevent 
regressions (especially around quoting/backticks and multi-partition parsing).



##########
catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/utils/DorisUtils.java:
##########
@@ -124,6 +131,106 @@ public static Optional<Transform> 
extractPartitionInfoFromSql(String createTable
     }
   }
 
+  private static ListPartition[] extractListPartitionAssignments(String 
mergedSql) {
+    try {
+      // Locate "PARTITION <name> VALUES IN (" and extract the outer paren 
content manually
+      // to correctly handle multi-column partitions: VALUES IN (("a", 1), 
("b", 2))
+      Pattern headerPattern = 
Pattern.compile("PARTITION\\s+(\\w+)\\s+VALUES\\s+IN\\s*\\(");
+      Matcher matcher = headerPattern.matcher(mergedSql);

Review Comment:
   extractListPartitionAssignments() only matches partition names via `\w+`, 
but Doris DDL commonly backticks identifiers (and partition names can include 
characters outside `\w`). This prevents assignment extraction from working on 
typical SHOW CREATE TABLE output like `PARTITION `p1` VALUES IN (...)`.



##########
catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/operation/DorisTableOperations.java:
##########
@@ -228,28 +524,119 @@ private static void validateDistribution(Distribution 
distribution, JdbcColumn[]
     }
   }
 
+  /**
+   * Appends the Doris table model key declaration (e.g. {@code UNIQUE 
KEY(`id`)}) to the CREATE
+   * TABLE statement. This declaration must appear after the closing {@code )} 
of the column list
+   * and before {@code DISTRIBUTED BY}.
+   *
+   * <p>Mapping from Gravitino index type to Doris key model:
+   *
+   * <ul>
+   *   <li>{@code UNIQUE_KEY} → {@code UNIQUE KEY}
+   *   <li>{@code PRIMARY_KEY} → {@code UNIQUE KEY} (Gravitino uses 
PRIMARY_KEY to indicate a unique
+   *       key constraint; Doris PRIMARY KEY model requires Doris 3.0+ and a 
separate feature flag)
+   * </ul>
+   *
+   * If no PRIMARY_KEY or UNIQUE_KEY index is present, no declaration is 
appended and Doris defaults
+   * to the DUPLICATE KEY model.
+   */
+  private static void appendTableModelKeySql(Index[] indexes, StringBuilder 
sqlBuilder) {
+    // A well-formed table definition should contain at most one PRIMARY_KEY 
or UNIQUE_KEY index.
+    // We take the first match; passing multiple key-type indexes is a caller 
error and is not
+    // explicitly validated here (validateIncrementCol already enforces one 
auto-increment column,
+    // and Doris itself will reject duplicate key declarations at execution 
time).
+    //
+    // Both PRIMARY_KEY and UNIQUE_KEY are mapped to Doris UNIQUE KEY model 
here.
+    // Doris's own PRIMARY KEY model (introduced in 3.0) requires a separate 
feature flag and is
+    // intentionally not used — UNIQUE KEY is the safe, broadly-compatible 
equivalent.
+    Arrays.stream(indexes)
+        .filter(
+            index ->
+                index.type() == Index.IndexType.PRIMARY_KEY
+                    || index.type() == Index.IndexType.UNIQUE_KEY)
+        .findFirst()
+        .ifPresent(
+            keyIndex -> {
+              String cols =
+                  Arrays.stream(keyIndex.fieldNames())
+                      .map(fieldName -> BACK_QUOTE + fieldName[0] + BACK_QUOTE)
+                      .collect(Collectors.joining(", "));
+              sqlBuilder.append(NEW_LINE).append("UNIQUE 
KEY(").append(cols).append(")");
+            });
+  }
+
   private static void appendIndexesSql(Index[] indexes, StringBuilder 
sqlBuilder) {
     if (indexes.length == 0) {
       return;
     }
 
-    // validate indexes
-    Arrays.stream(indexes)
-        .forEach(
-            index -> {
-              if (index.fieldNames().length > 1) {
-                throw new IllegalArgumentException("Index does not support 
multi fields in Doris");
-              }
-            });
+    // Log index types for debugging
+    LOG.debug("appendIndexesSql: processing {} indexes", indexes.length);
+    for (Index index : indexes) {
+      LOG.debug("  index: name={}, type={}", index.name(), index.type());
+    }
 
-    String indexSql =
+    // Filter out PRIMARY_KEY and UNIQUE_KEY indexes — they are table model 
keys in Doris,
+    // defined via UNIQUE KEY(col) / DUPLICATE KEY(col) syntax, not via INDEX 
clause.
+    List<Index> nonKeyIndexes =
         Arrays.stream(indexes)
-            .map(index -> String.format("INDEX %s (%s)", index.name(), 
index.fieldNames()[0][0]))
+            .filter(
+                index ->
+                    index.type() != Index.IndexType.PRIMARY_KEY
+                        && index.type() != Index.IndexType.UNIQUE_KEY)
+            .collect(Collectors.toList());
+
+    LOG.debug("appendIndexesSql: {} non-key indexes after filtering", 
nonKeyIndexes.size());
+

Review Comment:
   appendIndexesSql() does debug logging before and after filtering, but the 
logging loops and the `nonKeyIndexes.size()` computation are executed 
regardless of log level. Wrapping the debug-only work in `LOG.isDebugEnabled()` 
avoids extra overhead during SQL generation when debug is disabled.



##########
catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/operation/TestDorisTableOperationsSqlGeneration.java:
##########
@@ -107,4 +123,274 @@ public void 
testCreateTableWithNonEmptyStringDefaultValue() {
         sql.contains("DEFAULT " + 
converter.fromGravitino(col1.defaultValue())),
         "Should contain DEFAULT value but was: " + sql);
   }
+
+  @Test
+  public void testCreateTableWithPrimaryKeyIndex() {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    JdbcColumn idCol =
+        JdbcColumn.builder()
+            .withName("id")
+            .withType(Types.IntegerType.get())
+            .withNullable(false)
+            .build();
+    JdbcColumn nameCol =
+        JdbcColumn.builder()
+            .withName("name")
+            .withType(Types.VarCharType.of(100))
+            .withNullable(true)
+            .build();
+    Distribution distribution = Distributions.hash(1, 
NamedReference.field("id"));
+
+    // PRIMARY_KEY index should be filtered out — Doris uses table model keys, 
not INDEX clause
+    Index[] indexes =
+        new Index[] {Indexes.of(Index.IndexType.PRIMARY_KEY, "PRIMARY", new 
String[][] {{"id"}})};
+
+    TestableDorisTableOperations mockOps = Mockito.spy(ops);
+    Mockito.doAnswer(a -> a.getArgument(0))
+        .when(mockOps)
+        .appendNecessaryProperties(Mockito.anyMap());
+
+    String sql =
+        mockOps.createTableSqlWithIndexes(
+            "test_pk", new JdbcColumn[] {idCol, nameCol}, distribution, 
indexes);
+    Assertions.assertFalse(
+        sql.contains("INDEX PRIMARY"), "PRIMARY_KEY should be filtered out: " 
+ sql);
+    Assertions.assertFalse(
+        sql.contains("USING INVERTED"), "No USING clause for PRIMARY_KEY: " + 
sql);
+  }
+
+  @Test
+  public void testCreateTableWithInvertedIndex() {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    JdbcColumn idCol =
+        JdbcColumn.builder()
+            .withName("id")
+            .withType(Types.IntegerType.get())
+            .withNullable(false)
+            .build();
+    JdbcColumn nameCol =
+        JdbcColumn.builder()
+            .withName("name")
+            .withType(Types.VarCharType.of(100))
+            .withNullable(true)
+            .build();
+    Distribution distribution = Distributions.hash(1, 
NamedReference.field("id"));
+
+    Index[] indexes =
+        new Index[] {Indexes.of(Index.IndexType.INVERTED, "idx_name", new 
String[][] {{"name"}})};
+
+    TestableDorisTableOperations mockOps = Mockito.spy(ops);
+    Mockito.doAnswer(a -> a.getArgument(0))
+        .when(mockOps)
+        .appendNecessaryProperties(Mockito.anyMap());
+
+    String sql =
+        mockOps.createTableSqlWithIndexes(
+            "test_inverted", new JdbcColumn[] {idCol, nameCol}, distribution, 
indexes);
+    Assertions.assertTrue(
+        sql.contains("INDEX `idx_name` (`name`) USING INVERTED"),
+        "Should generate INVERTED index: " + sql);
+  }
+
+  @Test
+  public void testCreateTableWithBitmapIndex() {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    JdbcColumn idCol =
+        JdbcColumn.builder()
+            .withName("id")
+            .withType(Types.IntegerType.get())
+            .withNullable(false)
+            .build();
+    JdbcColumn tagCol =
+        JdbcColumn.builder()
+            .withName("tag")
+            .withType(Types.IntegerType.get())
+            .withNullable(true)
+            .build();
+    Distribution distribution = Distributions.hash(1, 
NamedReference.field("id"));
+
+    Index[] indexes =
+        new Index[] {Indexes.of(Index.IndexType.BITMAP, "idx_tag", new 
String[][] {{"tag"}})};
+
+    TestableDorisTableOperations mockOps = Mockito.spy(ops);
+    Mockito.doAnswer(a -> a.getArgument(0))
+        .when(mockOps)
+        .appendNecessaryProperties(Mockito.anyMap());
+
+    String sql =
+        mockOps.createTableSqlWithIndexes(
+            "test_bitmap", new JdbcColumn[] {idCol, tagCol}, distribution, 
indexes);
+    Assertions.assertTrue(
+        sql.contains("INDEX `idx_tag` (`tag`) USING BITMAP"),
+        "Should generate BITMAP index: " + sql);
+  }
+
+  @Test
+  public void testMapDorisIndexType() {
+    Assertions.assertEquals(
+        Index.IndexType.PRIMARY_KEY, 
DorisTableOperations.mapDorisIndexType("BTREE", "PRIMARY"));
+    Assertions.assertEquals(
+        Index.IndexType.INVERTED, 
DorisTableOperations.mapDorisIndexType("INVERTED", "idx_name"));
+    Assertions.assertEquals(
+        Index.IndexType.BITMAP, 
DorisTableOperations.mapDorisIndexType("BITMAP", "idx_name"));
+    Assertions.assertEquals(
+        Index.IndexType.DATA_SKIPPING_BLOOM_FILTER,
+        DorisTableOperations.mapDorisIndexType("BLOOMFILTER", "idx_name"));
+    Assertions.assertEquals(
+        Index.IndexType.VECTOR, DorisTableOperations.mapDorisIndexType("ANN", 
"idx_name"));
+    // Unknown type should fall back to INVERTED
+    Assertions.assertEquals(
+        Index.IndexType.INVERTED,
+        DorisTableOperations.mapDorisIndexType("UNKNOWN_TYPE", "idx_name"));
+  }
+
+  @Test
+  public void testCreateTableWithAutoIncrement() {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    JdbcColumn idCol =
+        JdbcColumn.builder()
+            .withName("id")
+            .withType(Types.LongType.get())
+            .withNullable(false)
+            .withAutoIncrement(true)
+            .build();
+    JdbcColumn nameCol =
+        JdbcColumn.builder()
+            .withName("name")
+            .withType(Types.VarCharType.of(100))
+            .withNullable(true)
+            .build();
+    Distribution distribution = Distributions.hash(1, 
NamedReference.field("id"));
+
+    Index[] indexes =
+        new Index[] {Indexes.of(Index.IndexType.PRIMARY_KEY, "PRIMARY", new 
String[][] {{"id"}})};
+
+    TestableDorisTableOperations mockOps = Mockito.spy(ops);
+    Mockito.doAnswer(a -> a.getArgument(0))
+        .when(mockOps)
+        .appendNecessaryProperties(Mockito.anyMap());
+
+    String sql =
+        mockOps.createTableSqlWithIndexes(
+            "test_auto_incr", new JdbcColumn[] {idCol, nameCol}, distribution, 
indexes);
+    Assertions.assertTrue(sql.contains("AUTO_INCREMENT"), "Should contain 
AUTO_INCREMENT: " + sql);
+    Assertions.assertFalse(sql.contains("INDEX PRIMARY"), "PRIMARY_KEY should 
be filtered: " + sql);
+    Assertions.assertTrue(
+        sql.contains("UNIQUE KEY(`id`)"), "Should contain UNIQUE KEY 
declaration: " + sql);
+  }
+
+  @Test
+  public void testAddIndexDefinition() {
+    // INVERTED index
+    TableChange.AddIndex addIndex =
+        (TableChange.AddIndex)
+            TableChange.addIndex(Index.IndexType.INVERTED, "idx_name", new 
String[][] {{"col1"}});
+    String sql = DorisTableOperations.addIndexDefinition(addIndex);
+    Assertions.assertEquals("ADD INDEX `idx_name` (`col1`) USING INVERTED", 
sql);
+
+    // BITMAP index
+    addIndex =
+        (TableChange.AddIndex)
+            TableChange.addIndex(Index.IndexType.BITMAP, "idx_tag", new 
String[][] {{"tag"}});
+    sql = DorisTableOperations.addIndexDefinition(addIndex);
+    Assertions.assertEquals("ADD INDEX `idx_tag` (`tag`) USING BITMAP", sql);
+
+    // VECTOR index (maps to ANN)
+    addIndex =
+        (TableChange.AddIndex)
+            TableChange.addIndex(Index.IndexType.VECTOR, "idx_vec", new 
String[][] {{"embedding"}});
+    sql = DorisTableOperations.addIndexDefinition(addIndex);
+    Assertions.assertEquals("ADD INDEX `idx_vec` (`embedding`) USING ANN", 
sql);
+  }
+
+  @Test
+  public void testExtractGeneratedColumnExpressions() {
+    // Single generated column
+    String createTableSql =
+        "CREATE TABLE `test_gen` (\n"
+            + "  `id` int(11) NOT NULL,\n"
+            + "  `price` double NULL,\n"
+            + "  `qty` int(11) NULL,\n"
+            + "  `total` double NULL AS (price * qty)\n"
+            + ") ENGINE=OLAP\n"
+            + "DUPLICATE KEY(`id`)\n"
+            + "DISTRIBUTED BY HASH(`id`) BUCKETS 10\n"
+            + "PROPERTIES (\"replication_num\" = \"1\")";
+    Map<String, String> result =
+        DorisTableOperations.extractGeneratedColumnExpressions(createTableSql);
+    Assertions.assertEquals(1, result.size());
+    Assertions.assertEquals("price * qty", result.get("total"));
+
+    // Multiple generated columns
+    createTableSql =
+        "CREATE TABLE `test_gen2` (\n"
+            + "  `id` int(11) NOT NULL,\n"
+            + "  `a` int(11) NULL,\n"
+            + "  `b` int(11) NULL,\n"
+            + "  `sum_ab` int(11) NULL AS (a + b),\n"
+            + "  `prod_ab` int(11) NULL AS (a * b)\n"
+            + ") ENGINE=OLAP\n"
+            + "DUPLICATE KEY(`id`)\n"
+            + "DISTRIBUTED BY HASH(`id`) BUCKETS 10";
+    result = 
DorisTableOperations.extractGeneratedColumnExpressions(createTableSql);
+    Assertions.assertEquals(2, result.size());
+    Assertions.assertEquals("a + b", result.get("sum_ab"));
+    Assertions.assertEquals("a * b", result.get("prod_ab"));
+
+    // No generated columns
+    createTableSql =
+        "CREATE TABLE `test_no_gen` (\n"
+            + "  `id` int(11) NOT NULL,\n"
+            + "  `name` varchar(100) NULL\n"
+            + ") ENGINE=OLAP\n"
+            + "DUPLICATE KEY(`id`)\n"
+            + "DISTRIBUTED BY HASH(`id`) BUCKETS 10";
+    result = 
DorisTableOperations.extractGeneratedColumnExpressions(createTableSql);
+    Assertions.assertTrue(result.isEmpty());
+
+    // Generated column with expression containing parentheses (CASE WHEN)
+    createTableSql =
+        "CREATE TABLE `test_gen3` (\n"
+            + "  `id` int(11) NOT NULL,\n"
+            + "  `score` int(11) NULL,\n"
+            + "  `grade` varchar(10) NULL AS (CASE WHEN score >= 90 THEN 'A' 
ELSE 'B' END)\n"
+            + ") ENGINE=OLAP\n"
+            + "DUPLICATE KEY(`id`)\n"
+            + "DISTRIBUTED BY HASH(`id`) BUCKETS 10";
+    result = 
DorisTableOperations.extractGeneratedColumnExpressions(createTableSql);
+    Assertions.assertEquals(1, result.size());
+    Assertions.assertEquals("CASE WHEN score >= 90 THEN 'A' ELSE 'B' END", 
result.get("grade"));
+
+    // Table with a struct<> column AND a generated column — verifies <> depth 
tracking does not
+    // split the struct field list at its inner comma
+    createTableSql =
+        "CREATE TABLE `test_struct_gen` (\n"
+            + "  `id` int(11) NOT NULL,\n"
+            + "  `point` struct<x:int,y:int> NULL,\n"
+            + "  `total` double NULL AS (id * 2)\n"
+            + ") ENGINE=OLAP\n"
+            + "DUPLICATE KEY(`id`)\n"
+            + "DISTRIBUTED BY HASH(`id`) BUCKETS 10";
+    result = 
DorisTableOperations.extractGeneratedColumnExpressions(createTableSql);
+    Assertions.assertEquals(1, result.size());
+    Assertions.assertEquals("id * 2", result.get("total"));
+  }
+
+  @Test
+  public void testDeleteIndexDefinition() {
+    // deleteIndexDefinition should quote the index name with backticks, 
matching addIndexDefinition
+    JdbcTable mockTable =
+        JdbcTable.builder()
+            .withName("t")
+            .withColumns(new org.apache.gravitino.catalog.jdbc.JdbcColumn[0])
+            .withIndexes(

Review Comment:
   Avoid fully-qualified class names in code when an import already exists 
(repo guideline: prefer normal imports). `JdbcColumn` is already imported at 
the top of this test, so the FQN here is unnecessary.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to