diff --git a/catalogs-contrib/catalog-jdbc-oceanbase/src/main/java/org/apache/gravitino/catalog/oceanbase/converter/OceanBaseTypeConverter.java b/catalogs-contrib/catalog-jdbc-oceanbase/src/main/java/org/apache/gravitino/catalog/oceanbase/converter/OceanBaseTypeConverter.java index fe5a43fa4b9..9ac67ca61ac 100644 --- a/catalogs-contrib/catalog-jdbc-oceanbase/src/main/java/org/apache/gravitino/catalog/oceanbase/converter/OceanBaseTypeConverter.java +++ b/catalogs-contrib/catalog-jdbc-oceanbase/src/main/java/org/apache/gravitino/catalog/oceanbase/converter/OceanBaseTypeConverter.java @@ -26,6 +26,8 @@ /** Type converter for OceanBase. */ public class OceanBaseTypeConverter extends JdbcTypeConverter { + private static final int MAX_TIMESTAMP_PRECISION = 6; + static final String TINYINT = "tinyint"; static final String TINYINT_UNSIGNED = "tinyint unsigned"; static final String SMALLINT = "smallint"; @@ -137,6 +139,13 @@ 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( + "OceanBase MySQL mode cannot preserve timestamp precision %d; " + + "the maximum supported precision is %d", + timestampType.precision(), MAX_TIMESTAMP_PRECISION)); + } String baseType = timestampType.hasTimeZone() ? TIMESTAMP : DATETIME; return timestampType.hasPrecisionSet() ? String.format("%s(%d)", baseType, timestampType.precision()) @@ -149,6 +158,18 @@ public String fromGravitino(Type type) { return type.simpleString(); } else if (type instanceof Types.BinaryType) { return type.simpleString(); + } else if (type instanceof Types.NullType) { + throw new IllegalArgumentException( + "OceanBase has no column type that preserves the Gravitino unknown type"); + } else if (type instanceof Types.VariantType) { + throw new IllegalArgumentException( + "OceanBase JSON cannot losslessly preserve the Gravitino variant type"); + } else if (type instanceof Types.GeometryType) { + throw new IllegalArgumentException( + "OceanBase JDBC metadata cannot preserve Gravitino geometry CRS/SRID metadata"); + } else if (type instanceof Types.GeographyType) { + throw new IllegalArgumentException( + "OceanBase has no geography column type that preserves CRS and edge algorithm metadata"); } else if (type instanceof Types.ExternalType) { return ((Types.ExternalType) type).catalogString(); } diff --git a/catalogs-contrib/catalog-jdbc-oceanbase/src/test/java/org/apache/gravitino/catalog/oceanbase/converter/TestOceanBaseTypeConverter.java b/catalogs-contrib/catalog-jdbc-oceanbase/src/test/java/org/apache/gravitino/catalog/oceanbase/converter/TestOceanBaseTypeConverter.java index 2026d9140a2..42e3c0a4784 100644 --- a/catalogs-contrib/catalog-jdbc-oceanbase/src/test/java/org/apache/gravitino/catalog/oceanbase/converter/TestOceanBaseTypeConverter.java +++ b/catalogs-contrib/catalog-jdbc-oceanbase/src/test/java/org/apache/gravitino/catalog/oceanbase/converter/TestOceanBaseTypeConverter.java @@ -99,10 +99,59 @@ public void testFromGravitinoType() { () -> OCEANBASE_TYPE_CONVERTER.fromGravitino(Types.UnparsedType.of(USER_DEFINED_TYPE))); } + @Test + public void testRejectNanosecondTimestampTypes() { + assertFromGravitinoRejected( + Types.TimestampType.withoutTimeZone(9), + "OceanBase MySQL mode cannot preserve timestamp precision 9; " + + "the maximum supported precision is 6"); + assertFromGravitinoRejected( + Types.TimestampType.withTimeZone(9), + "OceanBase MySQL mode cannot preserve timestamp precision 9; " + + "the maximum supported precision is 6"); + } + + @Test + public void testRejectVariantType() { + assertFromGravitinoRejected( + Types.VariantType.get(), + "OceanBase JSON cannot losslessly preserve the Gravitino variant type"); + } + + @Test + public void testRejectUnknownType() { + assertFromGravitinoRejected( + Types.NullType.get(), + "OceanBase has no column type that preserves the Gravitino unknown type"); + } + + @Test + public void testRejectGeometryTypes() { + String expectedMessage = + "OceanBase JDBC metadata cannot preserve Gravitino geometry CRS/SRID metadata"; + assertFromGravitinoRejected(Types.GeometryType.crs84(), expectedMessage); + assertFromGravitinoRejected(Types.GeometryType.of("EPSG:3857"), expectedMessage); + } + + @Test + public void testRejectGeographyTypes() { + String expectedMessage = + "OceanBase has no geography column type that preserves CRS and edge algorithm metadata"; + assertFromGravitinoRejected(Types.GeographyType.crs84(), expectedMessage); + assertFromGravitinoRejected(Types.GeographyType.of("EPSG:4326", "karney"), expectedMessage); + } + protected void checkGravitinoTypeToJdbcType(String jdbcTypeName, Type gravitinoType) { Assertions.assertEquals(jdbcTypeName, OCEANBASE_TYPE_CONVERTER.fromGravitino(gravitinoType)); } + private void assertFromGravitinoRejected(Type type, String expectedMessage) { + IllegalArgumentException exception = + Assertions.assertThrows( + IllegalArgumentException.class, () -> OCEANBASE_TYPE_CONVERTER.fromGravitino(type)); + Assertions.assertEquals(expectedMessage, exception.getMessage()); + } + protected void checkJdbcTypeToGravitinoType( Type gravitinoType, String jdbcTypeName, diff --git a/catalogs-contrib/catalog-jdbc-oceanbase/src/test/java/org/apache/gravitino/catalog/oceanbase/integration/test/CatalogOceanBaseIT.java b/catalogs-contrib/catalog-jdbc-oceanbase/src/test/java/org/apache/gravitino/catalog/oceanbase/integration/test/CatalogOceanBaseIT.java index 260d851ac7f..c7e87bca59a 100644 --- a/catalogs-contrib/catalog-jdbc-oceanbase/src/test/java/org/apache/gravitino/catalog/oceanbase/integration/test/CatalogOceanBaseIT.java +++ b/catalogs-contrib/catalog-jdbc-oceanbase/src/test/java/org/apache/gravitino/catalog/oceanbase/integration/test/CatalogOceanBaseIT.java @@ -69,6 +69,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.utils.RandomNameUtils; import org.junit.jupiter.api.AfterAll; @@ -1990,4 +1991,78 @@ void testTimeTypePrecision() { } } } + + @Test + void testRejectNanosecondTimestampsWithoutCreatingTables() { + assertCreateRejectedWithoutSideEffect( + "timestamp_ns", + Types.TimestampType.withoutTimeZone(9), + "OceanBase MySQL mode cannot preserve timestamp precision 9; " + + "the maximum supported precision is 6"); + assertCreateRejectedWithoutSideEffect( + "timestamptz_ns", + Types.TimestampType.withTimeZone(9), + "OceanBase MySQL mode cannot preserve timestamp precision 9; " + + "the maximum supported precision is 6"); + } + + @Test + void testRejectVariantWithoutCreatingTable() { + assertCreateRejectedWithoutSideEffect( + "variant", + Types.VariantType.get(), + "OceanBase JSON cannot losslessly preserve the Gravitino variant type"); + } + + @Test + void testRejectUnknownWithoutCreatingTable() { + assertCreateRejectedWithoutSideEffect( + "unknown", + Types.NullType.get(), + "OceanBase has no column type that preserves the Gravitino unknown type"); + } + + @Test + void testRejectGeometryWithoutCreatingTables() { + String expectedMessage = + "OceanBase JDBC metadata cannot preserve Gravitino geometry CRS/SRID metadata"; + assertCreateRejectedWithoutSideEffect( + "geometry_crs84", Types.GeometryType.crs84(), expectedMessage); + assertCreateRejectedWithoutSideEffect( + "geometry_epsg3857", Types.GeometryType.of("EPSG:3857"), expectedMessage); + } + + @Test + void testRejectGeographyWithoutCreatingTables() { + String expectedMessage = + "OceanBase has no geography column type that preserves CRS and edge algorithm metadata"; + assertCreateRejectedWithoutSideEffect( + "geography_crs84", Types.GeographyType.crs84(), expectedMessage); + assertCreateRejectedWithoutSideEffect( + "geography_karney", Types.GeographyType.of("EPSG:4326", "karney"), expectedMessage); + } + + private void assertCreateRejectedWithoutSideEffect( + String tablePrefix, Type type, String expectedMessage) { + TableCatalog tableCatalog = catalog.asTableCatalog(); + NameIdentifier tableIdentifier = + NameIdentifier.of(schemaName, GravitinoITUtils.genRandomName(tablePrefix)); + + IllegalArgumentException exception = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> + tableCatalog.createTable( + tableIdentifier, + new Column[] {Column.of("value", type)}, + "unsupported type", + Collections.emptyMap())); + + Assertions.assertTrue( + exception.getMessage().contains(expectedMessage), + String.format( + "Expected rejection to contain \"%s\", but was: %s", + expectedMessage, exception.getMessage())); + Assertions.assertFalse(tableCatalog.tableExists(tableIdentifier)); + } } 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 2897660bfc2..e4dec7e43f7 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 @@ -27,6 +27,7 @@ import org.apache.gravitino.rel.expressions.literals.Literals; import org.apache.gravitino.rel.expressions.transforms.Transforms; import org.apache.gravitino.rel.indexes.Indexes; +import org.apache.gravitino.rel.types.Type; import org.apache.gravitino.rel.types.Types; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -89,4 +90,53 @@ public void testCreateTableWithNonEmptyStringDefaultValue() { sql.contains("DEFAULT " + converter.fromGravitino(col1.defaultValue())), "Should contain DEFAULT value but was: " + sql); } + + @Test + public void testRejectNanosecondTimestampsBeforeSqlGeneration() { + String expectedMessage = + "OceanBase MySQL mode cannot preserve timestamp precision 9; " + + "the maximum supported precision is 6"; + assertCreateTableRejected(Types.TimestampType.withoutTimeZone(9), expectedMessage); + assertCreateTableRejected(Types.TimestampType.withTimeZone(9), expectedMessage); + } + + @Test + public void testRejectVariantBeforeSqlGeneration() { + assertCreateTableRejected( + Types.VariantType.get(), + "OceanBase JSON cannot losslessly preserve the Gravitino variant type"); + } + + @Test + public void testRejectUnknownBeforeSqlGeneration() { + assertCreateTableRejected( + Types.NullType.get(), + "OceanBase has no column type that preserves the Gravitino unknown type"); + } + + @Test + public void testRejectGeometryBeforeSqlGeneration() { + assertCreateTableRejected( + Types.GeometryType.crs84(), + "OceanBase JDBC metadata cannot preserve Gravitino geometry CRS/SRID metadata"); + } + + @Test + public void testRejectGeographyBeforeSqlGeneration() { + assertCreateTableRejected( + Types.GeographyType.crs84(), + "OceanBase has no geography column type that preserves CRS and edge algorithm metadata"); + } + + private void assertCreateTableRejected(Type type, String expectedMessage) { + TestableOceanBaseTableOperations ops = new TestableOceanBaseTableOperations(); + JdbcColumn column = + JdbcColumn.builder().withName("col1").withType(type).withNullable(true).build(); + + IllegalArgumentException exception = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> ops.createTableSql("test_table", new JdbcColumn[] {column})); + Assertions.assertEquals(expectedMessage, exception.getMessage()); + } } diff --git a/docs/jdbc-oceanbase-catalog.md b/docs/jdbc-oceanbase-catalog.md index 222b0d36439..86a6acd8942 100644 --- a/docs/jdbc-oceanbase-catalog.md +++ b/docs/jdbc-oceanbase-catalog.md @@ -144,6 +144,19 @@ Refer to [Manage Relational Metadata Using Gravitino](./manage-relational-metada | `FixedChar` | `FixedChar` | | `Binary` | `Binary` | +#### Iceberg V3 type interoperability + +The OceanBase catalog either preserves an Iceberg V3 type exactly or rejects it before executing +DDL. The pinned OceanBase 4.2.1 test environment uses MySQL compatibility mode. + +| Gravitino type family | OceanBase behavior | +|-----------------------------------------|------------------------------------------------------------------------------------------------------| +| `Timestamp(9)`, `Timestamp_tz(9)` | Rejected because MySQL-mode `DATETIME` and `TIMESTAMP` preserve at most microsecond precision (0–6). | +| `Variant` | Rejected because OceanBase `JSON` cannot losslessly preserve the complete Gravitino variant domain. | +| `Unknown` (`NullType`) | Rejected because OceanBase has no corresponding column type. | +| `Geometry(crs)` | Rejected because the connector cannot currently reload OceanBase spatial subtype and SRID metadata. | +| `Geography(crs, algorithm)` | Rejected because OceanBase has no type preserving geography edge semantics. | + :::info OceanBase doesn't support Gravitino `Boolean` `Fixed` `Struct` `List` `Map` `IntervalDay` `IntervalYear` `Union` `UUID` 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.