This is an automated email from the ASF dual-hosted git repository.
mchades 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 c2c1b8a672 [#11955] feat(iceberg): native geometry and geography type
support for Iceberg V3 (#11958)
c2c1b8a672 is described below
commit c2c1b8a6727bcd54742e248fb6e5f16e7ca497bb
Author: Nevin Zheng <[email protected]>
AuthorDate: Mon Jul 13 01:57:05 2026 -0700
[#11955] feat(iceberg): native geometry and geography type support for
Iceberg V3 (#11958)
### What changes were proposed in this pull request?
Adds native `Types.GeometryType(crs)` and `Types.GeographyType(crs,
algorithm)` to Gravitino's unified type system, so Iceberg V3 geospatial
columns load through the native metadata API as first-class types
instead of the read-only `ExternalType("GEOMETRY")` /
`ExternalType("GEOGRAPHY")` stopgap, and become creatable through
Gravitino.
- **api**: `Type.Name.GEOMETRY`/`GEOGRAPHY` + two parameterized
`PrimitiveType`s (modeled on `FixedType`/`DecimalType`). CRS is an
opaque string preserved verbatim and compared case-insensitively; the
geography edge algorithm is validated against the five spec values
(`spherical`, `vincenty`, `thomas`, `andoyer`, `karney`) and normalized
to lowercase. Defaults (`OGC:CRS84`, `spherical`) elide to the bare
`geometry`/`geography` token.
- **common (`JsonUtils`)**: registers the default tokens and parses the
parameterized form; the matcher runs against the original
(non-lowercased) type string so a case-sensitive CRS survives the
round-trip.
- **catalog-lakehouse-iceberg**: `FromIcebergType`/`ToIcebergType`
round-trip CRS (and algorithm) via the existing `atomic()` path.
- **clients/client-python, docs, OpenAPI**: mirror both types + serde.
### Why are the changes needed?
Iceberg V3 adds `geometry` and `geography` (planar vs. spheroidal
geospatial shapes, WKB-encoded, parameterized by CRS ± edge algorithm).
Today they load as `external(GEOMETRY/GEOGRAPHY)`, which drops the
CRS/algorithm to a bare name and is opaque to consumers (external types
have caused downstream issues — #10957, #11805), and the write path
throws. Native types preserve the parameters and make the columns
writable. As a catalog, Gravitino only describes the column and
preserves metadata; it does not interpret the WKB.
Fix: #11955
### Does this PR introduce _any_ user-facing change?
Yes. New unified types `Types.GeometryType` / `Types.GeographyType`
(Java + Python), with JSON tokens `geometry` / `geometry(<crs>)` and
`geography` / `geography(<crs>,<algorithm>)`. Iceberg
`geometry`/`geography` columns now load natively and can be created via
Gravitino at `format-version = 3`. No property-key changes.
### How was this patch tested?
- Unit: `TestTypes` (contract + validation + case-insensitivity),
`TestJsonUtils` (JSON serde incl. mixed-case CRS), `TestConvertUtil`
(converter both directions, non-default CRS/algorithm); Python
`test_types.py` + `test_type_serdes.py` (serialize/deserialize
round-trip for `geometry(srid:3857)` and `geography(EPSG:4326,karney)`).
- Integration (REST/IRC backend): `CatalogIcebergRestIT` —
`testV3TypeConversionViaIcebergClient` (IRC→Gravitino native load),
`testCreateGeometryColumnWriteRoundTrip` and
`testCreateGeographyColumnWriteRoundTrip` (Gravitino→IRC write
round-trip). Ran green: 3 tests, 0 skipped, 0 failures.
---------
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
.../java/org/apache/gravitino/rel/types/Type.java | 10 ++
.../java/org/apache/gravitino/rel/types/Types.java | 165 +++++++++++++++++++++
.../java/org/apache/gravitino/rel/TestTypes.java | 26 ++++
.../iceberg/converter/FromIcebergType.java | 8 +
.../lakehouse/iceberg/converter/ToIcebergType.java | 9 ++
.../iceberg/converter/TestConvertUtil.java | 42 ++++++
.../integration/test/CatalogIcebergBaseIT.java | 119 ++++++++++++++-
.../rel/types/json_serdes/_helper/serdes_utils.py | 24 ++-
.../client-python/gravitino/api/rel/types/type.py | 8 +
.../client-python/gravitino/api/rel/types/types.py | 129 ++++++++++++++++
clients/client-python/gravitino/utils/serdes.py | 10 ++
.../tests/unittests/api/rel/test_types.py | 26 ++++
.../unittests/json_serdes/test_type_serdes.py | 7 +
.../java/org/apache/gravitino/json/JsonUtils.java | 19 +++
.../org/apache/gravitino/json/TestJsonUtils.java | 33 +++++
docs/lakehouse-iceberg-catalog.md | 2 +
docs/manage-relational-metadata-using-gravitino.md | 2 +
docs/open-api/datatype.yaml | 4 +
18 files changed, 634 insertions(+), 9 deletions(-)
diff --git a/api/src/main/java/org/apache/gravitino/rel/types/Type.java
b/api/src/main/java/org/apache/gravitino/rel/types/Type.java
index 223762259c..8884a04dc6 100644
--- a/api/src/main/java/org/apache/gravitino/rel/types/Type.java
+++ b/api/src/main/java/org/apache/gravitino/rel/types/Type.java
@@ -78,6 +78,16 @@ public interface Type {
* schema.
*/
VARIANT,
+ /**
+ * The geometry type. A geometry holds a geospatial shape (WKB-encoded) on
a planar coordinate
+ * reference system.
+ */
+ GEOMETRY,
+ /**
+ * The geography type. A geography holds a geospatial shape (WKB-encoded)
on a spheroidal
+ * coordinate reference system, with an edge-interpolation algorithm.
+ */
+ GEOGRAPHY,
/**
* The struct type. A struct type is a complex type that contains a set of
named fields, each
* with a type, and optionally a comment.
diff --git a/api/src/main/java/org/apache/gravitino/rel/types/Types.java
b/api/src/main/java/org/apache/gravitino/rel/types/Types.java
index 11484db648..222d2ba24e 100644
--- a/api/src/main/java/org/apache/gravitino/rel/types/Types.java
+++ b/api/src/main/java/org/apache/gravitino/rel/types/Types.java
@@ -19,8 +19,11 @@
package org.apache.gravitino.rel.types;
import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
import java.util.Arrays;
+import java.util.Locale;
import java.util.Objects;
+import java.util.Set;
import java.util.StringJoiner;
/** The helper class for {@link Type}. */
@@ -679,6 +682,168 @@ public class Types {
}
}
+ /**
+ * The geometry type in Gravitino. A geometry column holds a geospatial
shape (point, line, or
+ * polygon) encoded as WKB on a planar coordinate reference system (CRS).
The CRS is carried as
+ * type metadata; Gravitino, as a catalog, does not interpret the shape
values.
+ */
+ public static class GeometryType extends Type.PrimitiveType {
+
+ /** The default coordinate reference system, {@code OGC:CRS84} (WGS84
longitude/latitude). */
+ public static final String DEFAULT_CRS = "OGC:CRS84";
+
+ /**
+ * @return A {@link GeometryType} with the default CRS ("OGC:CRS84").
+ */
+ public static GeometryType crs84() {
+ return new GeometryType(DEFAULT_CRS);
+ }
+
+ /**
+ * @param crs The coordinate reference system, e.g. "OGC:CRS84",
"EPSG:4326" or "srid:3857".
+ * @return A {@link GeometryType} with the given CRS.
+ */
+ public static GeometryType of(String crs) {
+ return new GeometryType(crs);
+ }
+
+ private final String crs;
+
+ private GeometryType(String crs) {
+ Preconditions.checkArgument(crs != null && !crs.isEmpty(), "crs cannot
be null or empty");
+ this.crs = crs;
+ }
+
+ /**
+ * @return The coordinate reference system of this geometry type.
+ */
+ public String crs() {
+ return crs;
+ }
+
+ @Override
+ public Name name() {
+ return Name.GEOMETRY;
+ }
+
+ @Override
+ public String simpleString() {
+ return DEFAULT_CRS.equalsIgnoreCase(crs) ? "geometry" :
String.format("geometry(%s)", crs);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof GeometryType)) {
+ return false;
+ }
+ GeometryType that = (GeometryType) o;
+ return crs.equalsIgnoreCase(that.crs);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(crs.toLowerCase(Locale.ROOT));
+ }
+ }
+
+ /**
+ * The geography type in Gravitino. A geography column holds a geospatial
shape (point, line, or
+ * polygon) encoded as WKB on a spheroidal coordinate reference system
(CRS), with an
+ * edge-interpolation algorithm describing how edges between points are
computed on the surface.
+ * Both are carried as type metadata; Gravitino, as a catalog, does not
interpret the shape
+ * values.
+ */
+ public static class GeographyType extends Type.PrimitiveType {
+
+ /** The default coordinate reference system, {@code OGC:CRS84} (WGS84
longitude/latitude). */
+ public static final String DEFAULT_CRS = "OGC:CRS84";
+
+ /** The default edge-interpolation algorithm, {@code spherical}. */
+ public static final String DEFAULT_ALGORITHM = "spherical";
+
+ /** The edge-interpolation algorithms defined by the geography
specification. */
+ private static final Set<String> VALID_ALGORITHMS =
+ ImmutableSet.of("spherical", "vincenty", "thomas", "andoyer",
"karney");
+
+ /**
+ * @return A {@link GeographyType} with the default CRS ("OGC:CRS84") and
algorithm
+ * ("spherical").
+ */
+ public static GeographyType crs84() {
+ return new GeographyType(DEFAULT_CRS, DEFAULT_ALGORITHM);
+ }
+
+ /**
+ * @param crs The coordinate reference system, e.g. "OGC:CRS84" or
"EPSG:4326".
+ * @param algorithm The edge-interpolation algorithm: one of "spherical",
"vincenty", "thomas",
+ * "andoyer" or "karney".
+ * @return A {@link GeographyType} with the given CRS and algorithm.
+ */
+ public static GeographyType of(String crs, String algorithm) {
+ return new GeographyType(crs, algorithm);
+ }
+
+ private final String crs;
+ private final String algorithm;
+
+ private GeographyType(String crs, String algorithm) {
+ Preconditions.checkArgument(crs != null && !crs.isEmpty(), "crs cannot
be null or empty");
+ Preconditions.checkArgument(
+ algorithm != null &&
VALID_ALGORITHMS.contains(algorithm.toLowerCase(Locale.ROOT)),
+ "algorithm must be one of %s, but got: %s",
+ VALID_ALGORITHMS,
+ algorithm);
+ this.crs = crs;
+ this.algorithm = algorithm.toLowerCase(Locale.ROOT);
+ }
+
+ /**
+ * @return The coordinate reference system of this geography type.
+ */
+ public String crs() {
+ return crs;
+ }
+
+ /**
+ * @return The edge-interpolation algorithm of this geography type.
+ */
+ public String algorithm() {
+ return algorithm;
+ }
+
+ @Override
+ public Name name() {
+ return Name.GEOGRAPHY;
+ }
+
+ @Override
+ public String simpleString() {
+ return DEFAULT_CRS.equalsIgnoreCase(crs) &&
DEFAULT_ALGORITHM.equals(algorithm)
+ ? "geography"
+ : String.format("geography(%s,%s)", crs, algorithm);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof GeographyType)) {
+ return false;
+ }
+ GeographyType that = (GeographyType) o;
+ return crs.equalsIgnoreCase(that.crs) &&
algorithm.equals(that.algorithm);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(crs.toLowerCase(Locale.ROOT), algorithm);
+ }
+ }
+
/**
* Fixed-length byte array type, if you want to use variable-length byte
array, use {@link
* BinaryType} instead.
diff --git a/api/src/test/java/org/apache/gravitino/rel/TestTypes.java
b/api/src/test/java/org/apache/gravitino/rel/TestTypes.java
index 8b58751721..4d2b66d3ae 100644
--- a/api/src/test/java/org/apache/gravitino/rel/TestTypes.java
+++ b/api/src/test/java/org/apache/gravitino/rel/TestTypes.java
@@ -159,6 +159,32 @@ public class TestTypes {
Assertions.assertSame(variantType, Types.VariantType.get());
Assertions.assertEquals("variant", variantType.simpleString());
+ Types.GeometryType geometryType = Types.GeometryType.crs84();
+ Assertions.assertEquals(Type.Name.GEOMETRY, geometryType.name());
+ Assertions.assertEquals("OGC:CRS84", geometryType.crs());
+ Assertions.assertEquals("geometry", geometryType.simpleString());
+ Assertions.assertEquals(geometryType, Types.GeometryType.of("ogc:crs84"));
+ Assertions.assertEquals("geometry",
Types.GeometryType.of("ogc:crs84").simpleString());
+ Types.GeometryType customCrs = Types.GeometryType.of("srid:3857");
+ Assertions.assertEquals("geometry(srid:3857)", customCrs.simpleString());
+ Assertions.assertNotEquals(geometryType, customCrs);
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
Types.GeometryType.of(""));
+
+ Types.GeographyType geographyType = Types.GeographyType.crs84();
+ Assertions.assertEquals(Type.Name.GEOGRAPHY, geographyType.name());
+ Assertions.assertEquals("OGC:CRS84", geographyType.crs());
+ Assertions.assertEquals("spherical", geographyType.algorithm());
+ Assertions.assertEquals("geography", geographyType.simpleString());
+ Assertions.assertEquals(geographyType, Types.GeographyType.of("ogc:crs84",
"SPHERICAL"));
+ Types.GeographyType karney = Types.GeographyType.of("EPSG:4326", "Karney");
+ Assertions.assertEquals("karney", karney.algorithm());
+ Assertions.assertEquals("geography(EPSG:4326,karney)",
karney.simpleString());
+ Assertions.assertNotEquals(geographyType, karney);
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
Types.GeographyType.of("OGC:CRS84", "bogus"));
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () -> Types.GeographyType.of("",
"spherical"));
+
Types.FixedType fixedType = Types.FixedType.of(10);
Assertions.assertEquals(Type.Name.FIXED, fixedType.name());
Assertions.assertEquals(10, fixedType.length());
diff --git
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/converter/FromIcebergType.java
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/converter/FromIcebergType.java
index 02b8be1dbd..12bc00ed36 100644
---
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/converter/FromIcebergType.java
+++
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/converter/FromIcebergType.java
@@ -120,6 +120,14 @@ public class FromIcebergType extends
TypeUtil.SchemaVisitor<Type> {
Types.DecimalType decimal = (Types.DecimalType) primitive;
return org.apache.gravitino.rel.types.Types.DecimalType.of(
decimal.precision(), decimal.scale());
+ case GEOMETRY:
+ return org.apache.gravitino.rel.types.Types.GeometryType.of(
+ ((Types.GeometryType) primitive).crs());
+ case GEOGRAPHY:
+ Types.GeographyType geography = (Types.GeographyType) primitive;
+ // EdgeAlgorithm.toString() is the lowercase spec name, which
GeographyType requires.
+ return org.apache.gravitino.rel.types.Types.GeographyType.of(
+ geography.crs(), geography.algorithm().toString());
default:
return
org.apache.gravitino.rel.types.Types.ExternalType.of(primitive.typeId().name());
}
diff --git
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/converter/ToIcebergType.java
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/converter/ToIcebergType.java
index b41e29cf12..2b0ec5d4d0 100644
---
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/converter/ToIcebergType.java
+++
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/converter/ToIcebergType.java
@@ -20,6 +20,7 @@ package
org.apache.gravitino.catalog.lakehouse.iceberg.converter;
import com.google.common.collect.Lists;
import java.util.List;
+import org.apache.iceberg.types.EdgeAlgorithm;
import org.apache.iceberg.types.Type;
import org.apache.iceberg.types.Types;
@@ -164,6 +165,14 @@ public class ToIcebergType extends
ToIcebergTypeVisitor<Type> {
return Types.UUIDType.get();
} else if (primitive instanceof
org.apache.gravitino.rel.types.Types.VariantType) {
return Types.VariantType.get();
+ } else if (primitive instanceof
org.apache.gravitino.rel.types.Types.GeometryType) {
+ return Types.GeometryType.of(
+ ((org.apache.gravitino.rel.types.Types.GeometryType)
primitive).crs());
+ } else if (primitive instanceof
org.apache.gravitino.rel.types.Types.GeographyType) {
+ org.apache.gravitino.rel.types.Types.GeographyType geography =
+ (org.apache.gravitino.rel.types.Types.GeographyType) primitive;
+ // EdgeAlgorithm.fromName is case-insensitive; the Gravitino algorithm
is a validated name.
+ return Types.GeographyType.of(geography.crs(),
EdgeAlgorithm.fromName(geography.algorithm()));
}
throw new UnsupportedOperationException("Not a supported type: " +
primitive.toString());
}
diff --git
a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/converter/TestConvertUtil.java
b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/converter/TestConvertUtil.java
index 659f180a2c..99beaa52ce 100644
---
a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/converter/TestConvertUtil.java
+++
b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/converter/TestConvertUtil.java
@@ -36,6 +36,7 @@ import org.apache.gravitino.rel.expressions.sorts.SortOrder;
import org.apache.gravitino.rel.types.Types.ByteType;
import org.apache.gravitino.rel.types.Types.ShortType;
import org.apache.iceberg.Schema;
+import org.apache.iceberg.types.EdgeAlgorithm;
import org.apache.iceberg.types.Type;
import org.apache.iceberg.types.Types;
import org.junit.jupiter.api.Assertions;
@@ -54,6 +55,47 @@ public class TestConvertUtil extends TestBaseConvert {
instanceof Types.VariantType);
}
+ @Test
+ public void testGeometryType() {
+ // Iceberg V3 geometry <-> Gravitino GeometryType, preserving the CRS in
both directions.
+ org.apache.gravitino.rel.types.Type defaultFromIceberg =
+ CONVERTER.toGravitino(Types.GeometryType.crs84());
+ Assertions.assertInstanceOf(
+ org.apache.gravitino.rel.types.Types.GeometryType.class,
defaultFromIceberg);
+ Assertions.assertEquals(
+ "OGC:CRS84",
+ ((org.apache.gravitino.rel.types.Types.GeometryType)
defaultFromIceberg).crs());
+
+ org.apache.gravitino.rel.types.Types.GeometryType customCrs =
+ org.apache.gravitino.rel.types.Types.GeometryType.of("srid:3857");
+ org.apache.iceberg.types.Type icebergCustom =
CONVERTER.fromGravitino(customCrs);
+ Assertions.assertInstanceOf(Types.GeometryType.class, icebergCustom);
+ Assertions.assertEquals("srid:3857", ((Types.GeometryType)
icebergCustom).crs());
+ Assertions.assertEquals(customCrs, CONVERTER.toGravitino(icebergCustom));
+ }
+
+ @Test
+ public void testGeographyType() {
+ // Iceberg V3 geography <-> Gravitino GeographyType, preserving CRS and
algorithm both ways.
+ org.apache.gravitino.rel.types.Type defaultFromIceberg =
+ CONVERTER.toGravitino(Types.GeographyType.crs84());
+ Assertions.assertInstanceOf(
+ org.apache.gravitino.rel.types.Types.GeographyType.class,
defaultFromIceberg);
+ org.apache.gravitino.rel.types.Types.GeographyType defaultGeography =
+ (org.apache.gravitino.rel.types.Types.GeographyType)
defaultFromIceberg;
+ Assertions.assertEquals("OGC:CRS84", defaultGeography.crs());
+ Assertions.assertEquals("spherical", defaultGeography.algorithm());
+
+ org.apache.gravitino.rel.types.Types.GeographyType custom =
+ org.apache.gravitino.rel.types.Types.GeographyType.of("EPSG:4326",
"karney");
+ org.apache.iceberg.types.Type icebergCustom =
CONVERTER.fromGravitino(custom);
+ Assertions.assertInstanceOf(Types.GeographyType.class, icebergCustom);
+ Types.GeographyType icebergGeography = (Types.GeographyType) icebergCustom;
+ Assertions.assertEquals("EPSG:4326", icebergGeography.crs());
+ Assertions.assertEquals(EdgeAlgorithm.KARNEY,
icebergGeography.algorithm());
+ Assertions.assertEquals(custom, CONVERTER.toGravitino(icebergCustom));
+ }
+
@Test
public void testToIcebergSchema() {
Column[] columns = createColumns("col_1", "col_2", "col_3", "col_4");
diff --git
a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/integration/test/CatalogIcebergBaseIT.java
b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/integration/test/CatalogIcebergBaseIT.java
index eb568ae126..53badb768e 100644
---
a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/integration/test/CatalogIcebergBaseIT.java
+++
b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/integration/test/CatalogIcebergBaseIT.java
@@ -646,10 +646,18 @@ public abstract class CatalogIcebergBaseIT extends BaseIT
{
// like Spark 4 would, directly through the Iceberg catalog, then load
through the native
// metadata interface.
//
- // variant has native support and loads as VariantType. The other V3
net-new types are not
- // modeled in Gravitino's unified type system yet and load as
ExternalType; native support for
- // them is pending and tracked in apache/gravitino#11929.
+ // variant loads as VariantType; geometry and geography load as their
native types. The
+ // remaining V3 net-new types are not modeled in Gravitino's unified type
system yet and load
+ // as ExternalType; native support for them is pending and tracked in
apache/gravitino#11929.
assertV3LoadsAsVariant("v3_variant");
+ assertV3LoadsAsGeometry("v3_geometry",
org.apache.iceberg.types.Types.GeometryType.crs84());
+ assertV3LoadsAsGeometry(
+ "v3_geometry_srid",
org.apache.iceberg.types.Types.GeometryType.of("srid:3857"));
+ assertV3LoadsAsGeography("v3_geography",
org.apache.iceberg.types.Types.GeographyType.crs84());
+ assertV3LoadsAsGeography(
+ "v3_geography_karney",
+ org.apache.iceberg.types.Types.GeographyType.of(
+ "EPSG:4326", org.apache.iceberg.types.EdgeAlgorithm.KARNEY));
// TODO(apache/gravitino#11929): expect native types once these gain
unified-model support.
assertV3LoadsAsExternal(
@@ -660,10 +668,6 @@ public abstract class CatalogIcebergBaseIT extends BaseIT {
"v3_timestamptz_ns",
org.apache.iceberg.types.Types.TimestampNanoType.withZone(),
"TIMESTAMP_NANO");
- assertV3LoadsAsExternal(
- "v3_geometry", org.apache.iceberg.types.Types.GeometryType.crs84(),
"GEOMETRY");
- assertV3LoadsAsExternal(
- "v3_geography", org.apache.iceberg.types.Types.GeographyType.crs84(),
"GEOGRAPHY");
assertV3LoadsAsExternal(
"v3_unknown", org.apache.iceberg.types.Types.UnknownType.get(),
"UNKNOWN");
}
@@ -730,6 +734,89 @@ public abstract class CatalogIcebergBaseIT extends BaseIT {
"3",
loaded.properties().get(IcebergTablePropertiesMetadata.FORMAT_VERSION));
}
+ @Test
+ void testCreateGeometryColumnWriteRoundTrip() {
+ // Write path: create a geometry column with a non-default CRS *through
the Gravitino relational
+ // API*, write it to the REST (IRC) backend, then load it back and confirm
both the type and the
+ // CRS round-trip. REST-backend only, for the same reason as
+ // testV3TypeConversionViaIcebergClient: the CI Hive metastore cannot
store V3 column types.
+ Assumptions.assumeTrue(
+ "rest".equalsIgnoreCase(TYPE),
+ "Geometry columns require a backend that does not validate against the
Hive metastore");
+
+ NameIdentifier ident = NameIdentifier.of(schemaName, "t_geometry_write");
+ Column[] columns =
+ new Column[] {
+ Column.of("id", Types.IntegerType.get(), "id"),
+ Column.of("shape", Types.GeometryType.of("srid:3857"), "geometry
col")
+ };
+ Map<String, String> properties = Maps.newHashMap();
+ // Geometry is a V3 type, so the table must be created at format-version 3.
+ properties.put(IcebergTablePropertiesMetadata.FORMAT_VERSION, "3");
+
+ TableCatalog tableCatalog = catalog.asTableCatalog();
+ Table created =
+ tableCatalog.createTable(
+ ident,
+ columns,
+ "geometry write",
+ properties,
+ Transforms.EMPTY_TRANSFORM,
+ Distributions.NONE,
+ new SortOrder[0]);
+ Assertions.assertInstanceOf(Types.GeometryType.class,
created.columns()[1].dataType());
+
+ // Load it back through the native metadata API to confirm the round-trip
against the backend.
+ Table loaded = tableCatalog.loadTable(ident);
+ Assertions.assertInstanceOf(Types.GeometryType.class,
loaded.columns()[1].dataType());
+ Assertions.assertEquals(
+ "srid:3857", ((Types.GeometryType)
loaded.columns()[1].dataType()).crs());
+ Assertions.assertEquals(
+ "3",
loaded.properties().get(IcebergTablePropertiesMetadata.FORMAT_VERSION));
+ }
+
+ @Test
+ void testCreateGeographyColumnWriteRoundTrip() {
+ // Write path: create a geography column with a non-default CRS and edge
algorithm *through the
+ // Gravitino relational API*, write it to the REST (IRC) backend, then
load it back and confirm
+ // the type, CRS and algorithm round-trip. REST-backend only, for the same
reason as
+ // testV3TypeConversionViaIcebergClient: the CI Hive metastore cannot
store V3 column types.
+ Assumptions.assumeTrue(
+ "rest".equalsIgnoreCase(TYPE),
+ "Geography columns require a backend that does not validate against
the Hive metastore");
+
+ NameIdentifier ident = NameIdentifier.of(schemaName, "t_geography_write");
+ Column[] columns =
+ new Column[] {
+ Column.of("id", Types.IntegerType.get(), "id"),
+ Column.of("shape", Types.GeographyType.of("EPSG:4326", "karney"),
"geography col")
+ };
+ Map<String, String> properties = Maps.newHashMap();
+ // Geography is a V3 type, so the table must be created at format-version
3.
+ properties.put(IcebergTablePropertiesMetadata.FORMAT_VERSION, "3");
+
+ TableCatalog tableCatalog = catalog.asTableCatalog();
+ Table created =
+ tableCatalog.createTable(
+ ident,
+ columns,
+ "geography write",
+ properties,
+ Transforms.EMPTY_TRANSFORM,
+ Distributions.NONE,
+ new SortOrder[0]);
+ Assertions.assertInstanceOf(Types.GeographyType.class,
created.columns()[1].dataType());
+
+ // Load it back through the native metadata API to confirm the round-trip
against the backend.
+ Table loaded = tableCatalog.loadTable(ident);
+ Assertions.assertInstanceOf(Types.GeographyType.class,
loaded.columns()[1].dataType());
+ Types.GeographyType geography = (Types.GeographyType)
loaded.columns()[1].dataType();
+ Assertions.assertEquals("EPSG:4326", geography.crs());
+ Assertions.assertEquals("karney", geography.algorithm());
+ Assertions.assertEquals(
+ "3",
loaded.properties().get(IcebergTablePropertiesMetadata.FORMAT_VERSION));
+ }
+
@Test
void testCreateVariantColumnRequiresFormatVersion3() {
Assumptions.assumeTrue(
@@ -825,6 +912,24 @@ public abstract class CatalogIcebergBaseIT extends BaseIT {
Assertions.assertInstanceOf(Types.VariantType.class, loaded.dataType());
}
+ private void assertV3LoadsAsGeometry(
+ String tableName, org.apache.iceberg.types.Types.GeometryType
icebergType) {
+ NameIdentifier ident = createV3Table(tableName, icebergType);
+ Column loaded = catalog.asTableCatalog().loadTable(ident).columns()[0];
+ Assertions.assertInstanceOf(Types.GeometryType.class, loaded.dataType());
+ Assertions.assertEquals(icebergType.crs(), ((Types.GeometryType)
loaded.dataType()).crs());
+ }
+
+ private void assertV3LoadsAsGeography(
+ String tableName, org.apache.iceberg.types.Types.GeographyType
icebergType) {
+ NameIdentifier ident = createV3Table(tableName, icebergType);
+ Column loaded = catalog.asTableCatalog().loadTable(ident).columns()[0];
+ Assertions.assertInstanceOf(Types.GeographyType.class, loaded.dataType());
+ Types.GeographyType geography = (Types.GeographyType) loaded.dataType();
+ Assertions.assertEquals(icebergType.crs(), geography.crs());
+ Assertions.assertEquals(icebergType.algorithm().toString(),
geography.algorithm());
+ }
+
private void assertV3LoadsAsExternal(
String tableName, org.apache.iceberg.types.Type icebergType, String
expectedCatalogString) {
NameIdentifier ident = createV3Table(tableName, icebergType);
diff --git
a/clients/client-python/gravitino/api/rel/types/json_serdes/_helper/serdes_utils.py
b/clients/client-python/gravitino/api/rel/types/json_serdes/_helper/serdes_utils.py
index 25656fa0e4..cdc60abb59 100644
---
a/clients/client-python/gravitino/api/rel/types/json_serdes/_helper/serdes_utils.py
+++
b/clients/client-python/gravitino/api/rel/types/json_serdes/_helper/serdes_utils.py
@@ -157,7 +157,7 @@ class SerdesUtils(SerdesUtilsBase):
)
if isinstance(type_data, str):
- return cls.from_primitive_type_string(type_data.lower())
+ return cls.from_primitive_type_string(type_data.lower(), type_data)
if isinstance(type_data, dict) and type_data.get(cls.TYPE):
type_str = type_data[cls.TYPE]
@@ -177,11 +177,31 @@ class SerdesUtils(SerdesUtilsBase):
return Types.UnparsedType.of(unparsed_type=json.dumps(type_data))
@classmethod
- def from_primitive_type_string(cls, type_string: str) -> Type:
+ def from_primitive_type_string( # pylint:
disable=too-many-return-statements
+ cls, type_string: str, original_type_string: str = None
+ ) -> Type:
+ # Flat type dispatch: one return per supported type, no branching
logic — the return
+ # count reflects the number of types, not complexity. A simple
converter function.
type_instance = cls.TYPES.get(type_string)
if type_instance is not None:
return type_instance
+ # Match against the original (non-lowercased) string so the CRS keeps
its case.
+ # fullmatch (not match) so a trailing suffix can't partially parse,
matching the
+ # Java side's Matcher.matches().
+ original = (
+ original_type_string if original_type_string is not None else
type_string
+ )
+ geometry_matched = cls.GEOMETRY_PATTERN.fullmatch(original)
+ if geometry_matched:
+ return Types.GeometryType.of(geometry_matched.group(1))
+
+ geography_matched = cls.GEOGRAPHY_PATTERN.fullmatch(original)
+ if geography_matched:
+ return Types.GeographyType.of(
+ geography_matched.group(1), geography_matched.group(2)
+ )
+
decimal_matched = cls.DECIMAL_PATTERN.match(type_string)
if decimal_matched:
return Types.DecimalType.of(
diff --git a/clients/client-python/gravitino/api/rel/types/type.py
b/clients/client-python/gravitino/api/rel/types/type.py
index 7cf9f921d8..949bf5378d 100644
--- a/clients/client-python/gravitino/api/rel/types/type.py
+++ b/clients/client-python/gravitino/api/rel/types/type.py
@@ -85,6 +85,14 @@ class Name(Enum):
""" The variant type. A variant holds semi-structured data whose shape is
not fixed by the
schema. """
+ GEOMETRY = "GEOMETRY"
+ """ The geometry type. A geometry holds a geospatial shape (WKB-encoded)
on a planar coordinate
+ reference system. """
+
+ GEOGRAPHY = "GEOGRAPHY"
+ """ The geography type. A geography holds a geospatial shape (WKB-encoded)
on a spheroidal
+ coordinate reference system, with an edge-interpolation algorithm. """
+
STRUCT = "STRUCT"
"""
The struct type.
diff --git a/clients/client-python/gravitino/api/rel/types/types.py
b/clients/client-python/gravitino/api/rel/types/types.py
index 622c4ac8cc..35165daedc 100644
--- a/clients/client-python/gravitino/api/rel/types/types.py
+++ b/clients/client-python/gravitino/api/rel/types/types.py
@@ -728,6 +728,135 @@ class Types:
def simple_string(self) -> str:
return "variant"
+ class GeometryType(PrimitiveType):
+ """The geometry type in Gravitino. A geometry column holds a
geospatial shape
+ (WKB-encoded) on a planar coordinate reference system (CRS). The CRS
is carried as type
+ metadata."""
+
+ DEFAULT_CRS: str = "OGC:CRS84"
+
+ _crs: str
+
+ def __init__(self, crs: str):
+ if not crs:
+ raise ValueError("crs cannot be null or empty")
+ self._crs = crs
+
+ @classmethod
+ def crs84(cls) -> Types.GeometryType:
+ """
+ Returns:
+ A GeometryType instance with the default CRS ("OGC:CRS84").
+ """
+ return cls(cls.DEFAULT_CRS)
+
+ @classmethod
+ def of(cls, crs: str) -> Types.GeometryType:
+ """
+ Args:
+ crs: The coordinate reference system, e.g. "OGC:CRS84",
"EPSG:4326" or "srid:3857".
+
+ Returns:
+ A GeometryType instance with the given CRS.
+ """
+ return cls(crs)
+
+ def name(self) -> Name:
+ return Name.GEOMETRY
+
+ def crs(self) -> str:
+ return self._crs
+
+ def simple_string(self) -> str:
+ # The default CRS elides to a bare "geometry"; a custom CRS is
carried verbatim.
+ if self._crs.lower() == self.DEFAULT_CRS.lower():
+ return "geometry"
+ return f"geometry({self._crs})"
+
+ def __eq__(self, other):
+ if not isinstance(other, Types.GeometryType):
+ return False
+ return self._crs.lower() == other._crs.lower()
+
+ def __hash__(self):
+ return hash(self._crs.lower())
+
+ class GeographyType(PrimitiveType):
+ """The geography type in Gravitino. A geography column holds a
geospatial shape
+ (WKB-encoded) on a spheroidal coordinate reference system (CRS), with
an edge-interpolation
+ algorithm. Both are carried as type metadata."""
+
+ DEFAULT_CRS: str = "OGC:CRS84"
+ DEFAULT_ALGORITHM: str = "spherical"
+ _VALID_ALGORITHMS = frozenset(
+ {"spherical", "vincenty", "thomas", "andoyer", "karney"}
+ )
+
+ _crs: str
+ _algorithm: str
+
+ def __init__(self, crs: str, algorithm: str):
+ if not crs:
+ raise ValueError("crs cannot be null or empty")
+ if algorithm is None or algorithm.lower() not in
self._VALID_ALGORITHMS:
+ raise ValueError(
+ f"algorithm must be one of
{sorted(self._VALID_ALGORITHMS)}, "
+ f"but got: {algorithm}"
+ )
+ self._crs = crs
+ self._algorithm = algorithm.lower()
+
+ @classmethod
+ def crs84(cls) -> Types.GeographyType:
+ """
+ Returns:
+ A GeographyType instance with the default CRS ("OGC:CRS84")
and algorithm
+ ("spherical").
+ """
+ return cls(cls.DEFAULT_CRS, cls.DEFAULT_ALGORITHM)
+
+ @classmethod
+ def of(cls, crs: str, algorithm: str) -> Types.GeographyType:
+ """
+ Args:
+ crs: The coordinate reference system, e.g. "OGC:CRS84" or
"EPSG:4326".
+ algorithm: The edge-interpolation algorithm: one of
"spherical", "vincenty",
+ "thomas", "andoyer" or "karney".
+
+ Returns:
+ A GeographyType instance with the given CRS and algorithm.
+ """
+ return cls(crs, algorithm)
+
+ def name(self) -> Name:
+ return Name.GEOGRAPHY
+
+ def crs(self) -> str:
+ return self._crs
+
+ def algorithm(self) -> str:
+ return self._algorithm
+
+ def simple_string(self) -> str:
+ # The default CRS and algorithm elide to a bare "geography";
otherwise both are carried.
+ if (
+ self._crs.lower() == self.DEFAULT_CRS.lower()
+ and self._algorithm == self.DEFAULT_ALGORITHM
+ ):
+ return "geography"
+ return f"geography({self._crs},{self._algorithm})"
+
+ def __eq__(self, other):
+ if not isinstance(other, Types.GeographyType):
+ return False
+ return (
+ self._crs.lower() == other._crs.lower()
+ and self._algorithm == other._algorithm
+ )
+
+ def __hash__(self):
+ return hash((self._crs.lower(), self._algorithm))
+
class StructType(ComplexType):
"""The struct type in Gravitino."""
diff --git a/clients/client-python/gravitino/utils/serdes.py
b/clients/client-python/gravitino/utils/serdes.py
index fa5fbe3c80..3130712dfd 100644
--- a/clients/client-python/gravitino/utils/serdes.py
+++ b/clients/client-python/gravitino/utils/serdes.py
@@ -92,6 +92,14 @@ class SerdesUtilsBase:
TIMESTAMP_TZ_PATTERN: Final[Pattern[str]] = re.compile(
r"timestamp_tz\(\s*(\d+)\s*\)"
)
+ # The keyword folds case; group 1 is the CRS, an opaque identifier kept
as-is.
+ GEOMETRY_PATTERN: Final[Pattern[str]] = re.compile(
+ r"geometry\(\s*(.+?)\s*\)", re.IGNORECASE
+ )
+ # Group 1 is the CRS (kept as-is); group 2 is the edge-interpolation
algorithm.
+ GEOGRAPHY_PATTERN: Final[Pattern[str]] = re.compile(
+ r"geography\(\s*(.+?)\s*,\s*(.+?)\s*\)", re.IGNORECASE
+ )
TYPES: Final[Mapping] = MappingProxyType(
{
type_instance.simple_string(): type_instance
@@ -117,6 +125,8 @@ class SerdesUtilsBase:
Types.StringType.get(),
Types.UUIDType.get(),
Types.VariantType.get(),
+ Types.GeometryType.crs84(),
+ Types.GeographyType.crs84(),
)
}
)
diff --git a/clients/client-python/tests/unittests/api/rel/test_types.py
b/clients/client-python/tests/unittests/api/rel/test_types.py
index 4cf0d41cc9..cfa78390f9 100644
--- a/clients/client-python/tests/unittests/api/rel/test_types.py
+++ b/clients/client-python/tests/unittests/api/rel/test_types.py
@@ -188,6 +188,32 @@ class TestTypes(unittest.TestCase):
self.assertEqual(instance.simple_string(), "variant")
self.assertIs(instance, Types.VariantType.get())
+ def test_geometry_type(self):
+ instance: Types.GeometryType = Types.GeometryType.crs84()
+ self.assertEqual(instance.name(), Name.GEOMETRY)
+ self.assertEqual(instance.crs(), "OGC:CRS84")
+ self.assertEqual(instance.simple_string(), "geometry")
+ self.assertEqual(instance, Types.GeometryType.of("ogc:crs84"))
+ self.assertEqual(Types.GeometryType.of("ogc:crs84").simple_string(),
"geometry")
+ custom = Types.GeometryType.of("srid:3857")
+ self.assertEqual(custom.simple_string(), "geometry(srid:3857)")
+ self.assertNotEqual(instance, custom)
+ self.assertRaises(ValueError, Types.GeometryType.of, "")
+
+ def test_geography_type(self):
+ instance: Types.GeographyType = Types.GeographyType.crs84()
+ self.assertEqual(instance.name(), Name.GEOGRAPHY)
+ self.assertEqual(instance.crs(), "OGC:CRS84")
+ self.assertEqual(instance.algorithm(), "spherical")
+ self.assertEqual(instance.simple_string(), "geography")
+ self.assertEqual(instance, Types.GeographyType.of("ogc:crs84",
"SPHERICAL"))
+ karney = Types.GeographyType.of("EPSG:4326", "Karney")
+ self.assertEqual(karney.algorithm(), "karney")
+ self.assertEqual(karney.simple_string(), "geography(EPSG:4326,karney)")
+ self.assertNotEqual(instance, karney)
+ self.assertRaises(ValueError, Types.GeographyType.of, "OGC:CRS84",
"bogus")
+ self.assertRaises(ValueError, Types.GeographyType.of, "", "spherical")
+
def test_fixed_type(self):
instance: Types.FixedType = Types.FixedType.of(5)
self.assertEqual(instance.name(), Name.FIXED)
diff --git
a/clients/client-python/tests/unittests/json_serdes/test_type_serdes.py
b/clients/client-python/tests/unittests/json_serdes/test_type_serdes.py
index f20e55cb9d..4be159e04f 100644
--- a/clients/client-python/tests/unittests/json_serdes/test_type_serdes.py
+++ b/clients/client-python/tests/unittests/json_serdes/test_type_serdes.py
@@ -65,6 +65,10 @@ class TestTypeSerdes(unittest.TestCase):
"timestamp(0)": Types.TimestampType.without_time_zone(0),
"timestamp_tz(6)": Types.TimestampType.with_time_zone(6),
"timestamp_tz(3)": Types.TimestampType.with_time_zone(3),
+ "geometry(srid:3857)": Types.GeometryType.of("srid:3857"),
+ "geography(EPSG:4326,karney)": Types.GeographyType.of(
+ "EPSG:4326", "karney"
+ ),
},
}
@@ -191,6 +195,9 @@ class TestTypeSerdes(unittest.TestCase):
{"invalid_key": "value"},
list(range(10)),
True,
+ # A valid-looking prefix with trailing characters must not
partially parse.
+ "geometry(EPSG:4326) trailing",
+ "geography(OGC:CRS84,spherical) trailing",
]
for data in unparsed_data:
result = TypeSerdes.deserialize(data=data)
diff --git a/common/src/main/java/org/apache/gravitino/json/JsonUtils.java
b/common/src/main/java/org/apache/gravitino/json/JsonUtils.java
index 5c87d28566..ad71feb557 100644
--- a/common/src/main/java/org/apache/gravitino/json/JsonUtils.java
+++ b/common/src/main/java/org/apache/gravitino/json/JsonUtils.java
@@ -171,6 +171,8 @@ public class JsonUtils {
Types.StringType.get(),
Types.UUIDType.get(),
Types.VariantType.get(),
+ Types.GeometryType.crs84(),
+ Types.GeographyType.crs84(),
Types.BinaryType.get(),
Types.IntervalYearType.get(),
Types.IntervalDayType.get()),
@@ -180,6 +182,10 @@ public class JsonUtils {
private static final Pattern VARCHAR =
Pattern.compile("varchar\\(\\s*(\\d+)\\s*\\)");
private static final Pattern DECIMAL =
Pattern.compile("decimal\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)");
+ private static final Pattern GEOMETRY =
+ Pattern.compile("geometry\\(\\s*(.+?)\\s*\\)", Pattern.CASE_INSENSITIVE);
+ private static final Pattern GEOGRAPHY =
+ Pattern.compile("geography\\(\\s*(.+?)\\s*,\\s*(.+?)\\s*\\)",
Pattern.CASE_INSENSITIVE);
private static final Pattern TIME = Pattern.compile("time\\((\\d+)\\)");
private static final Pattern TIMESTAMP_TZ =
Pattern.compile("timestamp_tz\\((\\d+)\\)");
private static final Pattern TIMESTAMP =
Pattern.compile("timestamp\\((\\d+)\\)");
@@ -682,6 +688,8 @@ public class JsonUtils {
case INTERVAL_YEAR:
case UUID:
case VARIANT:
+ case GEOMETRY:
+ case GEOGRAPHY:
case FIXED:
case BINARY:
case NULL:
@@ -871,6 +879,17 @@ public class JsonUtils {
Integer.parseInt(decimal.group(1)),
Integer.parseInt(decimal.group(2)));
}
+ // Match against the original string, not the lowercased one, so the CRS
keeps its case.
+ Matcher geometry = GEOMETRY.matcher(orignalTypeString);
+ if (geometry.matches()) {
+ return Types.GeometryType.of(geometry.group(1));
+ }
+
+ Matcher geography = GEOGRAPHY.matcher(orignalTypeString);
+ if (geography.matches()) {
+ return Types.GeographyType.of(geography.group(1), geography.group(2));
+ }
+
Matcher time = TIME.matcher(lowerTypeString);
if (time.matches()) {
return Types.TimeType.of(Integer.parseInt(time.group(1)));
diff --git a/common/src/test/java/org/apache/gravitino/json/TestJsonUtils.java
b/common/src/test/java/org/apache/gravitino/json/TestJsonUtils.java
index da92570e1d..84497a0505 100644
--- a/common/src/test/java/org/apache/gravitino/json/TestJsonUtils.java
+++ b/common/src/test/java/org/apache/gravitino/json/TestJsonUtils.java
@@ -126,6 +126,39 @@ public class TestJsonUtils {
Assertions.assertEquals(
Types.VariantType.get(), JsonUtils.objectMapper().readValue(jsonValue,
Type.class));
+ type = Types.GeometryType.crs84();
+ jsonValue = JsonUtils.objectMapper().writeValueAsString(type);
+ expected = "\"geometry\"";
+ Assertions.assertEquals(objectMapper.readTree(expected),
objectMapper.readTree(jsonValue));
+ Assertions.assertEquals(
+ Types.GeometryType.crs84(),
JsonUtils.objectMapper().readValue(jsonValue, Type.class));
+
+ // A custom, case-sensitive CRS round-trips verbatim (it is not lowercased
on parse).
+ type = Types.GeometryType.of("EPSG:4326");
+ jsonValue = JsonUtils.objectMapper().writeValueAsString(type);
+ expected = "\"geometry(EPSG:4326)\"";
+ Assertions.assertEquals(objectMapper.readTree(expected),
objectMapper.readTree(jsonValue));
+ Types.GeometryType parsedGeometry =
+ (Types.GeometryType) JsonUtils.objectMapper().readValue(jsonValue,
Type.class);
+ Assertions.assertEquals("EPSG:4326", parsedGeometry.crs());
+
+ type = Types.GeographyType.crs84();
+ jsonValue = JsonUtils.objectMapper().writeValueAsString(type);
+ expected = "\"geography\"";
+ Assertions.assertEquals(objectMapper.readTree(expected),
objectMapper.readTree(jsonValue));
+ Assertions.assertEquals(
+ Types.GeographyType.crs84(),
JsonUtils.objectMapper().readValue(jsonValue, Type.class));
+
+ // A custom CRS + algorithm round-trips, with the case-sensitive CRS
preserved verbatim.
+ type = Types.GeographyType.of("EPSG:4326", "karney");
+ jsonValue = JsonUtils.objectMapper().writeValueAsString(type);
+ expected = "\"geography(EPSG:4326,karney)\"";
+ Assertions.assertEquals(objectMapper.readTree(expected),
objectMapper.readTree(jsonValue));
+ Types.GeographyType parsedGeography =
+ (Types.GeographyType) JsonUtils.objectMapper().readValue(jsonValue,
Type.class);
+ Assertions.assertEquals("EPSG:4326", parsedGeography.crs());
+ Assertions.assertEquals("karney", parsedGeography.algorithm());
+
type =
Types.StructType.of(
Types.StructType.Field.nullableField("name",
Types.StringType.get(), "name field"),
diff --git a/docs/lakehouse-iceberg-catalog.md
b/docs/lakehouse-iceberg-catalog.md
index df9a44aaf3..b71fb65784 100644
--- a/docs/lakehouse-iceberg-catalog.md
+++ b/docs/lakehouse-iceberg-catalog.md
@@ -435,6 +435,8 @@ If you doesn't specify distribution expressions, the table
distribution will be
| `Binary` | `Binary` |
| `UUID` | `UUID` |
| `Variant` | `Variant` |
+| `Geometry` | `Geometry` |
+| `Geography` | `Geography` |
:::info
Apache Iceberg doesn't support Gravitino `Varchar` `Fixedchar` `Byte` `Short`
`Union` type.
diff --git a/docs/manage-relational-metadata-using-gravitino.md
b/docs/manage-relational-metadata-using-gravitino.md
index cfda9dc231..ebdace5c19 100644
--- a/docs/manage-relational-metadata-using-gravitino.md
+++ b/docs/manage-relational-metadata-using-gravitino.md
@@ -931,6 +931,8 @@ The following types that Gravitino supports:
| Union | `Types.UnionType.of([type1, type2, ...])`
| `{"type": "union", "types": [type JSON, ...]}`
| Union type, indicates a union of types
|
| UUID | `Types.UUIDType.get()`
| `uuid`
| UUID type, indicates a universally unique identifier
|
| Variant | `Types.VariantType.get()`
| `variant`
| Variant type, indicates semi-structured data whose shape is not fixed by the
schema
|
+| Geometry | `Types.GeometryType.of(crs)`
| `geometry` or `geometry(<crs>)`
| Geometry type, indicates a geospatial shape (WKB-encoded) on a planar
coordinate reference system (CRS), default `OGC:CRS84`
|
+| Geography | `Types.GeographyType.of(crs, algorithm)`
| `geography` or `geography(<crs>,<algorithm>)`
| Geography type, indicates a geospatial shape (WKB-encoded) on a spheroidal
CRS (default `OGC:CRS84`) with an edge-interpolation algorithm (default
`spherical`) |
The related java doc is
[here](pathname:///docs/2.0.0-SNAPSHOT/api/java/org/apache/gravitino/rel/types/Type.html).
diff --git a/docs/open-api/datatype.yaml b/docs/open-api/datatype.yaml
index a244aee188..0cc75b5e19 100644
--- a/docs/open-api/datatype.yaml
+++ b/docs/open-api/datatype.yaml
@@ -56,6 +56,10 @@ components:
- "fixed(16)"
- "binary"
- "variant"
+ - "geometry"
+ - "geometry(srid:3857)"
+ - "geography"
+ - "geography(EPSG:4326,karney)"
UnionType:
type: object