This is an automated email from the ASF dual-hosted git repository.

diqiu50 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 3b6fe775a6 [#13040] fix(postgresql): Report unconstrained NUMERIC as 
an external type (#13042)
3b6fe775a6 is described below

commit 3b6fe775a65ffa486862c50e8d32877afd47ae83
Author: Yuhui <[email protected]>
AuthorDate: Thu Sep 10 09:02:27 2026 +0800

    [#13040] fix(postgresql): Report unconstrained NUMERIC as an external type 
(#13042)
    
    ### What changes were proposed in this pull request?
    
    Report a PostgreSQL `NUMERIC` without precision and scale as the
    external type `numeric`, and report array elements as nullable. Read
    external types as string in the Trino, Spark and Flink connectors.
    
    ### Why are the changes needed?
    
    An unconstrained `NUMERIC` holds values whose precision and scale vary
    per row, so `Decimal(38, 18)` claims bounds the source never declared.
    PostgreSQL array elements always accept NULL and cannot be declared
    otherwise.
    
    Once a common column type maps to an external type, a column carrying
    one reaches the connectors, where an unmapped type fails the type
    conversion of the whole table.
    
    Fix: #13040
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. An unconstrained `NUMERIC` is reported as the external type
    `numeric`, an array as a list with nullable elements, and a connector
    reads an external type as a string. A `numeric(p, s)[]` column becomes a
    list of the external type `numeric`, since array element metadata is not
    carried through.
    
    ### How was this patch tested?
    
    Unit tests in the PostgreSQL catalog and the three connectors, plus a
    PostgreSQL integration test against a real container.
    
    Co-authored-by: Claude Opus 5 <[email protected]>
---
 .../converter/PostgreSqlTypeConverter.java         | 18 ++++++-------
 .../converter/TestPostgreSqlTypeConverter.java     | 28 +++++++-------------
 .../integration/test/CatalogPostgreSqlIT.java      | 20 ++++++++++++++-
 .../operation/TestPostgreSqlTableOperations.java   |  2 +-
 docs/jdbc-postgresql-catalog.md                    | 10 ++++++++
 .../gravitino/flink/connector/utils/TypeUtils.java | 30 ++++++++++++++++++----
 .../flink/connector/utils/TestTypeUtils.java       | 27 +++++++++++++++++++
 .../connector/jdbc/SparkJdbcTypeConverter.java     | 12 +++++++++
 .../connector/jdbc/TestSparkJdbcTypeConverter.java |  8 ++++++
 .../postgresql/PostgreSQLDataTypeTransformer.java  | 15 ++++++++---
 .../TestPostgreSQLDataTypeTransformer.java         | 11 ++++++++
 11 files changed, 143 insertions(+), 38 deletions(-)

diff --git 
a/catalogs/catalog-jdbc-postgresql/src/main/java/org/apache/gravitino/catalog/postgresql/converter/PostgreSqlTypeConverter.java
 
b/catalogs/catalog-jdbc-postgresql/src/main/java/org/apache/gravitino/catalog/postgresql/converter/PostgreSqlTypeConverter.java
index f5161b12f7..c87a63f86c 100644
--- 
a/catalogs/catalog-jdbc-postgresql/src/main/java/org/apache/gravitino/catalog/postgresql/converter/PostgreSqlTypeConverter.java
+++ 
b/catalogs/catalog-jdbc-postgresql/src/main/java/org/apache/gravitino/catalog/postgresql/converter/PostgreSqlTypeConverter.java
@@ -42,8 +42,6 @@ public class PostgreSqlTypeConverter extends 
JdbcTypeConverter {
   static final String UUID = "uuid";
   @VisibleForTesting static final String JDBC_ARRAY_PREFIX = "_";
   @VisibleForTesting static final String ARRAY_TOKEN = "[]";
-  @VisibleForTesting static final int DEFAULT_NUMERIC_PRECISION = 38;
-  @VisibleForTesting static final int DEFAULT_NUMERIC_SCALE = 18;
 
   @Override
   public Type toGravitino(JdbcTypeBean typeBean) {
@@ -81,11 +79,12 @@ public class PostgreSqlTypeConverter extends 
JdbcTypeConverter {
       case NUMERIC:
         Integer columnSize = typeBean.getColumnSize();
         Integer scale = typeBean.getScale();
+        // An unconstrained NUMERIC accepts values whose precision and scale 
vary per row, up to
+        // 131072 digits before and 16383 after the decimal point, which no 
DecimalType can
+        // describe, so it is reported as an external type. The driver reports 
column size 0 for it,
+        // null is handled defensively.
         if (columnSize == null || columnSize == 0) {
-          // PostgreSQL unconstrained NUMERIC has no fixed precision/scale. 
Gravitino DecimalType
-          // cannot represent that exactly, so use the maximum supported 
decimal as a compatibility
-          // tradeoff for engines and clients that cannot consume ExternalType.
-          return Types.DecimalType.of(DEFAULT_NUMERIC_PRECISION, 
DEFAULT_NUMERIC_SCALE);
+          return Types.ExternalType.of(NUMERIC);
         }
         return Types.DecimalType.of(columnSize, scale == null ? 0 : scale);
       case VARCHAR:
@@ -162,10 +161,11 @@ public class PostgreSqlTypeConverter extends 
JdbcTypeConverter {
   // the array size or number of dimensions in CREATE TABLE is simply 
documentation; it does not
   // affect run-time behavior.
   // https://www.postgresql.org/docs/current/arrays.html#ARRAYS-DECLARATION
+  // PostgreSQL array elements always accept NULL and cannot be declared 
otherwise, so
+  // elementNullable is ignored: a list declared with non-nullable elements is 
created as an
+  // ordinary array whose elements accept NULL.
   private String fromGravitinoArrayType(ListType listType) {
     Type elementType = listType.elementType();
-    Preconditions.checkArgument(
-        !listType.elementNullable(), "PostgreSQL doesn't support element to 
nullable");
     Preconditions.checkArgument(
         !(elementType instanceof ListType),
         "PostgreSQL doesn't support multidimensional list internally, please 
use one dimensional list");
@@ -176,6 +176,6 @@ public class PostgreSqlTypeConverter extends 
JdbcTypeConverter {
   private ListType toGravitinoArrayType(String typeName) {
     String elementTypeName = typeName.substring(JDBC_ARRAY_PREFIX.length(), 
typeName.length());
     JdbcTypeBean bean = new JdbcTypeBean(elementTypeName);
-    return ListType.of(toGravitino(bean), false);
+    return ListType.nullable(toGravitino(bean));
   }
 }
diff --git 
a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/converter/TestPostgreSqlTypeConverter.java
 
b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/converter/TestPostgreSqlTypeConverter.java
index e4139441b7..23a65e19a0 100644
--- 
a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/converter/TestPostgreSqlTypeConverter.java
+++ 
b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/converter/TestPostgreSqlTypeConverter.java
@@ -27,8 +27,6 @@ import static 
org.apache.gravitino.catalog.postgresql.converter.PostgreSqlTypeCo
 import static 
org.apache.gravitino.catalog.postgresql.converter.PostgreSqlTypeConverter.BOOL;
 import static 
org.apache.gravitino.catalog.postgresql.converter.PostgreSqlTypeConverter.BPCHAR;
 import static 
org.apache.gravitino.catalog.postgresql.converter.PostgreSqlTypeConverter.BYTEA;
-import static 
org.apache.gravitino.catalog.postgresql.converter.PostgreSqlTypeConverter.DEFAULT_NUMERIC_PRECISION;
-import static 
org.apache.gravitino.catalog.postgresql.converter.PostgreSqlTypeConverter.DEFAULT_NUMERIC_SCALE;
 import static 
org.apache.gravitino.catalog.postgresql.converter.PostgreSqlTypeConverter.FLOAT_4;
 import static 
org.apache.gravitino.catalog.postgresql.converter.PostgreSqlTypeConverter.FLOAT_8;
 import static 
org.apache.gravitino.catalog.postgresql.converter.PostgreSqlTypeConverter.INT_2;
@@ -71,16 +69,10 @@ public class TestPostgreSqlTypeConverter {
     checkJdbcTypeToGravitinoType(Types.TimestampType.withoutTimeZone(3), 
TIMESTAMP, 23, null, 3);
     checkJdbcTypeToGravitinoType(Types.TimestampType.withoutTimeZone(6), 
TIMESTAMP, 26, null, 6);
     checkJdbcTypeToGravitinoType(Types.DecimalType.of(10, 2), NUMERIC, 10, 2, 
0);
-    // Unconstrained NUMERIC (no precision) returns columnSize=0 from JDBC 
metadata;
-    // mapped to Gravitino's maximum supported decimal as a compatibility 
tradeoff.
-    checkJdbcTypeToGravitinoType(
-        Types.DecimalType.of(DEFAULT_NUMERIC_PRECISION, 
DEFAULT_NUMERIC_SCALE), NUMERIC, 0, 0, 0);
-    checkJdbcTypeToGravitinoType(
-        Types.DecimalType.of(DEFAULT_NUMERIC_PRECISION, DEFAULT_NUMERIC_SCALE),
-        NUMERIC,
-        null,
-        null,
-        0);
+    // Unconstrained NUMERIC (no precision) returns columnSize=0 from JDBC 
metadata; its precision
+    // and scale vary per row, so it is reported as an external type instead 
of a decimal.
+    checkJdbcTypeToGravitinoType(Types.ExternalType.of(NUMERIC), NUMERIC, 0, 
0, 0);
+    checkJdbcTypeToGravitinoType(Types.ExternalType.of(NUMERIC), NUMERIC, 
null, null, 0);
     checkJdbcTypeToGravitinoType(Types.DecimalType.of(9, 0), NUMERIC, 9, 0, 0);
     checkJdbcTypeToGravitinoType(Types.DecimalType.of(18, 0), NUMERIC, 18, 0, 
0);
     checkJdbcTypeToGravitinoType(Types.DecimalType.of(20, 0), NUMERIC, 20, 0, 
0);
@@ -96,19 +88,17 @@ public class TestPostgreSqlTypeConverter {
   @Test
   public void testArrayType() {
     Type elmentType = Types.IntegerType.get();
-    Type list1 = Types.ListType.of(elmentType, false);
+    Type list1 = Types.ListType.of(elmentType, true);
 
     checkGravitinoTypeToJdbcType(INT_4 + ARRAY_TOKEN, list1);
+    // PostgreSQL array elements are always nullable
     checkJdbcTypeToGravitinoType(list1, JDBC_ARRAY_PREFIX + INT_4, null, null, 
0);
 
-    // not support element nullable
-    Assertions.assertThrowsExactly(
-        IllegalArgumentException.class,
-        () ->
-            checkGravitinoTypeToJdbcType(INT_4 + ARRAY_TOKEN, 
Types.ListType.of(elmentType, true)));
+    // element nullability is not enforced, PostgreSQL cannot declare it 
either way
+    checkGravitinoTypeToJdbcType(INT_4 + ARRAY_TOKEN, 
Types.ListType.of(elmentType, false));
 
     // not support multidimensional
-    Type list2 = Types.ListType.of(list1, false);
+    Type list2 = Types.ListType.of(list1, true);
     Assertions.assertThrowsExactly(
         IllegalArgumentException.class,
         () -> checkGravitinoTypeToJdbcType(INT_4 + ARRAY_TOKEN, list2));
diff --git 
a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/integration/test/CatalogPostgreSqlIT.java
 
b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/integration/test/CatalogPostgreSqlIT.java
index 7bea7747eb..9441493386 100644
--- 
a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/integration/test/CatalogPostgreSqlIT.java
+++ 
b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/integration/test/CatalogPostgreSqlIT.java
@@ -278,7 +278,7 @@ public class CatalogPostgreSqlIT extends BaseIT {
   @Test
   void testCreateTableWithArrayType() {
     String tableName = 
GravitinoITUtils.genRandomName("postgresql_it_array_table");
-    Column col = Column.of("array", Types.ListType.of(IntegerType.get(), 
false), "col_4_comment");
+    Column col = Column.of("array", Types.ListType.of(IntegerType.get(), 
true), "col_4_comment");
     Column[] columns = new Column[] {col};
 
     NameIdentifier tableIdentifier = NameIdentifier.of(schemaName, tableName);
@@ -1632,6 +1632,24 @@ public class CatalogPostgreSqlIT extends BaseIT {
     Assertions.assertEquals(Types.ExternalType.of("bit"), 
loadedTable.columns()[0].dataType());
   }
 
+  @Test
+  void testUnconstrainedNumericAndArrayTypeConverter() {
+    String tableName = 
GravitinoITUtils.genRandomName("test_numeric_array_type");
+    postgreSqlService.executeQuery(
+        String.format(
+            "CREATE TABLE %s.%s (numeric_col numeric, numeric_col_2 
numeric(10,2), array_col integer[]);",
+            schemaName, tableName));
+    Table loadedTable =
+        catalog.asTableCatalog().loadTable(NameIdentifier.of(schemaName, 
tableName));
+
+    // An unconstrained numeric holds values whose precision and scale vary 
per row
+    Assertions.assertEquals(Types.ExternalType.of("numeric"), 
loadedTable.columns()[0].dataType());
+    Assertions.assertEquals(Types.DecimalType.of(10, 2), 
loadedTable.columns()[1].dataType());
+    // PostgreSQL array elements are always nullable
+    Assertions.assertEquals(
+        Types.ListType.of(Types.IntegerType.get(), true), 
loadedTable.columns()[2].dataType());
+  }
+
   @Test
   void testOperationTableIndex() {
     String tableName = GravitinoITUtils.genRandomName("test_add_index");
diff --git 
a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperations.java
 
b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperations.java
index fb6b1a1fa8..9fb4aafa49 100644
--- 
a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperations.java
+++ 
b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperations.java
@@ -356,7 +356,7 @@ public class TestPostgreSqlTableOperations extends 
TestPostgreSql {
     columns.add(
         JdbcColumn.builder()
             .withName("col_16")
-            .withType(Types.ListType.of(Types.IntegerType.get(), false))
+            .withType(Types.ListType.of(Types.IntegerType.get(), true))
             .withNullable(true)
             .build());
 
diff --git a/docs/jdbc-postgresql-catalog.md b/docs/jdbc-postgresql-catalog.md
index d61637b162..dcdf9428ec 100644
--- a/docs/jdbc-postgresql-catalog.md
+++ b/docs/jdbc-postgresql-catalog.md
@@ -118,6 +118,16 @@ Refer to [Manage Catalogs and 
Schemas](./manage-catalogs-and-schemas.md#schema-o
 :::info
 PostgreSQL doesn't support Gravitino `Fixed` `Struct` `Map` `IntervalDay` 
`IntervalYear` `Union` type.
 Meanwhile, the data types other than listed above are mapped to Gravitino 
**[External Type](./tables-and-views.md#external-type)** that represents an 
unresolvable data type.
+
+An unconstrained `Numeric` column, that is one declared without precision and 
scale, accepts values of up to
+131072 digits before and 16383 digits after the decimal point, and its 
precision and scale vary per row.
+Gravitino `Decimal` caps precision at 38 and is fixed per column, so such a 
column is mapped to the External
+Type `numeric` instead. A `Numeric(p, s)` column is mapped to `Decimal(p, s)` 
and a `Numeric(p)` column to
+`Decimal(p, 0)` as usual.
+
+PostgreSQL array elements always accept NULL and cannot be declared otherwise, 
so an `Array` column is always
+mapped to a `List` whose elements are nullable. A `List` created with 
non-nullable elements is accepted and
+produces an ordinary array whose elements accept NULL.
 :::
 
 ### Table Column Auto-Increment
diff --git 
a/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/utils/TypeUtils.java
 
b/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/utils/TypeUtils.java
index 2b032dac7e..5dd84f2e7d 100644
--- 
a/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/utils/TypeUtils.java
+++ 
b/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/utils/TypeUtils.java
@@ -23,12 +23,14 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Collectors;
 import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.api.ValidationException;
 import org.apache.flink.table.types.DataType;
 import org.apache.flink.table.types.logical.ArrayType;
 import org.apache.flink.table.types.logical.BinaryType;
 import org.apache.flink.table.types.logical.CharType;
 import org.apache.flink.table.types.logical.DecimalType;
 import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.LogicalTypeRoot;
 import org.apache.flink.table.types.logical.MapType;
 import org.apache.flink.table.types.logical.MultisetType;
 import org.apache.flink.table.types.logical.RowType;
@@ -36,9 +38,13 @@ import 
org.apache.flink.table.types.logical.utils.LogicalTypeParser;
 import org.apache.flink.table.types.utils.TypeConversions;
 import org.apache.gravitino.rel.types.Type;
 import org.apache.gravitino.rel.types.Types;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class TypeUtils {
 
+  private static final Logger LOG = LoggerFactory.getLogger(TypeUtils.class);
+
   // Flink supports time/timestamp precision from 0 to 9 (nanosecond 
precision).
   // @see
   // 
https://nightlies.apache.org/flink/flink-docs-release-2.1/docs/dev/table/types/#date-and-time
@@ -246,11 +252,25 @@ public class TypeUtils {
       case EXTERNAL:
         Types.ExternalType externalType = (Types.ExternalType) gravitinoType;
         String catalogString = externalType.catalogString();
-        // Parse the external catalog type string back to Flink LogicalType.
-        // This is used to restore types like MULTISET that Gravitino doesn't 
natively support.
-        LogicalType parsedType =
-            LogicalTypeParser.parse(catalogString, 
TypeUtils.class.getClassLoader());
-        return TypeConversions.fromLogicalToDataType(parsedType);
+        // MULTISET is the only Flink type carried as an external type, 
written here by
+        // toGravitinoType and by the Paimon catalog. Every other catalog 
string is a type name of
+        // some other data source, and some of those parse as a Flink type by 
accident, a
+        // PostgreSQL numeric parses as DECIMAL(10, 0). Only a parsed MULTISET 
is therefore
+        // restored, and any other type is read as a string to keep the rest 
of the table usable.
+        try {
+          LogicalType parsedType =
+              LogicalTypeParser.parse(catalogString, 
TypeUtils.class.getClassLoader());
+          if (parsedType.getTypeRoot() == LogicalTypeRoot.MULTISET) {
+            return TypeConversions.fromLogicalToDataType(parsedType);
+          }
+          LOG.warn("External type {} is not a Flink type, reading it as a 
string.", catalogString);
+        } catch (ValidationException e) {
+          LOG.warn(
+              "External type {} cannot be parsed as a Flink type, reading it 
as a string.",
+              catalogString,
+              e);
+        }
+        return DataTypes.STRING();
       default:
         throw new UnsupportedOperationException("Not support " + 
gravitinoType.toString());
     }
diff --git 
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/utils/TestTypeUtils.java
 
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/utils/TestTypeUtils.java
index 9d0c71ec84..ce64056c5b 100644
--- 
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/utils/TestTypeUtils.java
+++ 
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/utils/TestTypeUtils.java
@@ -285,4 +285,31 @@ public class TestTypeUtils {
         DataTypes.MULTISET(DataTypes.BIGINT()),
         TypeUtils.toFlinkType(Types.ExternalType.of("MULTISET<BIGINT>")));
   }
+
+  @Test
+  public void testExternalTypeFromOtherCatalogConversion() {
+    // A type name of another data source that parses as a Flink type by 
accident: a PostgreSQL
+    // numeric parses as DECIMAL(10, 0)
+    Assertions.assertEquals(
+        DataTypes.STRING(), 
TypeUtils.toFlinkType(Types.ExternalType.of("numeric")));
+
+    // A type name that is no Flink type at all
+    Assertions.assertEquals(
+        DataTypes.STRING(), 
TypeUtils.toFlinkType(Types.ExternalType.of("money")));
+    Assertions.assertEquals(
+        DataTypes.STRING(), 
TypeUtils.toFlinkType(Types.ExternalType.of("json")));
+  }
+
+  @Test
+  public void testMultisetSpellingsConversion() {
+    // The Paimon catalog writes the external type with asSQLString, which 
spells the element type
+    // STRING rather than VARCHAR(2147483647)
+    Assertions.assertEquals(
+        DataTypes.MULTISET(DataTypes.STRING()),
+        TypeUtils.toFlinkType(Types.ExternalType.of("MULTISET<STRING>")));
+
+    Assertions.assertEquals(
+        DataTypes.MULTISET(DataTypes.BIGINT()),
+        TypeUtils.toFlinkType(Types.ExternalType.of("multiset<bigint>")));
+  }
 }
diff --git 
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/jdbc/SparkJdbcTypeConverter.java
 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/jdbc/SparkJdbcTypeConverter.java
index e7dccbf1c8..e8b6433a47 100644
--- 
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/jdbc/SparkJdbcTypeConverter.java
+++ 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/jdbc/SparkJdbcTypeConverter.java
@@ -24,14 +24,26 @@ import org.apache.gravitino.rel.types.Types;
 import org.apache.gravitino.spark.connector.SparkTypeConverter;
 import org.apache.spark.sql.types.DataType;
 import org.apache.spark.sql.types.DataTypes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class SparkJdbcTypeConverter extends SparkTypeConverter {
 
+  private static final Logger LOG = 
LoggerFactory.getLogger(SparkJdbcTypeConverter.class);
+
   @Override
   public DataType toSparkType(Type gravitinoType) {
     if (gravitinoType instanceof Types.VarCharType) {
       // Spark's JDBC dialects reject VarcharType, so widen it to StringType.
       return DataTypes.StringType;
+    } else if (gravitinoType instanceof Types.ExternalType) {
+      // An external type carries a source type that Gravitino cannot 
represent, such as an
+      // unconstrained PostgreSQL numeric. Reading it as a string keeps an 
unmapped type from
+      // failing the type conversion of the whole table.
+      LOG.warn(
+          "Reading type {} as a string, Gravitino cannot represent it",
+          ((Types.ExternalType) gravitinoType).catalogString());
+      return DataTypes.StringType;
     } else if (gravitinoType instanceof Types.TimestampType) {
       // Both timestamp flavors map to TimestampType, never TimestampNTZType: 
the MySQL dialect only
       // pushes TimestampType literals down correctly, and NTZ literals 
produce invalid SQL.
diff --git 
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/jdbc/TestSparkJdbcTypeConverter.java
 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/jdbc/TestSparkJdbcTypeConverter.java
index 00fd1c7784..044af357d8 100644
--- 
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/jdbc/TestSparkJdbcTypeConverter.java
+++ 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/jdbc/TestSparkJdbcTypeConverter.java
@@ -44,4 +44,12 @@ public class TestSparkJdbcTypeConverter {
     Assertions.assertEquals(
         DataTypes.StringType, 
sparkJdbcTypeConverter.toSparkType(Types.VarCharType.of(10)));
   }
+
+  @Test
+  void testConvertExternalTypeToSparkString() {
+    Assertions.assertEquals(
+        DataTypes.StringType, 
sparkJdbcTypeConverter.toSparkType(Types.ExternalType.of("numeric")));
+    Assertions.assertEquals(
+        DataTypes.StringType, 
sparkJdbcTypeConverter.toSparkType(Types.ExternalType.of("json")));
+  }
 }
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/jdbc/postgresql/PostgreSQLDataTypeTransformer.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/jdbc/postgresql/PostgreSQLDataTypeTransformer.java
index cd04413e32..636544f0ba 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/jdbc/postgresql/PostgreSQLDataTypeTransformer.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/jdbc/postgresql/PostgreSQLDataTypeTransformer.java
@@ -19,6 +19,7 @@
 
 package org.apache.gravitino.trino.connector.catalog.jdbc.postgresql;
 
+import io.airlift.log.Logger;
 import io.trino.spi.TrinoException;
 import io.trino.spi.type.CharType;
 import io.trino.spi.type.TimeType;
@@ -32,6 +33,8 @@ import 
org.apache.gravitino.trino.connector.util.GeneralDataTypeTransformer;
 
 /** Type transformer between PostgreSQL and Trino */
 public class PostgreSQLDataTypeTransformer extends GeneralDataTypeTransformer {
+  private static final Logger LOG = 
Logger.get(PostgreSQLDataTypeTransformer.class);
+
   @SuppressWarnings("UnusedVariable")
   private static final int POSTGRESQL_CHAR_LENGTH_LIMIT = 10485760;
   // 1 GB, please refer to
@@ -82,6 +85,15 @@ public class PostgreSQLDataTypeTransformer extends 
GeneralDataTypeTransformer {
       }
       // When precision is not set, the default precision is 3 (milliseconds 
precision)
       return TimeType.TIME_MILLIS;
+    } else if (Name.EXTERNAL == type.name()) {
+      // An external type carries a PostgreSQL type that Gravitino cannot 
represent, such as an
+      // unconstrained numeric or money. Reading it as varchar is equivalent 
to configuring
+      // unsupported_type_handling=CONVERT_TO_VARCHAR, whose Trino default is 
IGNORE, and keeps an
+      // unmapped type from failing the type conversion of the whole table.
+      LOG.warn(
+          "Reading PostgreSQL type %s as varchar, Gravitino cannot represent 
it",
+          ((Types.ExternalType) type).catalogString());
+      return io.trino.spi.type.VarcharType.createUnboundedVarcharType();
     }
 
     return super.getTrinoType(type);
@@ -113,9 +125,6 @@ public class PostgreSQLDataTypeTransformer extends 
GeneralDataTypeTransformer {
       }
 
       return Types.VarCharType.of(varcharType.getLength().get());
-    } else if (typeClass == io.trino.spi.type.ArrayType.class) {
-      return Types.ListType.of(
-          getGravitinoType(((io.trino.spi.type.ArrayType) 
type).getElementType()), false);
     }
 
     return super.getGravitinoType(type);
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/jdbc/postgresql/TestPostgreSQLDataTypeTransformer.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/jdbc/postgresql/TestPostgreSQLDataTypeTransformer.java
index 831fce11f7..7e72fc4fa2 100644
--- 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/jdbc/postgresql/TestPostgreSQLDataTypeTransformer.java
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/jdbc/postgresql/TestPostgreSQLDataTypeTransformer.java
@@ -58,6 +58,17 @@ public class TestPostgreSQLDataTypeTransformer {
         Types.StringType.get());
   }
 
+  @Test
+  public void testGravitinoExternalTypeToTrinoType() {
+    GeneralDataTypeTransformer generalDataTypeTransformer = new 
PostgreSQLDataTypeTransformer();
+    Assertions.assertEquals(
+        
generalDataTypeTransformer.getTrinoType(Types.ExternalType.of("numeric")),
+        io.trino.spi.type.VarcharType.createUnboundedVarcharType());
+    Assertions.assertEquals(
+        
generalDataTypeTransformer.getTrinoType(Types.ExternalType.of("money")),
+        io.trino.spi.type.VarcharType.createUnboundedVarcharType());
+  }
+
   @Test
   public void testGravitinoCharToTrinoType() {
     GeneralDataTypeTransformer generalDataTypeTransformer = new 
PostgreSQLDataTypeTransformer();

Reply via email to