Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
/** Type converter for MySQL. */
public class MysqlTypeConverter extends JdbcTypeConverter {

private static final int MAX_DATETIME_PRECISION = 6;

static final String BIT = "bit";
static final String TINYINT = "tinyint";
static final String TINYINT_UNSIGNED = "tinyint unsigned";
Expand Down Expand Up @@ -146,6 +148,9 @@ public String fromGravitino(Type type) {
// from UTC to the current time zone for retrieval. (This does not occur for other types
// such as DATETIME.) see more details: https://dev.mysql.com/doc/refman/8.0/en/datetime.html
Types.TimestampType timestampType = (Types.TimestampType) type;
if (timestampType.hasPrecisionSet() && timestampType.precision() > MAX_DATETIME_PRECISION) {
throw unsupportedType(type, "fractional-second precision must be between 0 and 6");
}
String baseType = timestampType.hasTimeZone() ? TIMESTAMP : DATETIME;
return timestampType.hasPrecisionSet()
? String.format("%s(%d)", baseType, timestampType.precision())
Expand All @@ -160,10 +165,25 @@ public String fromGravitino(Type type) {
return type.simpleString();
} else if (type instanceof Types.BooleanType) {
return BIT;
} else if (type instanceof Types.VariantType) {
throw unsupportedType(
type, "MySQL JSON does not represent the complete Variant value domain");
} else if (type instanceof Types.NullType) {
throw unsupportedType(type, "the null-only placeholder has no MySQL column type");
} else if (type instanceof Types.GeometryType) {
throw unsupportedType(type, "the JDBC catalog cannot round-trip Geometry CRS/SRID metadata");
} else if (type instanceof Types.GeographyType) {
throw unsupportedType(
type, "MySQL has no Geography type that preserves CRS and edge-algorithm metadata");
} else if (type instanceof Types.ExternalType) {
return ((Types.ExternalType) type).catalogString();
}
throw new IllegalArgumentException(
String.format("Couldn't convert Gravitino type %s to MySQL type", type.simpleString()));
}

private static IllegalArgumentException unsupportedType(Type type, String reason) {
return new IllegalArgumentException(
String.format("MySQL does not support Gravitino type %s: %s", type.simpleString(), reason));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,11 @@ public void testToGravitinoType() {
checkJdbcTypeToGravitinoType(Types.TimestampType.withoutTimeZone(0), DATETIME, 19, null, 0);
checkJdbcTypeToGravitinoType(Types.TimestampType.withoutTimeZone(3), DATETIME, 23, null, 3);
checkJdbcTypeToGravitinoType(Types.TimestampType.withoutTimeZone(6), DATETIME, 26, null, 6);
checkJdbcTypeToGravitinoType(Types.TimestampType.withoutTimeZone(9), DATETIME, 29, null, 9);
checkJdbcTypeToGravitinoType(Types.TimestampType.withTimeZone(), TIMESTAMP, null, null, null);
checkJdbcTypeToGravitinoType(Types.TimestampType.withTimeZone(0), TIMESTAMP, null, null, 0);
checkJdbcTypeToGravitinoType(Types.TimestampType.withTimeZone(0), TIMESTAMP, null, null, 0);
checkJdbcTypeToGravitinoType(Types.TimestampType.withTimeZone(3), TIMESTAMP, 23, null, 3);
checkJdbcTypeToGravitinoType(Types.TimestampType.withTimeZone(6), TIMESTAMP, 26, null, 6);
checkJdbcTypeToGravitinoType(Types.TimestampType.withTimeZone(9), TIMESTAMP, 29, null, 9);
checkJdbcTypeToGravitinoType(Types.DecimalType.of(10, 2), DECIMAL, 10, 2, 0);
checkJdbcTypeToGravitinoType(Types.DecimalType.of(10, 2), DECIMAL_UNSIGNED, 10, 2, 0);
checkJdbcTypeToGravitinoType(Types.VarCharType.of(20), VARCHAR, 20, null, 0);
Expand Down Expand Up @@ -104,6 +102,78 @@ public void testFromGravitinoType() {
() -> MYSQL_TYPE_CONVERTER.fromGravitino(Types.UnparsedType.of(USER_DEFINED_TYPE)));
}

@Test
public void testRejectNanosecondTimestampTypes() {
checkGravitinoTypeToJdbcType("datetime(6)", Types.TimestampType.withoutTimeZone(6));
checkGravitinoTypeToJdbcType("timestamp(6)", Types.TimestampType.withTimeZone(6));

IllegalArgumentException timestampException =
Assertions.assertThrows(
IllegalArgumentException.class,
() -> MYSQL_TYPE_CONVERTER.fromGravitino(Types.TimestampType.withoutTimeZone(9)));
Assertions.assertTrue(
timestampException
.getMessage()
.contains("fractional-second precision must be between 0 and 6"));

IllegalArgumentException timestampTzException =
Assertions.assertThrows(
IllegalArgumentException.class,
() -> MYSQL_TYPE_CONVERTER.fromGravitino(Types.TimestampType.withTimeZone(9)));
Assertions.assertTrue(
timestampTzException
.getMessage()
.contains("fractional-second precision must be between 0 and 6"));
}

@Test
public void testRejectVariantType() {
IllegalArgumentException exception =
Assertions.assertThrows(
IllegalArgumentException.class,
() -> MYSQL_TYPE_CONVERTER.fromGravitino(Types.VariantType.get()));
Assertions.assertTrue(
exception
.getMessage()
.contains("MySQL JSON does not represent the complete Variant value domain"));
}

@Test
public void testRejectNullType() {
IllegalArgumentException exception =
Assertions.assertThrows(
IllegalArgumentException.class,
() -> MYSQL_TYPE_CONVERTER.fromGravitino(Types.NullType.get()));
Assertions.assertTrue(
exception.getMessage().contains("the null-only placeholder has no MySQL column type"));
}

@Test
public void testRejectGeometryType() {
IllegalArgumentException exception =
Assertions.assertThrows(
IllegalArgumentException.class,
() -> MYSQL_TYPE_CONVERTER.fromGravitino(Types.GeometryType.of("SRID:3857")));
Assertions.assertTrue(
exception
.getMessage()
.contains("the JDBC catalog cannot round-trip Geometry CRS/SRID metadata"));
}

@Test
public void testRejectGeographyType() {
IllegalArgumentException exception =
Assertions.assertThrows(
IllegalArgumentException.class,
() ->
MYSQL_TYPE_CONVERTER.fromGravitino(Types.GeographyType.of("EPSG:4326", "karney")));
Assertions.assertTrue(
exception
.getMessage()
.contains(
"MySQL has no Geography type that preserves CRS and edge-algorithm metadata"));
}

protected void checkGravitinoTypeToJdbcType(String jdbcTypeName, Type gravitinoType) {
Assertions.assertEquals(jdbcTypeName, MYSQL_TYPE_CONVERTER.fromGravitino(gravitinoType));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,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;
Expand Down Expand Up @@ -2381,6 +2382,48 @@ void testTimeTypePrecision() {
}
}

@Test
void testRejectNanosecondTimestampsWithoutSideEffects() {
assertUnsupportedV3TypeDoesNotCreateTable(
"test_timestamp_ns",
Types.TimestampType.withoutTimeZone(9),
"fractional-second precision must be between 0 and 6");
assertUnsupportedV3TypeDoesNotCreateTable(
"test_timestamp_tz_ns",
Types.TimestampType.withTimeZone(9),
"fractional-second precision must be between 0 and 6");
}

@Test
void testRejectVariantWithoutSideEffects() {
assertUnsupportedV3TypeDoesNotCreateTable(
"test_variant",
Types.VariantType.get(),
"MySQL JSON does not represent the complete Variant value domain");
}

@Test
void testRejectNullTypeWithoutSideEffects() {
assertUnsupportedV3TypeDoesNotCreateTable(
"test_null", Types.NullType.get(), "the null-only placeholder has no MySQL column type");
}

@Test
void testRejectGeometryWithoutSideEffects() {
assertUnsupportedV3TypeDoesNotCreateTable(
"test_geometry",
Types.GeometryType.of("SRID:3857"),
"the JDBC catalog cannot round-trip Geometry CRS/SRID metadata");
}

@Test
void testRejectGeographyWithoutSideEffects() {
assertUnsupportedV3TypeDoesNotCreateTable(
"test_geography",
Types.GeographyType.of("EPSG:4326", "karney"),
"MySQL has no Geography type that preserves CRS and edge-algorithm metadata");
}

@Test
void testObjectNamesWithDots() {
// Create a MySQL database with a dot in its name directly (bypassing Gravitino)
Expand Down Expand Up @@ -2426,4 +2469,22 @@ void testObjectNamesWithDots() {
mysqlService.executeQuery("DROP DATABASE IF EXISTS `" + dottedSchemaName + "`");
}
}

private void assertUnsupportedV3TypeDoesNotCreateTable(
String tablePrefix, Type type, String expectedMessage) {
NameIdentifier tableIdent =
NameIdentifier.of(schemaName, GravitinoITUtils.genRandomName(tablePrefix));
Column[] columns = {Column.of("v3_column", type, "V3 column")};

IllegalArgumentException exception =
Assertions.assertThrows(
IllegalArgumentException.class,
() ->
catalog
.asTableCatalog()
.createTable(tableIdent, columns, null, Collections.emptyMap()));

Assertions.assertTrue(exception.getMessage().contains(expectedMessage), exception::getMessage);
Assertions.assertFalse(catalog.asTableCatalog().tableExists(tableIdent));
}
}
21 changes: 18 additions & 3 deletions docs/jdbc-mysql-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,16 +134,31 @@ Refer to [Manage Relational Metadata Using Gravitino](./manage-relational-metada
| `String` | `Text` |
| `Date` | `Date` |
| `Time[(p)]` | `Time[(p)]` |
| `Timestamp_tz[(p)]` | `Timestamp(p)` |
| `Timestamp[(p)]` | `Datetime[(p)]` |
| `Timestamp_tz[(p)]` | `Timestamp[(p)]`, p in [0, 6] |
| `Timestamp[(p)]` | `Datetime[(p)]`, p in [0, 6] |
| `Decimal` | `Decimal` |
| `VarChar` | `VarChar` |
| `FixedChar` | `FixedChar` |
| `Binary` | `Binary` |
| `BOOLEAN` | `BIT` |

MySQL supports fractional-second precision from 0 through 6. The catalog rejects Gravitino
`Timestamp` and `Timestamp_tz` precision from 7 through 9 before executing DDL.

Gravitino `Variant` is rejected before executing DDL because MySQL `JSON` does not represent the
complete Variant value domain.

Gravitino `Null` (the V3 unknown type) is rejected before executing DDL because it is a null-only
placeholder, not a MySQL column type.

Although MySQL has a native `GEOMETRY` type, Gravitino `Geometry` is rejected before executing DDL
because the JDBC catalog cannot round-trip its CRS/SRID metadata without loss.

Gravitino `Geography` is rejected before executing DDL because MySQL has no distinct geography type
that preserves both its CRS and edge-algorithm metadata.

:::info
MySQL doesn't support Gravitino `Fixed` `Struct` `List` `Map` `IntervalDay` `IntervalYear` `Union` `UUID` type.
MySQL doesn't support Gravitino `Fixed` `Struct` `List` `Map` `IntervalDay` `IntervalYear` `Union` `UUID` `Variant` `Null` `Geometry` `Geography` 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.
:::

Expand Down
Loading