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 c27520bd81 [#11803] fix(clickhouse): add ON CLUSTER support for ALTER 
TABLE (#11807)
c27520bd81 is described below

commit c27520bd817e9090cb1e9865444bd656988f89fd
Author: StormSpirit <[email protected]>
AuthorDate: Tue Jun 30 19:16:35 2026 +0800

    [#11803] fix(clickhouse): add ON CLUSTER support for ALTER TABLE (#11807)
    
    ### What changes were proposed in this pull request?
    
    - `generateAlterTableSql()` now checks `ON_CLUSTER` and `CLUSTER_NAME`
    table properties and injects `ON CLUSTER <clusterName>` into the ALTER
    TABLE SQL when both are set.
    - Adds `getOrCreateTable()` call for change types that do not otherwise
    require loading the table, so cluster metadata is available.
    - Adds null-safety for `properties()` access, consistent with
    `generateDropTableSql()`.
    - Omits `SYNC` (unlike `generateDropTableSql()`) because ClickHouse does
    not support synchronous ALTER operations.
    
    ### Why are the changes needed?
    
    Fix: #11803
    
    Previously, `generateAlterTableSql()` always generated `ALTER TABLE ...`
    without `ON CLUSTER`. For tables created with `ON CLUSTER`, subsequent
    ALTER operations only affected the local node, causing schema divergence
    across cluster nodes.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. ALTER TABLE operations on clustered tables now propagate to all
    nodes in the cluster, preventing schema divergence.
    
    ### How was this patch tested?
    
    - `./gradlew :catalogs-contrib:catalog-jdbc-clickhouse:test --tests
    
"org.apache.gravitino.catalog.clickhouse.operations.TestClickHouseTableOperationsCluster"
    -PskipITs`
    - `./gradlew :catalogs-contrib:catalog-jdbc-clickhouse:test --tests
    
"org.apache.gravitino.catalog.clickhouse.integration.test.CatalogClickHouseClusterIT"
    -PskipDockerTests=false`
    
    ---------
    
    Signed-off-by: jiangxt2 <[email protected]>
    Co-authored-by: Chang-Tong <[email protected]>
    Co-authored-by: ArtificialIdoit <[email protected]>
    Co-authored-by: cwq222 <[email protected]>
---
 .../operations/ClickHouseTableOperations.java      |  24 +++-
 .../test/CatalogClickHouseClusterIT.java           |  88 +++++++++++++++
 .../TestClickHouseTableOperationsCluster.java      | 123 +++++++++++++++++++++
 3 files changed, 232 insertions(+), 3 deletions(-)

diff --git 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
index fc823ef78c..e3cd9378e1 100644
--- 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
+++ 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
@@ -817,10 +817,28 @@ public class ClickHouseTableOperations extends 
JdbcTableOperations {
       return "";
     }
 
+    // Check if the table is on a cluster, so that ALTER TABLE includes ON 
CLUSTER
+    lazyLoadTable = getOrCreateTable(databaseName, tableName, lazyLoadTable);
+    Map<String, String> props = lazyLoadTable.properties();
+    String clusterName = props == null ? null : 
props.get(ClusterConstants.CLUSTER_NAME);
+    boolean onCluster =
+        props != null
+            && 
Boolean.parseBoolean(props.getOrDefault(ClusterConstants.ON_CLUSTER, "false"));
+
     // Return the generated SQL statement
-    String result =
-        "ALTER TABLE %s \n%s;"
-            .formatted(quoteIdentifier(tableName), String.join(",\n", 
nonEmptySQLs));
+    String result;
+    if (onCluster && StringUtils.isNotBlank(clusterName)) {
+      result =
+          "ALTER TABLE %s ON CLUSTER %s \n%s;"
+              .formatted(
+                  quoteIdentifier(tableName),
+                  quoteIdentifier(clusterName),
+                  String.join(",\n", nonEmptySQLs));
+    } else {
+      result =
+          "ALTER TABLE %s \n%s;"
+              .formatted(quoteIdentifier(tableName), String.join(",\n", 
nonEmptySQLs));
+    }
     LOG.info("Generated alter table:{} sql: {}", databaseName + "." + 
tableName, result);
     return result;
   }
diff --git 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseClusterIT.java
 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseClusterIT.java
index 7e7187277a..48d6ee8f89 100644
--- 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseClusterIT.java
+++ 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseClusterIT.java
@@ -745,4 +745,92 @@ public class CatalogClickHouseClusterIT extends BaseIT {
               schemaName, name, ClickHouseContainer.DEFAULT_CLUSTER_NAME));
     }
   }
+
+  /**
+   * Verifies that ALTER TABLE on a cluster table includes ON CLUSTER in the 
executed SQL, while
+   * ALTER TABLE on a non-cluster table does not. This validates the fix for 
generating correct
+   * ALTER TABLE statements with ON CLUSTER support.
+   */
+  @Test
+  public void testAlterTableOnClusterSqlGeneration() throws Exception {
+    String clusterTbl = GravitinoITUtils.genRandomName("ck_alter_on_cluster");
+    String nonClusterTbl = 
GravitinoITUtils.genRandomName("ck_alter_no_cluster");
+    NameIdentifier clusterIdent = NameIdentifier.of(schemaName, clusterTbl);
+    NameIdentifier nonClusterIdent = NameIdentifier.of(schemaName, 
nonClusterTbl);
+    TableCatalog tableCatalog = catalog.asTableCatalog();
+
+    // Create a cluster table and a non-cluster table
+    tableCatalog.createTable(
+        clusterIdent,
+        createColumns(),
+        tableComment,
+        clusterMergeTreeProperties(),
+        Transforms.EMPTY_TRANSFORM,
+        Distributions.NONE,
+        getSortOrders("col_3"),
+        Indexes.EMPTY_INDEXES);
+
+    tableCatalog.createTable(
+        nonClusterIdent,
+        createColumns(),
+        tableComment,
+        Collections.singletonMap(GRAVITINO_ENGINE_KEY, 
ENGINE.MERGETREE.getValue()),
+        Transforms.EMPTY_TRANSFORM,
+        Distributions.NONE,
+        getSortOrders("col_3"),
+        Indexes.EMPTY_INDEXES);
+
+    // ALTER both tables — add a column
+    tableCatalog.alterTable(
+        clusterIdent, TableChange.addColumn(new String[] {"new_col"}, 
Types.StringType.get()));
+    tableCatalog.alterTable(
+        nonClusterIdent, TableChange.addColumn(new String[] {"new_col"}, 
Types.StringType.get()));
+
+    // Flush query_log and verify the executed SQL
+    try (Connection connection =
+            DriverManager.getConnection(
+                clickHouseClusterContainer.getJdbcUrl(TEST_DB_NAME),
+                clickHouseClusterContainer.getUsername(),
+                clickHouseClusterContainer.getPassword());
+        Statement statement = connection.createStatement()) {
+
+      statement.execute("SYSTEM FLUSH LOGS");
+
+      // Verify cluster table ALTER includes ON CLUSTER
+      try (ResultSet rs =
+          statement.executeQuery(
+              String.format(
+                  "SELECT query FROM system.query_log "
+                      + "WHERE type = 'QueryFinish' "
+                      + "AND query_kind = 'Alter' "
+                      + "AND query LIKE '%%`%s`%%' "
+                      + "AND query LIKE '%%ADD COLUMN%%' "
+                      + "ORDER BY event_time DESC LIMIT 1",
+                  clusterTbl))) {
+        Assertions.assertTrue(rs.next(), "Should find ALTER query for cluster 
table");
+        String sql = rs.getString("query");
+        Assertions.assertTrue(
+            sql.contains("ON CLUSTER"),
+            "ALTER TABLE on cluster table must include ON CLUSTER, actual: " + 
sql);
+      }
+
+      // Verify non-cluster table ALTER does NOT include ON CLUSTER
+      try (ResultSet rs =
+          statement.executeQuery(
+              String.format(
+                  "SELECT query FROM system.query_log "
+                      + "WHERE type = 'QueryFinish' "
+                      + "AND query_kind = 'Alter' "
+                      + "AND query LIKE '%%`%s`%%' "
+                      + "AND query LIKE '%%ADD COLUMN%%' "
+                      + "ORDER BY event_time DESC LIMIT 1",
+                  nonClusterTbl))) {
+        Assertions.assertTrue(rs.next(), "Should find ALTER query for 
non-cluster table");
+        String sql = rs.getString("query");
+        Assertions.assertFalse(
+            sql.contains("ON CLUSTER"),
+            "ALTER TABLE on non-cluster table must NOT include ON CLUSTER, 
actual: " + sql);
+      }
+    }
+  }
 }
diff --git 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperationsCluster.java
 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperationsCluster.java
index f231b9d4d3..e20e476a8f 100644
--- 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperationsCluster.java
+++ 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperationsCluster.java
@@ -31,6 +31,8 @@ import 
org.apache.gravitino.catalog.clickhouse.converter.ClickHouseColumnDefault
 import 
org.apache.gravitino.catalog.clickhouse.converter.ClickHouseExceptionConverter;
 import 
org.apache.gravitino.catalog.clickhouse.converter.ClickHouseTypeConverter;
 import org.apache.gravitino.catalog.jdbc.JdbcColumn;
+import org.apache.gravitino.catalog.jdbc.JdbcTable;
+import org.apache.gravitino.rel.TableChange;
 import org.apache.gravitino.rel.expressions.NamedReference;
 import org.apache.gravitino.rel.expressions.distributions.Distribution;
 import org.apache.gravitino.rel.expressions.distributions.Distributions;
@@ -360,6 +362,77 @@ class TestClickHouseTableOperationsCluster {
     Assertions.assertEquals("", stripClusterMetadata(stored));
   }
 
+  /** ALTER TABLE with ON CLUSTER=true should include ON CLUSTER in SQL. */
+  @Test
+  void testAlterTableWithOnCluster() {
+    StubClickHouseTableOperations ops = new StubClickHouseTableOperations();
+    ops.initialize(
+        null,
+        new ClickHouseExceptionConverter(),
+        new ClickHouseTypeConverter(),
+        new ClickHouseColumnDefaultValueConverter(),
+        new HashMap<>());
+    ops.setTable(buildStubTableWithCluster("ck_cluster", true));
+
+    String sql =
+        ops.buildAlterSql(
+            "default",
+            "orders",
+            new TableChange[] {
+              TableChange.addColumn(new String[] {"new_col"}, 
Types.IntegerType.get())
+            });
+
+    Assertions.assertTrue(sql.contains("ON CLUSTER"), "ALTER TABLE should 
include ON CLUSTER");
+    Assertions.assertTrue(sql.contains("`ck_cluster`"), "ALTER TABLE should 
include cluster name");
+  }
+
+  /** ALTER TABLE with ON CLUSTER=false should NOT include ON CLUSTER in SQL. 
*/
+  @Test
+  void testAlterTableWithoutOnCluster() {
+    StubClickHouseTableOperations ops = new StubClickHouseTableOperations();
+    ops.initialize(
+        null,
+        new ClickHouseExceptionConverter(),
+        new ClickHouseTypeConverter(),
+        new ClickHouseColumnDefaultValueConverter(),
+        new HashMap<>());
+    ops.setTable(buildStubTableWithCluster("ck_cluster", false));
+
+    String sql =
+        ops.buildAlterSql(
+            "default",
+            "orders",
+            new TableChange[] {
+              TableChange.addColumn(new String[] {"new_col"}, 
Types.IntegerType.get())
+            });
+
+    Assertions.assertFalse(sql.contains("ON CLUSTER"), "ALTER TABLE should NOT 
include ON CLUSTER");
+  }
+
+  /** ALTER TABLE with null properties should NOT throw NPE. */
+  @Test
+  void testAlterTableWithNullProperties() {
+    StubClickHouseTableOperations ops = new StubClickHouseTableOperations();
+    ops.initialize(
+        null,
+        new ClickHouseExceptionConverter(),
+        new ClickHouseTypeConverter(),
+        new ClickHouseColumnDefaultValueConverter(),
+        new HashMap<>());
+    ops.setTable(buildStubTableWithNullProperties());
+
+    String sql =
+        ops.buildAlterSql(
+            "default",
+            "orders",
+            new TableChange[] {
+              TableChange.addColumn(new String[] {"new_col"}, 
Types.IntegerType.get())
+            });
+
+    Assertions.assertFalse(sql.contains("ON CLUSTER"), "ALTER TABLE should NOT 
include ON CLUSTER");
+    Assertions.assertTrue(sql.contains("ADD COLUMN"), "ALTER TABLE should 
contain ADD COLUMN");
+  }
+
   private static class TestableClickHouseTableOperations extends 
ClickHouseTableOperations {
     String buildCreateSql(
         String tableName,
@@ -378,4 +451,54 @@ class TestClickHouseTableOperationsCluster {
       return generateDropTableSql(tableName, properties);
     }
   }
+
+  private static final class StubClickHouseTableOperations extends 
ClickHouseTableOperations {
+    private JdbcTable table;
+
+    void setTable(JdbcTable table) {
+      this.table = table;
+    }
+
+    @Override
+    protected JdbcTable getOrCreateTable(
+        String databaseName, String tableName, JdbcTable lazyLoadCreateTable) {
+      return table;
+    }
+
+    String buildAlterSql(String db, String tableName, TableChange[] changes) {
+      return generateAlterTableSql(db, tableName, changes);
+    }
+  }
+
+  private static JdbcTable buildStubTableWithCluster(String clusterName, 
boolean onCluster) {
+    JdbcColumn c1 =
+        JdbcColumn.builder()
+            .withName("id")
+            .withType(Types.IntegerType.get())
+            .withNullable(false)
+            .build();
+    Map<String, String> props = new HashMap<>();
+    props.put(ClusterConstants.CLUSTER_NAME, clusterName);
+    props.put(ClusterConstants.ON_CLUSTER, String.valueOf(onCluster));
+    return JdbcTable.builder()
+        .withName("orders")
+        .withColumns(new JdbcColumn[] {c1})
+        .withProperties(props)
+        .withTableOperation(null)
+        .build();
+  }
+
+  private static JdbcTable buildStubTableWithNullProperties() {
+    JdbcColumn c1 =
+        JdbcColumn.builder()
+            .withName("id")
+            .withType(Types.IntegerType.get())
+            .withNullable(false)
+            .build();
+    return JdbcTable.builder()
+        .withName("orders")
+        .withColumns(new JdbcColumn[] {c1})
+        .withTableOperation(null)
+        .build();
+  }
 }

Reply via email to