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 4091d5c56b [#12272] feat(clickhouse): map ClickHouse Date32 to 
Gravitino DateType and verify Enum round-trip (#12275)
4091d5c56b is described below

commit 4091d5c56b217cc03516e69eee19780584631d64
Author: StormSpirit <[email protected]>
AuthorDate: Fri Aug 21 15:38:08 2026 +0800

    [#12272] feat(clickhouse): map ClickHouse Date32 to Gravitino DateType and 
verify Enum round-trip (#12275)
    
    ### What changes were proposed in this pull request?
    
    1. **Date32 → DateType**: Add `case DATE32` in
    `ClickHouseTypeConverter.toGravitino()`, mapping ClickHouse Date32 to
    Gravitino `Types.DateType`. Previously Date32 fell through to the
    default branch and was mapped to `ExternalType`, despite being a
    calendar date type semantically identical to Date (just with a wider
    year range: 1900–2299 vs 1970–2149).
    
    2. **Enum IT test**: Add `testEnumRoundTrip()` integration test
    verifying that Enum8/Enum16 round-trip correctly through the Gravitino
    type system via ExternalType. The enum definitions (including spaces and
    equals signs) are fully preserved across create → load → recreate
    cycles.
    
    ### Why are the changes needed?
    
    Date32 was the last user-visible ClickHouse type that has a Gravitino
    native counterpart (`Types.DateType`) but was not mapped to it. The
    `DateTime64(3)` → `TimestampType.withoutTimeZone(3)` → `DateTime(0)`
    pattern already establishes the precedent of one-way precision/range
    narrowing in the ClickHouse converter — Date32 follows the same pattern.
    
    Mapping Date32 to `DateType` (instead of `ExternalType`) also keeps the
    column usable in downstream systems: the Gravitino Spark connector
    currently throws `UnsupportedOperationException` for `ExternalType`
    columns, making the entire table inaccessible (see #12104), and the
    proposed fix there maps them to `StringType`, losing the date semantics
    entirely. A native `DateType` mapping preserves the date semantics
    across Spark/Flink/Trino integrations.
    
    Enum8/Enum16 already work correctly via ExternalType pass-through, but
    had no integration test coverage, leaving a gap in verifying their
    round-trip correctness. The test confirms no code change is needed for
    Enum support.
    
    Fix: #12272
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, with a caveat. After this change:
    - **Read path**: ClickHouse `Date32` columns are now loaded as Gravitino
    `DateType` (instead of `ExternalType("Date32")`)
    - **Write path**: `DateType` is still mapped to ClickHouse `Date` (not
    `Date32`), so round-tripping a Date32 table through Gravitino will
    produce `Date` on the ClickHouse side, narrowing the supported range
    from 1900–2299 to 1970–2149-06-06
    
    ⚠️ **Silent saturation of out-of-range values** (verified against
    ClickHouse 25.3.2.39): ClickHouse does not reject dates outside the
    `Date` range — it silently saturates them. Writing `1900-01-01` to a
    `Date` column stores `1970-01-01`, and writing `2150-01-01` stores
    `2149-06-06`. This only affects values in 1900–1969 or after 2149-06-06,
    which are rare in practice.
    
    Users who need the full 1900–2299 range should use
    `ExternalType("Date32")` explicitly when creating tables — the
    ExternalType pass-through path remains fully available.
    
    This is consistent with the existing `DateTime64(3)` → `DateTime(0)`
    precision narrowing pattern.
    
    ### How was this patch tested?
    
    - Unit tests: `TestClickHouseTypeConverter` — verified Date32 now maps
    to `DateType.get()` (previously `ExternalType.of("Date32")`)
    - Docker integration tests: `CatalogClickHouseIT.testEnumRoundTrip` —
    verified Enum8/Enum16 round-trip correctness (create → load → verify →
    recreate → SHOW CREATE TABLE) against ClickHouse 25.3.2.39
    - Static checks: `spotlessCheck`, `rat` (license headers),
    `gravitino-pr-precheck.py` — all passed
    
    ---------
    
    Signed-off-by: jiangxt2 <[email protected]>
---
 .../converter/ClickHouseTypeConverter.java         |   2 +-
 .../integration/test/CatalogClickHouseIT.java      | 125 +++++++++++++++++++++
 2 files changed, 126 insertions(+), 1 deletion(-)

diff --git 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/converter/ClickHouseTypeConverter.java
 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/converter/ClickHouseTypeConverter.java
index 03f184b1f7..cc4d9c8adf 100644
--- 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/converter/ClickHouseTypeConverter.java
+++ 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/converter/ClickHouseTypeConverter.java
@@ -162,7 +162,7 @@ public class ClickHouseTypeConverter extends 
JdbcTypeConverter {
       case DATE:
         return Types.DateType.get();
       case DATE32:
-        // Date32 supports 1900-2299 vs Date's 1970-2149. Use ExternalType to 
preserve round-trip.
+        // Date32 has a wider range than Date; preserve its catalog type for 
round-trip.
         return Types.ExternalType.of(DATE32);
       case DATETIME:
         // Default is 0 precision
diff --git 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseIT.java
 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseIT.java
index 5ffa5344c4..71d9bc9203 100644
--- 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseIT.java
+++ 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseIT.java
@@ -78,6 +78,7 @@ import 
org.apache.gravitino.rel.expressions.transforms.Transforms;
 import org.apache.gravitino.rel.indexes.Index;
 import org.apache.gravitino.rel.indexes.Indexes;
 import org.apache.gravitino.rel.types.Decimal;
+import org.apache.gravitino.rel.types.Type;
 import org.apache.gravitino.rel.types.Types;
 import org.apache.gravitino.utils.RandomNameUtils;
 import org.junit.jupiter.api.AfterAll;
@@ -271,6 +272,50 @@ public class CatalogClickHouseIT extends BaseIT {
     return properties;
   }
 
+  private static String normalizeEnumFormatting(String value) {
+    StringBuilder normalized = new StringBuilder(value.length());
+    boolean inSingleQuote = false;
+    boolean pendingWhitespace = false;
+    for (int i = 0; i < value.length(); i++) {
+      char current = value.charAt(i);
+      boolean escapedQuote =
+          current == '\''
+              && i > 0
+              && value.charAt(i - 1) == '\\'
+              && (i < 2 || value.charAt(i - 2) != '\\');
+      if (current == '\'' && !escapedQuote) {
+        if (pendingWhitespace) {
+          appendWhitespaceUnlessSeparator(normalized);
+          pendingWhitespace = false;
+        }
+        normalized.append(current);
+        inSingleQuote = !inSingleQuote;
+      } else if (!inSingleQuote && Character.isWhitespace(current)) {
+        pendingWhitespace = true;
+      } else if (!inSingleQuote && (current == '=' || current == ',')) {
+        pendingWhitespace = false;
+        normalized.append(current);
+      } else {
+        if (pendingWhitespace) {
+          appendWhitespaceUnlessSeparator(normalized);
+          pendingWhitespace = false;
+        }
+        normalized.append(current);
+      }
+    }
+    return normalized.toString().trim();
+  }
+
+  private static void appendWhitespaceUnlessSeparator(StringBuilder builder) {
+    if (builder.length() == 0) {
+      return;
+    }
+    char previous = builder.charAt(builder.length() - 1);
+    if (previous != '=' && previous != ',') {
+      builder.append(' ');
+    }
+  }
+
   @Test
   void testOperationClickhouseSchema() {
     SupportsSchemas schemas = catalog.asSchemas();
@@ -2836,6 +2881,7 @@ public class CatalogClickHouseIT extends BaseIT {
     // may normalize the enum definition format (e.g., spacing around '=' and 
',').
     Assertions.assertTrue(loadedTable.columns()[4].dataType() instanceof 
Types.ExternalType);
     Assertions.assertTrue(loadedTable.columns()[5].dataType() instanceof 
Types.ExternalType);
+    // Date32 remains ExternalType so its wider range and catalog type survive 
round-trip.
     Assertions.assertEquals(Types.ExternalType.of("Date32"), 
loadedTable.columns()[6].dataType());
   }
 
@@ -2872,4 +2918,83 @@ public class CatalogClickHouseIT extends BaseIT {
                     getSortOrders("id"),
                     Indexes.EMPTY_INDEXES));
   }
+
+  @Test
+  void testEnumRoundTrip() {
+    // Create a table in ClickHouse with Enum8 and Enum16 columns
+    String tableName = GravitinoITUtils.genRandomName("enum_test");
+    clickhouseService.executeQuery(
+        String.format(
+            "CREATE TABLE `%s`.`%s` ("
+                + "id Int32, "
+                + "status Enum8('active' = 1, 'inactive' = 2), "
+                + "priority Enum16('low' = 100, 'medium' = 200, 'high' = 300)"
+                + ") ENGINE = MergeTree ORDER BY id",
+            schemaName, tableName));
+
+    // Load through Gravitino and verify column types are ExternalType
+    Table loaded = 
catalog.asTableCatalog().loadTable(NameIdentifier.of(schemaName, tableName));
+    Column[] columns = loaded.columns();
+    Assertions.assertEquals(3, columns.length);
+
+    // Enum8 type verification
+    Type enum8Type = columns[1].dataType();
+    Assertions.assertTrue(
+        enum8Type instanceof Types.ExternalType,
+        "Enum8 should map to ExternalType, but got: " + 
enum8Type.simpleString());
+    Assertions.assertEquals(
+        normalizeEnumFormatting("Enum8('active' = 1, 'inactive' = 2)"),
+        normalizeEnumFormatting(((Types.ExternalType) 
enum8Type).catalogString()));
+
+    // Enum16 type verification
+    Type enum16Type = columns[2].dataType();
+    Assertions.assertTrue(
+        enum16Type instanceof Types.ExternalType,
+        "Enum16 should map to ExternalType, but got: " + 
enum16Type.simpleString());
+    Assertions.assertEquals(
+        normalizeEnumFormatting("Enum16('low' = 100, 'medium' = 200, 'high' = 
300)"),
+        normalizeEnumFormatting(((Types.ExternalType) 
enum16Type).catalogString()));
+
+    // Round-trip: recreate table through Gravitino with the loaded schema
+    String rtTableName = GravitinoITUtils.genRandomName("enum_rt");
+    catalog
+        .asTableCatalog()
+        .createTable(
+            NameIdentifier.of(schemaName, rtTableName),
+            columns,
+            "enum round-trip test",
+            createProperties(),
+            Transforms.EMPTY_TRANSFORM,
+            Distributions.NONE,
+            new SortOrder[] {SortOrders.of(NamedReference.field("id"), 
SortDirection.ASCENDING)});
+
+    // Verify types survived round-trip
+    Table rtLoaded = 
catalog.asTableCatalog().loadTable(NameIdentifier.of(schemaName, rtTableName));
+    Column[] rtColumns = rtLoaded.columns();
+    Type rtEnum8 = rtColumns[1].dataType();
+    Type rtEnum16 = rtColumns[2].dataType();
+    Assertions.assertTrue(
+        rtEnum8 instanceof Types.ExternalType, "Enum8 should survive 
round-trip as ExternalType");
+    Assertions.assertTrue(
+        rtEnum16 instanceof Types.ExternalType, "Enum16 should survive 
round-trip as ExternalType");
+    Assertions.assertEquals(
+        normalizeEnumFormatting(((Types.ExternalType) 
enum8Type).catalogString()),
+        normalizeEnumFormatting(((Types.ExternalType) 
rtEnum8).catalogString()));
+    Assertions.assertEquals(
+        normalizeEnumFormatting(((Types.ExternalType) 
enum16Type).catalogString()),
+        normalizeEnumFormatting(((Types.ExternalType) 
rtEnum16).catalogString()));
+
+    // Verify DDL on ClickHouse side contains full enum definitions
+    String createSql =
+        clickhouseService.executeQueryForResult(
+            String.format("SHOW CREATE TABLE `%s`.`%s`", schemaName, 
rtTableName));
+    Assertions.assertNotNull(createSql, "SHOW CREATE TABLE should return a 
result");
+    String normalizedCreateSql = normalizeEnumFormatting(createSql);
+    Assertions.assertTrue(
+        normalizedCreateSql.contains("Enum8('active'=1,'inactive'=2)"),
+        "SHOW CREATE TABLE should contain Enum8 definition: " + createSql);
+    Assertions.assertTrue(
+        
normalizedCreateSql.contains("Enum16('low'=100,'medium'=200,'high'=300)"),
+        "SHOW CREATE TABLE should contain Enum16 definition: " + createSql);
+  }
 }

Reply via email to