From ee9a252f2610b37d03f3e617c07229741bfbe602 Mon Sep 17 00:00:00 2001 From: Nevin Zheng Date: Fri, 17 Jul 2026 23:13:01 -0700 Subject: [PATCH 1/5] fix(postgresql): reject nanosecond timestamps --- .../converter/PostgreSqlTypeConverter.java | 7 +++++ .../TestPostgreSqlTypeConverter.java | 19 ++++++++++++ .../integration/test/CatalogPostgreSqlIT.java | 29 +++++++++++++++++++ ...ostgreSqlTableOperationsSqlGeneration.java | 18 ++++++++++++ docs/jdbc-postgresql-catalog.md | 6 ++++ 5 files changed, 79 insertions(+) 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 f5161b12f76..2b63b683ea4 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 @@ -44,6 +44,7 @@ public class PostgreSqlTypeConverter extends JdbcTypeConverter { @VisibleForTesting static final String ARRAY_TOKEN = "[]"; @VisibleForTesting static final int DEFAULT_NUMERIC_PRECISION = 38; @VisibleForTesting static final int DEFAULT_NUMERIC_SCALE = 18; + @VisibleForTesting static final int MAX_TIMESTAMP_PRECISION = 6; @Override public Type toGravitino(JdbcTypeBean typeBean) { @@ -127,6 +128,12 @@ public String fromGravitino(Type type) { return type.simpleString(); } else if (type instanceof Types.TimestampType) { Types.TimestampType timestampType = (Types.TimestampType) type; + if (timestampType.hasPrecisionSet() && timestampType.precision() > MAX_TIMESTAMP_PRECISION) { + throw new IllegalArgumentException( + String.format( + "PostgreSQL supports timestamp precision up to %d, but cannot convert Gravitino type %s", + MAX_TIMESTAMP_PRECISION, type.simpleString())); + } String baseType = timestampType.hasTimeZone() ? TIMESTAMP_TZ : TIMESTAMP; return timestampType.hasPrecisionSet() ? String.format("%s(%d)", baseType, timestampType.precision()) 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 e4139441b79..1319a4be0f0 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 @@ -35,6 +35,7 @@ import static org.apache.gravitino.catalog.postgresql.converter.PostgreSqlTypeConverter.INT_4; import static org.apache.gravitino.catalog.postgresql.converter.PostgreSqlTypeConverter.INT_8; import static org.apache.gravitino.catalog.postgresql.converter.PostgreSqlTypeConverter.JDBC_ARRAY_PREFIX; +import static org.apache.gravitino.catalog.postgresql.converter.PostgreSqlTypeConverter.MAX_TIMESTAMP_PRECISION; import static org.apache.gravitino.catalog.postgresql.converter.PostgreSqlTypeConverter.NUMERIC; import static org.apache.gravitino.catalog.postgresql.converter.PostgreSqlTypeConverter.UUID; @@ -137,6 +138,24 @@ public void testFromGravitinoType() { () -> POSTGRE_SQL_TYPE_CONVERTER.fromGravitino(Types.UnparsedType.of(USER_DEFINED_TYPE))); } + @Test + public void testRejectNanosecondTimestampTypes() { + Type[] nanosecondTypes = { + Types.TimestampType.withoutTimeZone(9), Types.TimestampType.withTimeZone(9) + }; + + for (Type type : nanosecondTypes) { + IllegalArgumentException exception = + Assertions.assertThrows( + IllegalArgumentException.class, () -> POSTGRE_SQL_TYPE_CONVERTER.fromGravitino(type)); + Assertions.assertTrue( + exception + .getMessage() + .contains( + "PostgreSQL supports timestamp precision up to " + MAX_TIMESTAMP_PRECISION)); + } + } + protected void checkGravitinoTypeToJdbcType(String jdbcTypeName, Type gravitinoType) { Assertions.assertEquals(jdbcTypeName, POSTGRE_SQL_TYPE_CONVERTER.fromGravitino(gravitinoType)); } 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 68216e674b2..6559dfdff6a 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 @@ -73,6 +73,7 @@ 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.rel.types.Types.IntegerType; import org.apache.gravitino.utils.RandomNameUtils; @@ -1973,4 +1974,32 @@ void testTimeTypePrecision() { } } } + + @Test + void testRejectNanosecondTimestampTypesWithoutSideEffects() { + assertCreateRejectedWithoutSideEffects( + "timestamp_ns", Types.TimestampType.withoutTimeZone(9), "timestamp precision up to 6"); + assertCreateRejectedWithoutSideEffects( + "timestamptz_ns", Types.TimestampType.withTimeZone(9), "timestamp precision up to 6"); + } + + private void assertCreateRejectedWithoutSideEffects( + String typeName, Type type, String expectedMessage) { + String rejectedTableName = GravitinoITUtils.genRandomName("rejected_" + typeName); + NameIdentifier tableIdentifier = NameIdentifier.of(schemaName, rejectedTableName); + TableCatalog tableCatalog = catalog.asTableCatalog(); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + tableCatalog.createTable( + tableIdentifier, + new Column[] {Column.of("unsupported_col", type)}, + null, + ImmutableMap.of())); + + Assertions.assertTrue(exception.getMessage().contains(expectedMessage)); + Assertions.assertFalse(tableCatalog.tableExists(tableIdentifier)); + } } diff --git a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java index 12f2e1834e0..a3f70fe94a2 100644 --- a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java +++ b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java @@ -89,4 +89,22 @@ public void testCreateTableWithNonEmptyStringDefaultValue() { sql.contains("DEFAULT " + converter.fromGravitino(col1.defaultValue())), "Should contain DEFAULT value but was: " + sql); } + + @Test + public void testCreateTableRejectsNanosecondTimestampTypesBeforeSqlGeneration() { + TestablePostgreSqlTableOperations ops = new TestablePostgreSqlTableOperations(); + Types.TimestampType[] nanosecondTypes = { + Types.TimestampType.withoutTimeZone(9), Types.TimestampType.withTimeZone(9) + }; + + for (Types.TimestampType type : nanosecondTypes) { + JdbcColumn column = JdbcColumn.builder().withName("nanosecond_col").withType(type).build(); + IllegalArgumentException exception = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> ops.createTableSql("test_table", new JdbcColumn[] {column})); + Assertions.assertTrue( + exception.getMessage().contains("PostgreSQL supports timestamp precision up to 6")); + } + } } diff --git a/docs/jdbc-postgresql-catalog.md b/docs/jdbc-postgresql-catalog.md index 8235f1b0693..74cce24ebbf 100644 --- a/docs/jdbc-postgresql-catalog.md +++ b/docs/jdbc-postgresql-catalog.md @@ -115,6 +115,12 @@ Refer to [Manage Relational Metadata Using Gravitino](./manage-relational-metada | `UUID` | `Uuid` | | `List` | `Array` | +#### V3 Type Compatibility + +| Gravitino Type | PostgreSQL outcome | +|-------------------------------------|-------------------------------------------------------------------------------------------------------------------------| +| `Timestamp(9)` / `Timestamp_tz(9)` | Rejected before DDL. PostgreSQL only supports precision 0 through 6 and cannot preserve nanosecond timestamp precision. | + :::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](./manage-relational-metadata-using-gravitino.md#external-type)** that represents an unresolvable data type since 0.6.0-incubating. From e2c760180d223cc3792ee309f9f5660ecbfd8a98 Mon Sep 17 00:00:00 2001 From: Nevin Zheng Date: Fri, 17 Jul 2026 23:15:04 -0700 Subject: [PATCH 2/5] fix(postgresql): reject variant type --- .../converter/PostgreSqlTypeConverter.java | 3 +++ .../converter/TestPostgreSqlTypeConverter.java | 15 +++++++++++++++ .../integration/test/CatalogPostgreSqlIT.java | 8 ++++++++ ...stPostgreSqlTableOperationsSqlGeneration.java | 16 ++++++++++++++++ docs/jdbc-postgresql-catalog.md | 1 + 5 files changed, 43 insertions(+) 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 2b63b683ea4..06260ad6f7b 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 @@ -153,6 +153,9 @@ public String fromGravitino(Type type) { return BYTEA; } else if (type instanceof Types.UUIDType) { return UUID; + } else if (type instanceof Types.VariantType) { + throw new IllegalArgumentException( + "PostgreSQL JSON and JSONB do not preserve Gravitino Variant semantics; cannot convert Gravitino type variant"); } else if (type instanceof Types.ListType) { return fromGravitinoArrayType((ListType) type); } else if (type instanceof Types.ExternalType) { 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 1319a4be0f0..28bc2187e90 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 @@ -92,6 +92,8 @@ public void testToGravitinoType() { checkJdbcTypeToGravitinoType(Types.UUIDType.get(), UUID, null, null, 0); checkJdbcTypeToGravitinoType( Types.ExternalType.of(USER_DEFINED_TYPE), USER_DEFINED_TYPE, null, null, 0); + checkJdbcTypeToGravitinoType(Types.ExternalType.of("json"), "json", null, null, 0); + checkJdbcTypeToGravitinoType(Types.ExternalType.of("jsonb"), "jsonb", null, null, 0); } @Test @@ -156,6 +158,19 @@ public void testRejectNanosecondTimestampTypes() { } } + @Test + public void testRejectVariantType() { + IllegalArgumentException exception = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> POSTGRE_SQL_TYPE_CONVERTER.fromGravitino(Types.VariantType.get())); + + Assertions.assertTrue( + exception + .getMessage() + .contains("PostgreSQL JSON and JSONB do not preserve Gravitino Variant semantics")); + } + protected void checkGravitinoTypeToJdbcType(String jdbcTypeName, Type gravitinoType) { Assertions.assertEquals(jdbcTypeName, POSTGRE_SQL_TYPE_CONVERTER.fromGravitino(gravitinoType)); } 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 6559dfdff6a..c7b205d6140 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 @@ -1983,6 +1983,14 @@ void testRejectNanosecondTimestampTypesWithoutSideEffects() { "timestamptz_ns", Types.TimestampType.withTimeZone(9), "timestamp precision up to 6"); } + @Test + void testRejectVariantWithoutSideEffects() { + assertCreateRejectedWithoutSideEffects( + "variant", + Types.VariantType.get(), + "PostgreSQL JSON and JSONB do not preserve Gravitino Variant semantics"); + } + private void assertCreateRejectedWithoutSideEffects( String typeName, Type type, String expectedMessage) { String rejectedTableName = GravitinoITUtils.genRandomName("rejected_" + typeName); diff --git a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java index a3f70fe94a2..a64498fad53 100644 --- a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java +++ b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java @@ -107,4 +107,20 @@ public void testCreateTableRejectsNanosecondTimestampTypesBeforeSqlGeneration() exception.getMessage().contains("PostgreSQL supports timestamp precision up to 6")); } } + + @Test + public void testCreateTableRejectsVariantBeforeSqlGeneration() { + TestablePostgreSqlTableOperations ops = new TestablePostgreSqlTableOperations(); + JdbcColumn column = + JdbcColumn.builder().withName("variant_col").withType(Types.VariantType.get()).build(); + + IllegalArgumentException exception = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> ops.createTableSql("test_table", new JdbcColumn[] {column})); + Assertions.assertTrue( + exception + .getMessage() + .contains("PostgreSQL JSON and JSONB do not preserve Gravitino Variant semantics")); + } } diff --git a/docs/jdbc-postgresql-catalog.md b/docs/jdbc-postgresql-catalog.md index 74cce24ebbf..caa1dd2dc16 100644 --- a/docs/jdbc-postgresql-catalog.md +++ b/docs/jdbc-postgresql-catalog.md @@ -120,6 +120,7 @@ Refer to [Manage Relational Metadata Using Gravitino](./manage-relational-metada | Gravitino Type | PostgreSQL outcome | |-------------------------------------|-------------------------------------------------------------------------------------------------------------------------| | `Timestamp(9)` / `Timestamp_tz(9)` | Rejected before DDL. PostgreSQL only supports precision 0 through 6 and cannot preserve nanosecond timestamp precision. | +| `Variant` | Rejected before DDL. Native `json` and `jsonb` columns load as `External` because they do not preserve Variant semantics. | :::info PostgreSQL doesn't support Gravitino `Fixed` `Struct` `Map` `IntervalDay` `IntervalYear` `Union` type. From 56801664d20a166ed5b010b43cc9fabb4c610895 Mon Sep 17 00:00:00 2001 From: Nevin Zheng Date: Fri, 17 Jul 2026 23:17:26 -0700 Subject: [PATCH 3/5] fix(postgresql): reject unknown type --- .../converter/PostgreSqlTypeConverter.java | 3 +++ .../converter/TestPostgreSqlTypeConverter.java | 14 ++++++++++++++ .../integration/test/CatalogPostgreSqlIT.java | 8 ++++++++ ...stPostgreSqlTableOperationsSqlGeneration.java | 16 ++++++++++++++++ docs/jdbc-postgresql-catalog.md | 1 + 5 files changed, 42 insertions(+) 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 06260ad6f7b..2644912c9f2 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 @@ -153,6 +153,9 @@ public String fromGravitino(Type type) { return BYTEA; } else if (type instanceof Types.UUIDType) { return UUID; + } else if (type instanceof Types.NullType) { + throw new IllegalArgumentException( + "PostgreSQL table columns cannot represent Gravitino Unknown (NullType); cannot convert Gravitino type null"); } else if (type instanceof Types.VariantType) { throw new IllegalArgumentException( "PostgreSQL JSON and JSONB do not preserve Gravitino Variant semantics; cannot convert Gravitino type variant"); 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 28bc2187e90..74a8bf8cd9d 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 @@ -94,6 +94,7 @@ public void testToGravitinoType() { Types.ExternalType.of(USER_DEFINED_TYPE), USER_DEFINED_TYPE, null, null, 0); checkJdbcTypeToGravitinoType(Types.ExternalType.of("json"), "json", null, null, 0); checkJdbcTypeToGravitinoType(Types.ExternalType.of("jsonb"), "jsonb", null, null, 0); + checkJdbcTypeToGravitinoType(Types.ExternalType.of("unknown"), "unknown", null, null, 0); } @Test @@ -171,6 +172,19 @@ public void testRejectVariantType() { .contains("PostgreSQL JSON and JSONB do not preserve Gravitino Variant semantics")); } + @Test + public void testRejectUnknownType() { + IllegalArgumentException exception = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> POSTGRE_SQL_TYPE_CONVERTER.fromGravitino(Types.NullType.get())); + + Assertions.assertTrue( + exception + .getMessage() + .contains("PostgreSQL table columns cannot represent Gravitino Unknown (NullType)")); + } + protected void checkGravitinoTypeToJdbcType(String jdbcTypeName, Type gravitinoType) { Assertions.assertEquals(jdbcTypeName, POSTGRE_SQL_TYPE_CONVERTER.fromGravitino(gravitinoType)); } 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 c7b205d6140..e6a604ff713 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 @@ -1991,6 +1991,14 @@ void testRejectVariantWithoutSideEffects() { "PostgreSQL JSON and JSONB do not preserve Gravitino Variant semantics"); } + @Test + void testRejectUnknownWithoutSideEffects() { + assertCreateRejectedWithoutSideEffects( + "unknown", + Types.NullType.get(), + "PostgreSQL table columns cannot represent Gravitino Unknown (NullType)"); + } + private void assertCreateRejectedWithoutSideEffects( String typeName, Type type, String expectedMessage) { String rejectedTableName = GravitinoITUtils.genRandomName("rejected_" + typeName); diff --git a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java index a64498fad53..8367267d639 100644 --- a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java +++ b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java @@ -123,4 +123,20 @@ public void testCreateTableRejectsVariantBeforeSqlGeneration() { .getMessage() .contains("PostgreSQL JSON and JSONB do not preserve Gravitino Variant semantics")); } + + @Test + public void testCreateTableRejectsUnknownBeforeSqlGeneration() { + TestablePostgreSqlTableOperations ops = new TestablePostgreSqlTableOperations(); + JdbcColumn column = + JdbcColumn.builder().withName("unknown_col").withType(Types.NullType.get()).build(); + + IllegalArgumentException exception = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> ops.createTableSql("test_table", new JdbcColumn[] {column})); + Assertions.assertTrue( + exception + .getMessage() + .contains("PostgreSQL table columns cannot represent Gravitino Unknown (NullType)")); + } } diff --git a/docs/jdbc-postgresql-catalog.md b/docs/jdbc-postgresql-catalog.md index caa1dd2dc16..0e9ee0c27ce 100644 --- a/docs/jdbc-postgresql-catalog.md +++ b/docs/jdbc-postgresql-catalog.md @@ -121,6 +121,7 @@ Refer to [Manage Relational Metadata Using Gravitino](./manage-relational-metada |-------------------------------------|-------------------------------------------------------------------------------------------------------------------------| | `Timestamp(9)` / `Timestamp_tz(9)` | Rejected before DDL. PostgreSQL only supports precision 0 through 6 and cannot preserve nanosecond timestamp precision. | | `Variant` | Rejected before DDL. Native `json` and `jsonb` columns load as `External` because they do not preserve Variant semantics. | +| `Unknown` | Rejected before DDL. PostgreSQL table columns cannot represent an optional, null-only Unknown logical type. | :::info PostgreSQL doesn't support Gravitino `Fixed` `Struct` `Map` `IntervalDay` `IntervalYear` `Union` type. From 862c408e2cd7d0323437a562937728cebf510291 Mon Sep 17 00:00:00 2001 From: Nevin Zheng Date: Fri, 17 Jul 2026 23:18:59 -0700 Subject: [PATCH 4/5] fix(postgresql): reject geometry type --- .../converter/PostgreSqlTypeConverter.java | 5 +++++ .../TestPostgreSqlTypeConverter.java | 15 ++++++++++++++ .../integration/test/CatalogPostgreSqlIT.java | 8 ++++++++ ...ostgreSqlTableOperationsSqlGeneration.java | 20 +++++++++++++++++++ docs/jdbc-postgresql-catalog.md | 1 + 5 files changed, 49 insertions(+) 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 2644912c9f2..83759af0163 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 @@ -159,6 +159,11 @@ public String fromGravitino(Type type) { } else if (type instanceof Types.VariantType) { throw new IllegalArgumentException( "PostgreSQL JSON and JSONB do not preserve Gravitino Variant semantics; cannot convert Gravitino type variant"); + } else if (type instanceof Types.GeometryType) { + throw new IllegalArgumentException( + String.format( + "PostgreSQL PostGIS geometry does not preserve Gravitino Geometry CRS semantics; cannot convert Gravitino type %s", + type.simpleString())); } else if (type instanceof Types.ListType) { return fromGravitinoArrayType((ListType) type); } else if (type instanceof Types.ExternalType) { 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 74a8bf8cd9d..e8a05e4851a 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 @@ -95,6 +95,7 @@ public void testToGravitinoType() { checkJdbcTypeToGravitinoType(Types.ExternalType.of("json"), "json", null, null, 0); checkJdbcTypeToGravitinoType(Types.ExternalType.of("jsonb"), "jsonb", null, null, 0); checkJdbcTypeToGravitinoType(Types.ExternalType.of("unknown"), "unknown", null, null, 0); + checkJdbcTypeToGravitinoType(Types.ExternalType.of("geometry"), "geometry", null, null, 0); } @Test @@ -185,6 +186,20 @@ public void testRejectUnknownType() { .contains("PostgreSQL table columns cannot represent Gravitino Unknown (NullType)")); } + @Test + public void testRejectGeometryType() { + IllegalArgumentException exception = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> POSTGRE_SQL_TYPE_CONVERTER.fromGravitino(Types.GeometryType.of("EPSG:3857"))); + + Assertions.assertTrue( + exception + .getMessage() + .contains( + "PostgreSQL PostGIS geometry does not preserve Gravitino Geometry CRS semantics")); + } + protected void checkGravitinoTypeToJdbcType(String jdbcTypeName, Type gravitinoType) { Assertions.assertEquals(jdbcTypeName, POSTGRE_SQL_TYPE_CONVERTER.fromGravitino(gravitinoType)); } 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 e6a604ff713..c26be079aa2 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 @@ -1999,6 +1999,14 @@ void testRejectUnknownWithoutSideEffects() { "PostgreSQL table columns cannot represent Gravitino Unknown (NullType)"); } + @Test + void testRejectGeometryWithoutSideEffects() { + assertCreateRejectedWithoutSideEffects( + "geometry", + Types.GeometryType.of("EPSG:3857"), + "PostgreSQL PostGIS geometry does not preserve Gravitino Geometry CRS semantics"); + } + private void assertCreateRejectedWithoutSideEffects( String typeName, Type type, String expectedMessage) { String rejectedTableName = GravitinoITUtils.genRandomName("rejected_" + typeName); diff --git a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java index 8367267d639..6228266e91d 100644 --- a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java +++ b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java @@ -139,4 +139,24 @@ public void testCreateTableRejectsUnknownBeforeSqlGeneration() { .getMessage() .contains("PostgreSQL table columns cannot represent Gravitino Unknown (NullType)")); } + + @Test + public void testCreateTableRejectsGeometryBeforeSqlGeneration() { + TestablePostgreSqlTableOperations ops = new TestablePostgreSqlTableOperations(); + JdbcColumn column = + JdbcColumn.builder() + .withName("geometry_col") + .withType(Types.GeometryType.of("EPSG:3857")) + .build(); + + IllegalArgumentException exception = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> ops.createTableSql("test_table", new JdbcColumn[] {column})); + Assertions.assertTrue( + exception + .getMessage() + .contains( + "PostgreSQL PostGIS geometry does not preserve Gravitino Geometry CRS semantics")); + } } diff --git a/docs/jdbc-postgresql-catalog.md b/docs/jdbc-postgresql-catalog.md index 0e9ee0c27ce..439589e5e81 100644 --- a/docs/jdbc-postgresql-catalog.md +++ b/docs/jdbc-postgresql-catalog.md @@ -122,6 +122,7 @@ Refer to [Manage Relational Metadata Using Gravitino](./manage-relational-metada | `Timestamp(9)` / `Timestamp_tz(9)` | Rejected before DDL. PostgreSQL only supports precision 0 through 6 and cannot preserve nanosecond timestamp precision. | | `Variant` | Rejected before DDL. Native `json` and `jsonb` columns load as `External` because they do not preserve Variant semantics. | | `Unknown` | Rejected before DDL. PostgreSQL table columns cannot represent an optional, null-only Unknown logical type. | +| `Geometry` | Rejected before DDL. Native PostGIS `geometry` columns load as `External`; exact CRS semantics are not translated. | :::info PostgreSQL doesn't support Gravitino `Fixed` `Struct` `Map` `IntervalDay` `IntervalYear` `Union` type. From cac653dd9f7f98ebc22ced9ef05c9a4ae61b61c8 Mon Sep 17 00:00:00 2001 From: Nevin Zheng Date: Fri, 17 Jul 2026 23:20:17 -0700 Subject: [PATCH 5/5] fix(postgresql): reject geography type --- .../converter/PostgreSqlTypeConverter.java | 5 +++++ .../TestPostgreSqlTypeConverter.java | 17 ++++++++++++++++ .../integration/test/CatalogPostgreSqlIT.java | 8 ++++++++ ...ostgreSqlTableOperationsSqlGeneration.java | 20 +++++++++++++++++++ docs/jdbc-postgresql-catalog.md | 1 + 5 files changed, 51 insertions(+) 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 83759af0163..f2ad3254589 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 @@ -164,6 +164,11 @@ public String fromGravitino(Type type) { String.format( "PostgreSQL PostGIS geometry does not preserve Gravitino Geometry CRS semantics; cannot convert Gravitino type %s", type.simpleString())); + } else if (type instanceof Types.GeographyType) { + throw new IllegalArgumentException( + String.format( + "PostgreSQL PostGIS geography does not preserve Gravitino Geography CRS and edge-algorithm semantics; cannot convert Gravitino type %s", + type.simpleString())); } else if (type instanceof Types.ListType) { return fromGravitinoArrayType((ListType) type); } else if (type instanceof Types.ExternalType) { 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 e8a05e4851a..a4532949579 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 @@ -96,6 +96,7 @@ public void testToGravitinoType() { checkJdbcTypeToGravitinoType(Types.ExternalType.of("jsonb"), "jsonb", null, null, 0); checkJdbcTypeToGravitinoType(Types.ExternalType.of("unknown"), "unknown", null, null, 0); checkJdbcTypeToGravitinoType(Types.ExternalType.of("geometry"), "geometry", null, null, 0); + checkJdbcTypeToGravitinoType(Types.ExternalType.of("geography"), "geography", null, null, 0); } @Test @@ -200,6 +201,22 @@ public void testRejectGeometryType() { "PostgreSQL PostGIS geometry does not preserve Gravitino Geometry CRS semantics")); } + @Test + public void testRejectGeographyType() { + IllegalArgumentException exception = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> + POSTGRE_SQL_TYPE_CONVERTER.fromGravitino( + Types.GeographyType.of("EPSG:4326", "karney"))); + + Assertions.assertTrue( + exception + .getMessage() + .contains( + "PostgreSQL PostGIS geography does not preserve Gravitino Geography CRS and edge-algorithm semantics")); + } + protected void checkGravitinoTypeToJdbcType(String jdbcTypeName, Type gravitinoType) { Assertions.assertEquals(jdbcTypeName, POSTGRE_SQL_TYPE_CONVERTER.fromGravitino(gravitinoType)); } 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 c26be079aa2..e0a75d45fe5 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 @@ -2007,6 +2007,14 @@ void testRejectGeometryWithoutSideEffects() { "PostgreSQL PostGIS geometry does not preserve Gravitino Geometry CRS semantics"); } + @Test + void testRejectGeographyWithoutSideEffects() { + assertCreateRejectedWithoutSideEffects( + "geography", + Types.GeographyType.of("EPSG:4326", "karney"), + "PostgreSQL PostGIS geography does not preserve Gravitino Geography CRS and edge-algorithm semantics"); + } + private void assertCreateRejectedWithoutSideEffects( String typeName, Type type, String expectedMessage) { String rejectedTableName = GravitinoITUtils.genRandomName("rejected_" + typeName); diff --git a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java index 6228266e91d..c56222426b5 100644 --- a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java +++ b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperationsSqlGeneration.java @@ -159,4 +159,24 @@ public void testCreateTableRejectsGeometryBeforeSqlGeneration() { .contains( "PostgreSQL PostGIS geometry does not preserve Gravitino Geometry CRS semantics")); } + + @Test + public void testCreateTableRejectsGeographyBeforeSqlGeneration() { + TestablePostgreSqlTableOperations ops = new TestablePostgreSqlTableOperations(); + JdbcColumn column = + JdbcColumn.builder() + .withName("geography_col") + .withType(Types.GeographyType.of("EPSG:4326", "karney")) + .build(); + + IllegalArgumentException exception = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> ops.createTableSql("test_table", new JdbcColumn[] {column})); + Assertions.assertTrue( + exception + .getMessage() + .contains( + "PostgreSQL PostGIS geography does not preserve Gravitino Geography CRS and edge-algorithm semantics")); + } } diff --git a/docs/jdbc-postgresql-catalog.md b/docs/jdbc-postgresql-catalog.md index 439589e5e81..a21de41c4a6 100644 --- a/docs/jdbc-postgresql-catalog.md +++ b/docs/jdbc-postgresql-catalog.md @@ -123,6 +123,7 @@ Refer to [Manage Relational Metadata Using Gravitino](./manage-relational-metada | `Variant` | Rejected before DDL. Native `json` and `jsonb` columns load as `External` because they do not preserve Variant semantics. | | `Unknown` | Rejected before DDL. PostgreSQL table columns cannot represent an optional, null-only Unknown logical type. | | `Geometry` | Rejected before DDL. Native PostGIS `geometry` columns load as `External`; exact CRS semantics are not translated. | +| `Geography` | Rejected before DDL. Native PostGIS `geography` columns load as `External`; exact CRS and edge-algorithm semantics are not translated. | :::info PostgreSQL doesn't support Gravitino `Fixed` `Struct` `Map` `IntervalDay` `IntervalYear` `Union` type.