You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by ji...@apache.org on 2026/07/28 03:36:27 UTC
(sedona) branch master updated: [GH-3179] Add Geography support for ST_X and ST_Y (#3188)
This is an automated email from the ASF dual-hosted git repository.
jiayuasu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sedona.git
The following commit(s) were added to refs/heads/master by this push:
new af9acce09a8 [GH-3179] Add Geography support for ST_X and ST_Y (#3188)
af9acce09a8 is described below
commit af9acce09a8937b7f0a1987adfb367a6f6433891
Author: Jia Yu <ji...@apache.org>
AuthorDate: Mon Jul 27 20:36:22 2026 -0700
[GH-3179] Add Geography support for ST_X and ST_Y (#3188)
---
.../sedona/common/S2Geography/WKBGeography.java | 28 +++++++++++++++
.../apache/sedona/common/geography/Functions.java | 14 ++++++++
.../sedona/common/Geography/FunctionTest.java | 19 ++++++++++
.../common/S2Geography/WKBGeographyTest.java | 8 +++++
docs/api/sql/geography/Geography-Functions.md | 2 ++
docs/api/sql/geography/Geography-Functions/ST_X.md | 42 ++++++++++++++++++++++
docs/api/sql/geography/Geography-Functions/ST_Y.md | 42 ++++++++++++++++++++++
.../sql/sedona_sql/expressions/Functions.scala | 8 +++--
.../sql/geography/GeographyFunctionTest.scala | 24 +++++++++++++
9 files changed, 185 insertions(+), 2 deletions(-)
diff --git a/common/src/main/java/org/apache/sedona/common/S2Geography/WKBGeography.java b/common/src/main/java/org/apache/sedona/common/S2Geography/WKBGeography.java
index 2b4adec8a23..b730ba52f69 100644
--- a/common/src/main/java/org/apache/sedona/common/S2Geography/WKBGeography.java
+++ b/common/src/main/java/org/apache/sedona/common/S2Geography/WKBGeography.java
@@ -245,6 +245,34 @@ public class WKBGeography extends Geography {
return wkbBaseType() == 1;
}
+ /** Returns the X coordinate directly from Point WKB, or null for non-points and empty points. */
+ public Double getPointX() {
+ return getPointOrdinate(0);
+ }
+
+ /** Returns the Y coordinate directly from Point WKB, or null for non-points and empty points. */
+ public Double getPointY() {
+ return getPointOrdinate(1);
+ }
+
+ /**
+ * Reads X or Y directly from the WKB payload. Z/M ordinates are intentionally tolerated and
+ * ignored so these accessors match Geometry ST_X/ST_Y behavior for higher-dimensional points.
+ */
+ private Double getPointOrdinate(int ordinate) {
+ if (!isPoint()) return null;
+
+ boolean le = (wkbBytes[0] == 0x01);
+ int coordOffset = wkbPayloadOffset();
+ ByteBuffer bb =
+ ByteBuffer.wrap(wkbBytes).order(le ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
+ double x = bb.getDouble(coordOffset);
+ double y = bb.getDouble(coordOffset + Double.BYTES);
+ // JTS treats a point as empty when either X or Y is NaN.
+ if (Double.isNaN(x) || Double.isNaN(y)) return null;
+ return ordinate == 0 ? x : y;
+ }
+
/** Extract the S2Point from a Point WKB without full S2 parse. */
public S2Point extractPoint() {
requireXYOnly();
diff --git a/common/src/main/java/org/apache/sedona/common/geography/Functions.java b/common/src/main/java/org/apache/sedona/common/geography/Functions.java
index 5d484afc4b6..93a88bc7ae8 100644
--- a/common/src/main/java/org/apache/sedona/common/geography/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/geography/Functions.java
@@ -183,6 +183,20 @@ public class Functions {
return toJTS(g).toText();
}
+ /** Return the longitude (X coordinate) of a point geography, or null for non-point inputs. */
+ public static Double x(Geography g) {
+ if (g == null) return null;
+ if (g instanceof WKBGeography) return ((WKBGeography) g).getPointX();
+ return org.apache.sedona.common.Functions.x(toJTS(g));
+ }
+
+ /** Return the latitude (Y coordinate) of a point geography, or null for non-point inputs. */
+ public static Double y(Geography g) {
+ if (g == null) return null;
+ if (g instanceof WKBGeography) return ((WKBGeography) g).getPointY();
+ return org.apache.sedona.common.Functions.y(toJTS(g));
+ }
+
// ─── Level 2: Geodesic metrics ───────────────────────────────────────────
/**
diff --git a/common/src/test/java/org/apache/sedona/common/Geography/FunctionTest.java b/common/src/test/java/org/apache/sedona/common/Geography/FunctionTest.java
index b70bb6f814e..3f0432b5758 100644
--- a/common/src/test/java/org/apache/sedona/common/Geography/FunctionTest.java
+++ b/common/src/test/java/org/apache/sedona/common/Geography/FunctionTest.java
@@ -148,6 +148,25 @@ public class FunctionTest {
assertEquals(5, Functions.nPoints(g));
}
+ @Test
+ public void xY_point() throws ParseException {
+ Geography g = Constructors.geogFromWKT("POINT (-73.9857 40.7484)", 4326);
+ assertEquals(-73.9857, Functions.x(g), 1e-12);
+ assertEquals(40.7484, Functions.y(g), 1e-12);
+ }
+
+ @Test
+ public void xY_nonPointAndNull() throws ParseException {
+ Geography line = Constructors.geogFromWKT("LINESTRING (0 0, 1 1)", 4326);
+ Geography emptyPoint = Constructors.geogFromWKT("POINT EMPTY", 4326);
+ assertNull(Functions.x(line));
+ assertNull(Functions.y(line));
+ assertNull(Functions.x(emptyPoint));
+ assertNull(Functions.y(emptyPoint));
+ assertNull(Functions.x(null));
+ assertNull(Functions.y(null));
+ }
+
// S2 area-weighted centroids on small polygons differ from planar by an O(d^2/R^2)
// spherical correction. 5e-3 deg (~500 m) is wide enough to absorb that drift on
// 1°-scale shapes near origin, while still catching any real bug (a planar/JTS centroid
diff --git a/common/src/test/java/org/apache/sedona/common/S2Geography/WKBGeographyTest.java b/common/src/test/java/org/apache/sedona/common/S2Geography/WKBGeographyTest.java
index dd5ededd87e..165cdecd9bb 100644
--- a/common/src/test/java/org/apache/sedona/common/S2Geography/WKBGeographyTest.java
+++ b/common/src/test/java/org/apache/sedona/common/S2Geography/WKBGeographyTest.java
@@ -49,6 +49,8 @@ public class WKBGeographyTest {
WKBGeography geog = WKBGeography.fromWKB(wkb, 4326);
assertEquals(4326, geog.getSRID());
assertSame(wkb, geog.getWKBBytes());
+ assertEquals(30.0, geog.getPointX(), EPS);
+ assertEquals(10.0, geog.getPointY(), EPS);
// Accessing JTS should parse lazily
Geometry jts = geog.getJTSGeometry();
@@ -245,6 +247,8 @@ public class WKBGeographyTest {
byte[] bytes = GeographyWKBSerializer.serialize(original);
Geography deserialized = GeographyWKBSerializer.deserialize(bytes);
assertTrue(deserialized instanceof WKBGeography);
+ assertNull(((WKBGeography) deserialized).getPointX());
+ assertNull(((WKBGeography) deserialized).getPointY());
}
// ─── Serialize S2 Geography via new serializer ────────────────────────────
@@ -418,6 +422,8 @@ public class WKBGeographyTest {
// isPoint() must recognize the base type after stripping the EWKB SRID flag.
assertTrue(geog.isPoint());
assertEquals(0, geog.dimension());
+ assertEquals(30.0, geog.getPointX(), EPS);
+ assertEquals(10.0, geog.getPointY(), EPS);
// extractPoint() must skip the 4 SRID bytes; lon/lat should be the original values.
S2Point p = geog.extractPoint();
@@ -432,6 +438,8 @@ public class WKBGeographyTest {
WKBGeography geog = WKBGeography.fromWKB(wkbZ, 0);
// isPoint() is safe — just tests base type — but extractPoint/shape must refuse Z/M.
assertTrue(geog.isPoint());
+ assertEquals(30.0, geog.getPointX(), EPS);
+ assertEquals(10.0, geog.getPointY(), EPS);
assertThrows(UnsupportedOperationException.class, geog::extractPoint);
assertThrows(UnsupportedOperationException.class, () -> new WkbS2Shape(wkbZ));
}
diff --git a/docs/api/sql/geography/Geography-Functions.md b/docs/api/sql/geography/Geography-Functions.md
index 122e596e7c4..b07ecf5d6ce 100644
--- a/docs/api/sql/geography/Geography-Functions.md
+++ b/docs/api/sql/geography/Geography-Functions.md
@@ -57,3 +57,5 @@ These functions operate on geography type objects.
| [ST_DWithin](Geography-Functions/ST_DWithin.md) | Boolean | Test whether two geographies are within a given geodesic distance (in meters) of each other. | v1.9.1 |
| [ST_Within](Geography-Functions/ST_Within.md) | Boolean | Test whether geography A is fully within geography B. | v1.9.1 |
| [ST_Equals](Geography-Functions/ST_Equals.md) | Boolean | Test whether two geographies are spatially equal. | v1.9.1 |
+| [ST_X](Geography-Functions/ST_X.md) | Double | Return the longitude (X coordinate) of a point geography. | v1.9.1 |
+| [ST_Y](Geography-Functions/ST_Y.md) | Double | Return the latitude (Y coordinate) of a point geography. | v1.9.1 |
diff --git a/docs/api/sql/geography/Geography-Functions/ST_X.md b/docs/api/sql/geography/Geography-Functions/ST_X.md
new file mode 100644
index 00000000000..a1793308396
--- /dev/null
+++ b/docs/api/sql/geography/Geography-Functions/ST_X.md
@@ -0,0 +1,42 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ -->
+
+# ST_X
+
+Introduction: Returns the longitude (X coordinate) of a point geography. Returns `NULL` for non-point geographies and empty points.
+
+Format:
+
+`ST_X (A: Geography)`
+
+Return type: `Double`
+
+Since: `v1.9.1`
+
+SQL Example
+
+```sql
+SELECT ST_X(ST_GeogFromWKT('POINT (-73.9857 40.7484)'));
+```
+
+Output:
+
+```
+-73.9857
+```
diff --git a/docs/api/sql/geography/Geography-Functions/ST_Y.md b/docs/api/sql/geography/Geography-Functions/ST_Y.md
new file mode 100644
index 00000000000..b4922c0f078
--- /dev/null
+++ b/docs/api/sql/geography/Geography-Functions/ST_Y.md
@@ -0,0 +1,42 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ -->
+
+# ST_Y
+
+Introduction: Returns the latitude (Y coordinate) of a point geography. Returns `NULL` for non-point geographies and empty points.
+
+Format:
+
+`ST_Y (A: Geography)`
+
+Return type: `Double`
+
+Since: `v1.9.1`
+
+SQL Example
+
+```sql
+SELECT ST_Y(ST_GeogFromWKT('POINT (-73.9857 40.7484)'));
+```
+
+Output:
+
+```
+40.7484
+```
diff --git a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
index a80e2eada81..83cd65ec3c4 100644
--- a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
+++ b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
@@ -736,7 +736,9 @@ private[apache] case class ST_Azimuth(inputExpressions: Seq[Expression])
}
private[apache] case class ST_X(inputExpressions: Seq[Expression])
- extends InferredExpression(Functions.x _) {
+ extends InferredExpression(
+ inferrableFunction1(Functions.x),
+ inferrableFunction1(org.apache.sedona.common.geography.Functions.x)) {
protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = {
copy(inputExpressions = newChildren)
@@ -744,7 +746,9 @@ private[apache] case class ST_X(inputExpressions: Seq[Expression])
}
private[apache] case class ST_Y(inputExpressions: Seq[Expression])
- extends InferredExpression(Functions.y _) {
+ extends InferredExpression(
+ inferrableFunction1(Functions.y),
+ inferrableFunction1(org.apache.sedona.common.geography.Functions.y)) {
protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = {
copy(inputExpressions = newChildren)
diff --git a/spark/common/src/test/scala/org/apache/sedona/sql/geography/GeographyFunctionTest.scala b/spark/common/src/test/scala/org/apache/sedona/sql/geography/GeographyFunctionTest.scala
index 18e89d7c2a7..8f5ea19a288 100644
--- a/spark/common/src/test/scala/org/apache/sedona/sql/geography/GeographyFunctionTest.scala
+++ b/spark/common/src/test/scala/org/apache/sedona/sql/geography/GeographyFunctionTest.scala
@@ -141,6 +141,30 @@ class GeographyFunctionTest extends TestBaseScala {
assertEquals(1.0, point.getX, 1e-9)
assertEquals(2.0, point.getY, 1e-9)
}
+
+ it("ST_X and ST_Y") {
+ val row = sparkSession
+ .sql("""
+ SELECT
+ ST_X(ST_GeogFromWKT('POINT (-73.9857 40.7484)', 4326)) AS x,
+ ST_Y(ST_GeogFromWKT('POINT (-73.9857 40.7484)', 4326)) AS y
+ """)
+ .first()
+ assertEquals(-73.9857, row.getDouble(0), 1e-12)
+ assertEquals(40.7484, row.getDouble(1), 1e-12)
+ }
+
+ it("ST_X and ST_Y return null for non-point geography") {
+ val row = sparkSession
+ .sql("""
+ SELECT
+ ST_X(ST_GeogFromWKT('LINESTRING (0 0, 1 1)', 4326)) AS x,
+ ST_Y(ST_GeogFromWKT('LINESTRING (0 0, 1 1)', 4326)) AS y
+ """)
+ .first()
+ assertTrue(row.isNullAt(0))
+ assertTrue(row.isNullAt(1))
+ }
}
// ─── Level 2: ST_Length, ST_Area, ST_Distance ──────────────────────────