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 409d1c4013 [#13943] fix(oceanbase): Escape table and column comments 
(#13944)
409d1c4013 is described below

commit 409d1c4013306c091c440e9bd3603c73d8b4788e
Author: Qi Yu <[email protected]>
AuthorDate: Mon Oct 19 22:05:59 2026 +0800

    [#13943] fix(oceanbase): Escape table and column comments (#13944)
    
    ### What changes were proposed in this pull request?
    
    Use the existing `escapeSqlLiteral` helper for all four OceanBase
    comment SQL-generation paths: create table, update table comment, add
    column, and create/modify column definitions.
    
    Add SQL-generation regression tests and real OceanBase round-trip tests,
    including changes that rebuild a column definition while preserving its
    existing comment.
    
    ### Why are the changes needed?
    
    Unescaped apostrophes break comment DDL, and backslash escapes can
    change the stored text. Apply the same literal escaping already used by
    the MySQL catalog.
    
    This follows the existing MySQL-compatible literal semantics with
    backslash escaping enabled. SQL-mode-aware escaping for
    `NO_BACKSLASH_ESCAPES` is outside this patch.
    
    Fix: #13943
    
    ### Does this PR introduce _any_ user-facing change?
    
    OceanBase table and column comments containing quotes or backslashes can
    be created and altered without corrupting their text. No API or
    configuration changes.
    
    ### How was this patch tested?
    
    - OceanBase catalog non-Docker unit tests: 25 passed, including 16 new
    SQL-generation cases.
    - OceanBase catalog tests with Docker enabled: 43 passed, including two
    real comment round-trip cases on OceanBase CE 4.2.1.10.
    - Actual SQL mode:
    `STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_AUTO_CREATE_USER`.
    - Mutation check: reverting the production fix makes 12 of the 16 new
    SQL-generation cases fail; the four cases requiring no escaping still
    pass.
    - OceanBase catalog Spotless checks.
---
 .../operation/OceanBaseTableOperations.java        | 12 ++-
 .../operation/TestOceanBaseTableOperations.java    | 54 ++++++++++++
 .../TestOceanBaseTableOperationsSqlGeneration.java | 96 ++++++++++++++++++++++
 3 files changed, 158 insertions(+), 4 deletions(-)

diff --git 
a/catalogs-contrib/catalog-jdbc-oceanbase/src/main/java/org/apache/gravitino/catalog/oceanbase/operation/OceanBaseTableOperations.java
 
b/catalogs-contrib/catalog-jdbc-oceanbase/src/main/java/org/apache/gravitino/catalog/oceanbase/operation/OceanBaseTableOperations.java
index 312e6d7a1e..687cf5f19d 100644
--- 
a/catalogs-contrib/catalog-jdbc-oceanbase/src/main/java/org/apache/gravitino/catalog/oceanbase/operation/OceanBaseTableOperations.java
+++ 
b/catalogs-contrib/catalog-jdbc-oceanbase/src/main/java/org/apache/gravitino/catalog/oceanbase/operation/OceanBaseTableOperations.java
@@ -18,6 +18,7 @@
  */
 package org.apache.gravitino.catalog.oceanbase.operation;
 
+import static 
org.apache.gravitino.catalog.jdbc.utils.JdbcConnectorUtils.escapeSqlLiteral;
 import static org.apache.gravitino.rel.Column.DEFAULT_VALUE_NOT_SET;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -108,7 +109,7 @@ public class OceanBaseTableOperations extends 
JdbcTableOperations {
 
     // Add table comment if specified
     if (StringUtils.isNotEmpty(comment)) {
-      sqlBuilder.append(" COMMENT='").append(comment).append("'");
+      sqlBuilder.append(" COMMENT='").append(escapeSqlLiteral(comment, 
'\'')).append("'");
     }
 
     // Add table properties
@@ -314,7 +315,7 @@ public class OceanBaseTableOperations extends 
JdbcTableOperations {
           newComment = StringIdentifier.addToComment(identifier, newComment);
         }
       }
-      alterSql.add("COMMENT '" + newComment + "'");
+      alterSql.add("COMMENT '" + escapeSqlLiteral(newComment, '\'') + "'");
     }
 
     if (!setProperties.isEmpty()) {
@@ -479,7 +480,10 @@ public class OceanBaseTableOperations extends 
JdbcTableOperations {
     }
     // Append comment if available
     if (StringUtils.isNotEmpty(addColumn.getComment())) {
-      columnDefinition.append("COMMENT 
'").append(addColumn.getComment()).append("' ");
+      columnDefinition
+          .append("COMMENT '")
+          .append(escapeSqlLiteral(addColumn.getComment(), '\''))
+          .append("' ");
     }
 
     // Append default value if available
@@ -650,7 +654,7 @@ public class OceanBaseTableOperations extends 
JdbcTableOperations {
 
     // Add column comment if specified
     if (StringUtils.isNotEmpty(column.comment())) {
-      sqlBuilder.append("COMMENT '").append(column.comment()).append("' ");
+      sqlBuilder.append("COMMENT '").append(escapeSqlLiteral(column.comment(), 
'\'')).append("' ");
     }
     return sqlBuilder;
   }
diff --git 
a/catalogs-contrib/catalog-jdbc-oceanbase/src/test/java/org/apache/gravitino/catalog/oceanbase/operation/TestOceanBaseTableOperations.java
 
b/catalogs-contrib/catalog-jdbc-oceanbase/src/test/java/org/apache/gravitino/catalog/oceanbase/operation/TestOceanBaseTableOperations.java
index 2a063d8a94..06fc9101ad 100644
--- 
a/catalogs-contrib/catalog-jdbc-oceanbase/src/test/java/org/apache/gravitino/catalog/oceanbase/operation/TestOceanBaseTableOperations.java
+++ 
b/catalogs-contrib/catalog-jdbc-oceanbase/src/test/java/org/apache/gravitino/catalog/oceanbase/operation/TestOceanBaseTableOperations.java
@@ -45,6 +45,8 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 @Tag("gravitino-docker-test")
 public class TestOceanBaseTableOperations extends TestOceanBase {
@@ -56,6 +58,58 @@ public class TestOceanBaseTableOperations extends 
TestOceanBase {
     DATABASE_OPERATIONS.create(TEST_DB_NAME, null, new HashMap<>());
   }
 
+  @ParameterizedTest
+  @ValueSource(strings = {"customer's id", "quote\\' and \\new\\path\\; 
\"double\"\n中文"})
+  void testCommentRoundTrips(String comment) {
+    String tableName = RandomNameUtils.genRandomName("comment_round_trip");
+    JdbcColumn column =
+        JdbcColumn.builder()
+            .withName("col1")
+            .withType(VARCHAR)
+            .withNullable(true)
+            .withComment(comment)
+            .build();
+    try {
+      TABLE_OPERATIONS.create(
+          TEST_DB_NAME,
+          tableName,
+          new JdbcColumn[] {column},
+          comment,
+          Collections.emptyMap(),
+          Transforms.EMPTY_TRANSFORM,
+          Distributions.NONE,
+          Indexes.EMPTY_INDEXES);
+      JdbcTable table = TABLE_OPERATIONS.load(TEST_DB_NAME, tableName);
+      Assertions.assertEquals(comment, table.comment());
+      Assertions.assertEquals(comment, table.columns()[0].comment());
+
+      String updated = comment + " updated";
+      TABLE_OPERATIONS.alterTable(TEST_DB_NAME, tableName, 
TableChange.updateComment(updated));
+      Assertions.assertEquals(updated, TABLE_OPERATIONS.load(TEST_DB_NAME, 
tableName).comment());
+
+      TABLE_OPERATIONS.alterTable(
+          TEST_DB_NAME, tableName, TableChange.addColumn(new String[] 
{"col2"}, VARCHAR, comment));
+      table = TABLE_OPERATIONS.load(TEST_DB_NAME, tableName);
+      Assertions.assertEquals(comment, table.columns()[1].comment());
+
+      TABLE_OPERATIONS.alterTable(
+          TEST_DB_NAME, tableName, TableChange.updateColumnComment(new 
String[] {"col1"}, updated));
+      table = TABLE_OPERATIONS.load(TEST_DB_NAME, tableName);
+      Assertions.assertEquals(updated, table.columns()[0].comment());
+
+      // Rebuilding a column definition must also preserve an existing escaped 
comment.
+      TABLE_OPERATIONS.alterTable(
+          TEST_DB_NAME,
+          tableName,
+          TableChange.updateColumnType(new String[] {"col1"}, 
Types.VarCharType.of(512)));
+      table = TABLE_OPERATIONS.load(TEST_DB_NAME, tableName);
+      Assertions.assertEquals(updated, table.columns()[0].comment());
+      Assertions.assertEquals(updated, table.comment());
+    } finally {
+      TABLE_OPERATIONS.drop(TEST_DB_NAME, tableName);
+    }
+  }
+
   @Test
   public void testOperationTable() {
     String tableName = RandomStringUtils.randomAlphabetic(16).toLowerCase() + 
"_op_table";
diff --git 
a/catalogs-contrib/catalog-jdbc-oceanbase/src/test/java/org/apache/gravitino/catalog/oceanbase/operation/TestOceanBaseTableOperationsSqlGeneration.java
 
b/catalogs-contrib/catalog-jdbc-oceanbase/src/test/java/org/apache/gravitino/catalog/oceanbase/operation/TestOceanBaseTableOperationsSqlGeneration.java
index 74ea40b2d5..612c034022 100644
--- 
a/catalogs-contrib/catalog-jdbc-oceanbase/src/test/java/org/apache/gravitino/catalog/oceanbase/operation/TestOceanBaseTableOperationsSqlGeneration.java
+++ 
b/catalogs-contrib/catalog-jdbc-oceanbase/src/test/java/org/apache/gravitino/catalog/oceanbase/operation/TestOceanBaseTableOperationsSqlGeneration.java
@@ -20,11 +20,14 @@ package org.apache.gravitino.catalog.oceanbase.operation;
 
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Stream;
 import org.apache.gravitino.catalog.jdbc.JdbcColumn;
+import org.apache.gravitino.catalog.jdbc.JdbcTable;
 import 
org.apache.gravitino.catalog.jdbc.converter.JdbcColumnDefaultValueConverter;
 import org.apache.gravitino.catalog.jdbc.converter.JdbcExceptionConverter;
 import org.apache.gravitino.catalog.jdbc.utils.ConnectionCountingDataSource;
 import org.apache.gravitino.catalog.oceanbase.converter.OceanBaseTypeConverter;
+import org.apache.gravitino.rel.TableChange;
 import org.apache.gravitino.rel.expressions.distributions.Distributions;
 import org.apache.gravitino.rel.expressions.literals.Literals;
 import org.apache.gravitino.rel.expressions.transforms.Transforms;
@@ -32,6 +35,9 @@ import org.apache.gravitino.rel.indexes.Indexes;
 import org.apache.gravitino.rel.types.Types;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
 public class TestOceanBaseTableOperationsSqlGeneration {
 
@@ -59,6 +65,61 @@ public class TestOceanBaseTableOperationsSqlGeneration {
     }
   }
 
+  @ParameterizedTest
+  @MethodSource("comments")
+  void testCreateEscapesTableAndColumnComments(String comment, String escaped) 
{
+    TestableOceanBaseTableOperations ops = new 
TestableOceanBaseTableOperations();
+    String sql =
+        ops.generateCreateTableSql(
+            "test_table",
+            new JdbcColumn[] {column(comment)},
+            comment,
+            Collections.emptyMap(),
+            Transforms.EMPTY_TRANSFORM,
+            Distributions.NONE,
+            Indexes.EMPTY_INDEXES);
+
+    Assertions.assertTrue(sql.contains("COMMENT '" + escaped + "' "), sql);
+    Assertions.assertTrue(sql.endsWith("COMMENT='" + escaped + "';"), sql);
+  }
+
+  @ParameterizedTest
+  @MethodSource("comments")
+  void testUpdateTableCommentEscapesLiteral(String comment, String escaped) {
+    TestableOceanBaseTableOperations ops = operationsWithTable();
+    String sql =
+        ops.generateAlterTableSql("database", "test_table", 
TableChange.updateComment(comment));
+
+    Assertions.assertEquals("ALTER TABLE `test_table`\nCOMMENT '" + escaped + 
"';", sql);
+  }
+
+  @ParameterizedTest
+  @MethodSource("comments")
+  void testAddColumnEscapesComment(String comment, String escaped) {
+    TestableOceanBaseTableOperations ops = operationsWithTable();
+    String sql =
+        ops.generateAlterTableSql(
+            "database",
+            "test_table",
+            TableChange.addColumn(new String[] {"new_col"}, 
Types.IntegerType.get(), comment));
+
+    Assertions.assertTrue(sql.contains("COMMENT '" + escaped + "' "), sql);
+  }
+
+  @ParameterizedTest
+  @MethodSource("comments")
+  void testModifyColumnEscapesComment(String comment, String escaped) {
+    TestableOceanBaseTableOperations ops = operationsWithTable();
+    String sql =
+        ops.generateAlterTableSql(
+            "database",
+            "test_table",
+            TableChange.updateColumnComment(new String[] {"col1"}, comment));
+
+    Assertions.assertTrue(sql.contains("MODIFY COLUMN `col1`"), sql);
+    Assertions.assertTrue(sql.contains("COMMENT '" + escaped + "' "), sql);
+  }
+
   @Test
   void testPurgeIsRejectedWithoutExecutingSql() {
     ConnectionCountingDataSource connections = new 
ConnectionCountingDataSource();
@@ -123,4 +184,39 @@ public class TestOceanBaseTableOperationsSqlGeneration {
         sql.contains("DEFAULT " + 
converter.fromGravitino(col1.defaultValue())),
         "Should contain DEFAULT value but was: " + sql);
   }
+
+  private static JdbcColumn column(String comment) {
+    return JdbcColumn.builder()
+        .withName("col1")
+        .withType(Types.IntegerType.get())
+        .withNullable(true)
+        .withComment(comment)
+        .build();
+  }
+
+  private static TestableOceanBaseTableOperations operationsWithTable() {
+    JdbcTable table =
+        JdbcTable.builder()
+            .withName("test_table")
+            .withComment("old comment")
+            .withColumns(new JdbcColumn[] {column("old comment")})
+            .build();
+    return new TestableOceanBaseTableOperations() {
+      /** Returns the table fixture without accessing a database. */
+      @Override
+      public JdbcTable load(String databaseName, String tableName) {
+        Assertions.assertEquals("database", databaseName);
+        Assertions.assertEquals("test_table", tableName);
+        return table;
+      }
+    };
+  }
+
+  private static Stream<Arguments> comments() {
+    return Stream.of(
+        Arguments.of("customer's id", "customer''s id"),
+        Arguments.of("C:\\new\\trailing\\", "C:\\\\new\\\\trailing\\\\"),
+        Arguments.of("backslash\\'quote; --", "backslash\\\\''quote; --"),
+        Arguments.of("line1\n中文 \"double\"", "line1\n中文 \"double\""));
+  }
 }

Reply via email to