This is an automated email from the ASF dual-hosted git repository.
wenchen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new 0db8d05a02e9 [SPARK-55449][GEO][SQL] Enable WKB parsing and writing
for Geography
0db8d05a02e9 is described below
commit 0db8d05a02e94c7616e86b806f6e074ce1c343f5
Author: Uros Bojanic <[email protected]>
AuthorDate: Fri Feb 13 23:47:49 2026 +0800
[SPARK-55449][GEO][SQL] Enable WKB parsing and writing for Geography
### What changes were proposed in this pull request?
Implement Geography coordinate validation in WKB parser:
- Longitude must be between -180 and 180 (inclusive).
- Latitude must be between -90 and 90 (inclusive).
### Why are the changes needed?
Enable Geography parsing from WKB.
### Does this PR introduce _any_ user-facing change?
Yes, Geography coordinate values are now limited to the supported ranges
for longitude and latitude.
### How was this patch tested?
Added new unit tests.
### Was this patch authored or co-authored using generative AI tooling?
Yes.
Closes #54227 from uros-db/geo-wkb-geography.
Authored-by: Uros Bojanic <[email protected]>
Signed-off-by: Wenchen Fan <[email protected]>
---
.../apache/spark/sql/catalyst/util/Geography.java | 25 +-
.../spark/sql/catalyst/util/geo/WkbReader.java | 75 ++-
.../sql/catalyst/util/geo/WkbGeographyTest.java | 635 +++++++++++++++++++++
.../util/geo/WkbReaderWriterAdvancedTest.java | 126 +++-
.../sql/catalyst/util/GeographyExecutionSuite.java | 28 +-
5 files changed, 857 insertions(+), 32 deletions(-)
diff --git
a/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/util/Geography.java
b/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/util/Geography.java
index da513d399f8b..8831548bf1fc 100644
---
a/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/util/Geography.java
+++
b/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/util/Geography.java
@@ -16,6 +16,9 @@
*/
package org.apache.spark.sql.catalyst.util;
+import org.apache.spark.sql.catalyst.util.geo.GeometryModel;
+import org.apache.spark.sql.catalyst.util.geo.WkbReader;
+import org.apache.spark.sql.catalyst.util.geo.WkbWriter;
import org.apache.spark.unsafe.types.GeographyVal;
import java.nio.ByteBuffer;
@@ -77,6 +80,9 @@ public final class Geography implements Geo {
// Returns a Geography object with the specified SRID value by parsing the
input WKB.
public static Geography fromWkb(byte[] wkb, int srid) {
+ WkbReader reader = new WkbReader(true);
+ reader.read(wkb); // Validate WKB with geography coordinate bounds.
+
byte[] bytes = new byte[HEADER_SIZE + wkb.length];
ByteBuffer.wrap(bytes).order(DEFAULT_ENDIANNESS).putInt(srid);
System.arraycopy(wkb, 0, bytes, WKB_OFFSET, wkb.length);
@@ -118,19 +124,20 @@ public final class Geography implements Geo {
@Override
public byte[] toWkb() {
- // This method returns only the WKB portion of the in-memory Geography
representation.
- // Note that the header is skipped, and that the WKB is returned as-is
(little-endian).
- return Arrays.copyOfRange(getBytes(), WKB_OFFSET, getBytes().length);
+ return toWkbInternal(DEFAULT_ENDIANNESS);
}
@Override
public byte[] toWkb(ByteOrder endianness) {
- // The default endianness is Little Endian (NDR).
- if (endianness == DEFAULT_ENDIANNESS) {
- return toWkb();
- } else {
- throw new UnsupportedOperationException("Geography WKB endianness is not
yet supported.");
- }
+ return toWkbInternal(endianness);
+ }
+
+ private byte[] toWkbInternal(ByteOrder endianness) {
+ WkbReader reader = new WkbReader(true);
+ GeometryModel model = reader.read(Arrays.copyOfRange(
+ getBytes(), WKB_OFFSET, getBytes().length));
+ WkbWriter writer = new WkbWriter();
+ return writer.write(model, endianness);
}
@Override
diff --git
a/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/util/geo/WkbReader.java
b/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/util/geo/WkbReader.java
index 022b961c8f71..e0b8b543300b 100644
---
a/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/util/geo/WkbReader.java
+++
b/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/util/geo/WkbReader.java
@@ -25,29 +25,60 @@ import java.util.ArrayList;
import java.util.List;
/**
- * Reader for parsing Well-Known Binary (WKB) format geometries.
+ * Reader for parsing Well-Known Binary (WKB) format geometries and
geographies.
* This class implements the OGC Simple Features specification for WKB parsing.
+ * For geographies, coordinate bounds validation is enforced:
+ * - X (longitude) must be between -180 and 180 (inclusive),
+ * - Y (latitude) must be between -90 and 90 (inclusive).
* This class is not thread-safe. Create a new instance for each thread.
* This class should be catalyst-internal.
*/
public class WkbReader {
private ByteBuffer buffer;
private final int validationLevel;
+ private final boolean isGeography;
private byte[] currentWkb;
+ // Geography coordinate bounds.
+ private static final double MIN_LONGITUDE = -180.0;
+ private static final double MAX_LONGITUDE = 180.0;
+ private static final double MIN_LATITUDE = -90.0;
+ private static final double MAX_LATITUDE = 90.0;
+ // Default WKB reader settings.
+ private static final int DEFAULT_VALIDATION_LEVEL = 1; // basic validation
+
/**
- * Constructor for WkbReader with default validation level (1 = basic
validation).
+ * Constructor for WkbReader with default validation level (1 = basic
validation)
+ * and geometry mode (no geography coordinate bounds checking).
*/
public WkbReader() {
- this(1);
+ this(DEFAULT_VALIDATION_LEVEL, false);
}
/**
- * Constructor for WkbReader with specified validation level.
+ * Constructor for WkbReader with specified validation level and geometry
mode.
* @param validationLevel validation level (0 = no validation, 1 = basic
validation)
*/
public WkbReader(int validationLevel) {
+ this(validationLevel, false);
+ }
+
+ /**
+ * Constructor for WkbReader with default validation level and geography
mode.
+ * @param isGeography if true, validates geography coordinate bounds for
longitude and latitude
+ */
+ public WkbReader(boolean isGeography) {
+ this(DEFAULT_VALIDATION_LEVEL, isGeography);
+ }
+
+ /**
+ * Constructor for WkbReader with specified validation level and geography
mode.
+ * @param validationLevel validation level (0 = no validation, 1 = basic
validation)
+ * @param isGeography if true, validates geography coordinate bounds for
longitude and latitude
+ */
+ public WkbReader(int validationLevel, boolean isGeography) {
this.validationLevel = validationLevel;
+ this.isGeography = isGeography;
}
// ========== Coordinate Validation Helpers ==========
@@ -69,6 +100,32 @@ public class WkbReader {
return Double.isFinite(value) || Double.isNaN(value);
}
+ /**
+ * Returns true if the longitude value is within valid geography bounds
[-180, 180].
+ */
+ private static boolean isValidLongitude(double value) {
+ return value >= MIN_LONGITUDE && value <= MAX_LONGITUDE;
+ }
+
+ /**
+ * Returns true if the latitude value is within valid geography bounds [-90,
90].
+ */
+ private static boolean isValidLatitude(double value) {
+ return value >= MIN_LATITUDE && value <= MAX_LATITUDE;
+ }
+
+ /**
+ * Validates geography coordinate bounds for a point. In geography mode with
validation
+ * level > 0, longitude must be between -180 and 180, and latitude must be
between -90 and 90.
+ */
+ private void validateGeographyBounds(Point point, long pos) {
+ if (isGeography && validationLevel > 0 && !point.isEmpty()) {
+ if (!isValidLongitude(point.getX()) || !isValidLatitude(point.getY())) {
+ throw new WkbParseException("Invalid coordinate value found", pos,
currentWkb);
+ }
+ }
+ }
+
/**
* Reads a geometry from WKB bytes.
*/
@@ -301,11 +358,14 @@ public class WkbReader {
* Reads a top-level point geometry (allows empty points with NaN
coordinates).
*/
private Point readPoint(int srid, int dimensionCount, boolean hasZ, boolean
hasM) {
+ long coordsStartPos = buffer.position();
double[] coords = new double[dimensionCount];
for (int i = 0; i < dimensionCount; i++) {
coords[i] = readDoubleAllowEmpty();
}
- return new Point(coords, srid, hasZ, hasM);
+ Point point = new Point(coords, srid, hasZ, hasM);
+ validateGeographyBounds(point, coordsStartPos);
+ return point;
}
/**
@@ -314,11 +374,14 @@ public class WkbReader {
*/
private Point readInternalPoint(int srid, int dimensionCount, boolean hasZ,
boolean hasM) {
+ long coordsStartPos = buffer.position();
double[] coords = new double[dimensionCount];
for (int i = 0; i < dimensionCount; i++) {
coords[i] = readDoubleNoEmpty();
}
- return new Point(coords, srid, hasZ, hasM);
+ Point point = new Point(coords, srid, hasZ, hasM);
+ validateGeographyBounds(point, coordsStartPos);
+ return point;
}
private LineString readLineString(int srid, int dimensionCount, boolean
hasZ, boolean hasM) {
diff --git
a/sql/catalyst/src/test/java/org/apache/spark/sql/catalyst/util/geo/WkbGeographyTest.java
b/sql/catalyst/src/test/java/org/apache/spark/sql/catalyst/util/geo/WkbGeographyTest.java
new file mode 100644
index 000000000000..5c9412519216
--- /dev/null
+++
b/sql/catalyst/src/test/java/org/apache/spark/sql/catalyst/util/geo/WkbGeographyTest.java
@@ -0,0 +1,635 @@
+/*
+ * 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.
+ */
+
+package org.apache.spark.sql.catalyst.util.geo;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Test suite for WKB geography parsing with coordinate bounds validation. In
geography mode,
+ * X must be between -180 and 180 (inclusive), and Y must be between -90 and
90 (inclusive).
+ */
+public class WkbGeographyTest extends WkbTestBase {
+
+ /** Creates a geography-mode WkbReader with default validation (level 1). */
+ private WkbReader geographyReader1() {
+ return new WkbReader(1, true);
+ }
+
+ /** Constructs WKB for a 2D point in little endian format. */
+ private byte[] makePointWkb2D(double x, double y) {
+ ByteBuffer buf = ByteBuffer.allocate(1 + 4 + 8 + 8);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ buf.put((byte) 1); // Little endian.
+ buf.putInt(1); // Point type (2D).
+ buf.putDouble(x);
+ buf.putDouble(y);
+ return buf.array();
+ }
+
+ /** Constructs WKB for a 3DZ point in little endian format. */
+ private byte[] makePointWkb3DZ(double x, double y, double z) {
+ ByteBuffer buf = ByteBuffer.allocate(1 + 4 + 8 + 8 + 8);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ buf.put((byte) 1); // Little endian.
+ buf.putInt(1001); // Point type (3DZ).
+ buf.putDouble(x);
+ buf.putDouble(y);
+ buf.putDouble(z);
+ return buf.array();
+ }
+
+ /** Constructs WKB for a 3DM point in little endian format. */
+ private byte[] makePointWkb3DM(double x, double y, double m) {
+ ByteBuffer buf = ByteBuffer.allocate(1 + 4 + 8 + 8 + 8);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ buf.put((byte) 1); // Little endian.
+ buf.putInt(2001); // Point type (3DM).
+ buf.putDouble(x);
+ buf.putDouble(y);
+ buf.putDouble(m);
+ return buf.array();
+ }
+
+ /** Constructs WKB for a 4D (ZM) point in little endian format. */
+ private byte[] makePointWkb4D(double x, double y, double z, double m) {
+ ByteBuffer buf = ByteBuffer.allocate(1 + 4 + 8 + 8 + 8 + 8);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ buf.put((byte) 1); // Little endian.
+ buf.putInt(3001); // Point type (4D).
+ buf.putDouble(x);
+ buf.putDouble(y);
+ buf.putDouble(z);
+ buf.putDouble(m);
+ return buf.array();
+ }
+
+ /** Constructs WKB for a 2D linestring in little endian format. */
+ private byte[] makeLineStringWkb2D(double[][] points) {
+ ByteBuffer buf = ByteBuffer.allocate(1 + 4 + 4 + points.length * 16);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ buf.put((byte) 1); // Little endian.
+ buf.putInt(2); // LineString type (2D).
+ buf.putInt(points.length);
+ for (double[] point : points) {
+ buf.putDouble(point[0]);
+ buf.putDouble(point[1]);
+ }
+ return buf.array();
+ }
+
+ /** Constructs WKB for a 2D polygon in little endian format. */
+ private byte[] makePolygonWkb2D(double[][][] rings) {
+ int size = 1 + 4 + 4;
+ for (double[][] ring : rings) {
+ size += 4 + ring.length * 16;
+ }
+ ByteBuffer buf = ByteBuffer.allocate(size);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ buf.put((byte) 1); // Little endian.
+ buf.putInt(3); // Polygon type (2D).
+ buf.putInt(rings.length);
+ for (double[][] ring : rings) {
+ buf.putInt(ring.length);
+ for (double[] point : ring) {
+ buf.putDouble(point[0]);
+ buf.putDouble(point[1]);
+ }
+ }
+ return buf.array();
+ }
+
+ /** Constructs WKB for a 2D multipoint in little endian format. */
+ private byte[] makeMultiPointWkb2D(double[][] points) {
+ // Each nested point: 1 (endian) + 4 (type) + 16 (coords) = 21 bytes
+ ByteBuffer buf = ByteBuffer.allocate(1 + 4 + 4 + points.length * 21);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ buf.put((byte) 1); // Little endian.
+ buf.putInt(4); // MultiPoint type (2D).
+ buf.putInt(points.length);
+ for (double[] point : points) {
+ buf.put((byte) 1); // Little endian.
+ buf.putInt(1); // Point type (2D).
+ buf.putDouble(point[0]);
+ buf.putDouble(point[1]);
+ }
+ return buf.array();
+ }
+
+ // ========== Valid Geography 2D Point Tests ==========
+
+ @Test
+ public void testGeographyPointOrigin() {
+ // POINT(0 0) - valid geography
+ byte[] wkb = makePointWkb2D(0.0, 0.0);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Assertions.assertTrue(geom.isPoint(), "Should be a Point");
+ Point point = geom.asPoint();
+ Assertions.assertEquals(0.0, point.getX(), 1e-10, "X coordinate");
+ Assertions.assertEquals(0.0, point.getY(), 1e-10, "Y coordinate");
+ }
+
+ @Test
+ public void testGeographyPointBoundaryMinLonMinLat() {
+ // POINT(-180 -90) - valid at minimum boundary
+ byte[] wkb = makePointWkb2D(-180.0, -90.0);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Point point = geom.asPoint();
+ Assertions.assertEquals(-180.0, point.getX(), 1e-10, "X at min longitude");
+ Assertions.assertEquals(-90.0, point.getY(), 1e-10, "Y at min latitude");
+ }
+
+ @Test
+ public void testGeographyPointBoundaryMaxLonMaxLat() {
+ // POINT(180 90) - valid at maximum boundary
+ byte[] wkb = makePointWkb2D(180.0, 90.0);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Point point = geom.asPoint();
+ Assertions.assertEquals(180.0, point.getX(), 1e-10, "X at max longitude");
+ Assertions.assertEquals(90.0, point.getY(), 1e-10, "Y at max latitude");
+ }
+
+ @Test
+ public void testGeographyPointBoundaryMinLon() {
+ // POINT(-180 0) - valid at min longitude
+ byte[] wkb = makePointWkb2D(-180.0, 0.0);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Assertions.assertEquals(-180.0, geom.asPoint().getX(), 1e-10, "X at min
longitude");
+ }
+
+ @Test
+ public void testGeographyPointBoundaryMaxLon() {
+ // POINT(180 0) - valid at max longitude
+ byte[] wkb = makePointWkb2D(180.0, 0.0);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Assertions.assertEquals(180.0, geom.asPoint().getX(), 1e-10, "X at max
longitude");
+ }
+
+ @Test
+ public void testGeographyPointBoundaryMinLat() {
+ // POINT(0 -90) - valid at min latitude
+ byte[] wkb = makePointWkb2D(0.0, -90.0);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Assertions.assertEquals(-90.0, geom.asPoint().getY(), 1e-10, "Y at min
latitude");
+ }
+
+ @Test
+ public void testGeographyPointBoundaryMaxLat() {
+ // POINT(0 90) - valid at max latitude
+ byte[] wkb = makePointWkb2D(0.0, 90.0);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Assertions.assertEquals(90.0, geom.asPoint().getY(), 1e-10, "Y at max
latitude");
+ }
+
+ @Test
+ public void testGeographyPointTypicalCoordinates() {
+ // POINT(-73.9857 40.7484) - New York City
+ byte[] wkb = makePointWkb2D(-73.9857, 40.7484);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Point point = geom.asPoint();
+ Assertions.assertEquals(-73.9857, point.getX(), 1e-10, "X longitude");
+ Assertions.assertEquals(40.7484, point.getY(), 1e-10, "Y latitude");
+ }
+
+ // ========== Empty Point in Geography Mode ==========
+
+ @Test
+ public void testGeographyEmptyPoint2D() {
+ // POINT EMPTY - valid in geography mode (NaN coordinates skip bounds
check)
+ String wkbHex = "0101000000000000000000f87f000000000000f87f";
+ byte[] wkb = hexToBytes(wkbHex);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Assertions.assertTrue(geom.isPoint(), "Should be a Point");
+ Assertions.assertTrue(geom.asPoint().isEmpty(), "Point should be empty");
+ }
+
+ @Test
+ public void testGeographyEmptyPoint3DZ() {
+ // POINT Z EMPTY
+ String wkbHex =
"01e9030000000000000000f87f000000000000f87f000000000000f87f";
+ byte[] wkb = hexToBytes(wkbHex);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Assertions.assertTrue(geom.asPoint().isEmpty(), "Point Z should be empty");
+ }
+
+ @Test
+ public void testGeographyEmptyPoint4D() {
+ // POINT ZM EMPTY
+ String wkbHex =
+
"01b90b0000000000000000f87f000000000000f87f000000000000f87f000000000000f87f";
+ byte[] wkb = hexToBytes(wkbHex);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Assertions.assertTrue(geom.asPoint().isEmpty(), "Point ZM should be
empty");
+ }
+
+ // ========== Invalid Geography Point - Longitude Out of Bounds ==========
+
+ @Test
+ public void testGeographyPointLongitudeJustOverMax() {
+ // POINT(180.0001 0) - longitude just over 180
+ byte[] wkb = makePointWkb2D(180.0001, 0.0);
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ @Test
+ public void testGeographyPointLongitudeJustUnderMin() {
+ // POINT(-180.0001 0) - longitude just under -180
+ byte[] wkb = makePointWkb2D(-180.0001, 0.0);
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ @Test
+ public void testGeographyPointLongitudeTooHigh() {
+ // POINT(181 0) - longitude > 180
+ byte[] wkb = makePointWkb2D(181.0, 0.0);
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ @Test
+ public void testGeographyPointLongitudeTooLow() {
+ // POINT(-181 0) - longitude < -180
+ byte[] wkb = makePointWkb2D(-181.0, 0.0);
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ @Test
+ public void testGeographyPointLongitudeWayTooHigh() {
+ // POINT(360 0) - longitude way over 180
+ byte[] wkb = makePointWkb2D(360.0, 0.0);
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ @Test
+ public void testGeographyPointLongitudeWayTooLow() {
+ // POINT(-360 0) - longitude way under -180
+ byte[] wkb = makePointWkb2D(-360.0, 0.0);
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ // ========== Invalid Geography Point - Latitude Out of Bounds ==========
+
+ @Test
+ public void testGeographyPointLatitudeJustOverMax() {
+ // POINT(0 90.0001) - latitude just over 90
+ byte[] wkb = makePointWkb2D(0.0, 90.0001);
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ @Test
+ public void testGeographyPointLatitudeJustUnderMin() {
+ // POINT(0 -90.0001) - latitude just under -90
+ byte[] wkb = makePointWkb2D(0.0, -90.0001);
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ @Test
+ public void testGeographyPointLatitudeTooHigh() {
+ // POINT(0 91) - latitude > 90
+ byte[] wkb = makePointWkb2D(0.0, 91.0);
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ @Test
+ public void testGeographyPointLatitudeTooLow() {
+ // POINT(0 -91) - latitude < -90
+ byte[] wkb = makePointWkb2D(0.0, -91.0);
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ @Test
+ public void testGeographyPointLatitudeWayTooHigh() {
+ // POINT(0 180) - latitude way over 90
+ byte[] wkb = makePointWkb2D(0.0, 180.0);
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ @Test
+ public void testGeographyPointLatitudeWayTooLow() {
+ // POINT(0 -180) - latitude way under -90
+ byte[] wkb = makePointWkb2D(0.0, -180.0);
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ // ========== Invalid Geography Point - Both Out of Bounds ==========
+
+ @Test
+ public void testGeographyPointBothOutOfBounds() {
+ // POINT(200 100) - both longitude and latitude out of bounds
+ byte[] wkb = makePointWkb2D(200.0, 100.0);
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ // ========== Geography 3DZ Point Tests (Z has no bounds) ==========
+
+ @Test
+ public void testGeographyPoint3DZ_ValidWithLargeZ() {
+ // POINT Z (0 0 999999) - Z has no bounds in geography mode
+ byte[] wkb = makePointWkb3DZ(0.0, 0.0, 999999.0);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Point point = geom.asPoint();
+ Assertions.assertEquals(0.0, point.getX(), 1e-10, "X coordinate");
+ Assertions.assertEquals(0.0, point.getY(), 1e-10, "Y coordinate");
+ Assertions.assertEquals(999999.0, point.getZ(), 1e-10, "Z coordinate (no
bounds)");
+ }
+
+ @Test
+ public void testGeographyPoint3DZ_ValidWithNegativeZ() {
+ // POINT Z (0 0 -999999) - negative Z has no bounds
+ byte[] wkb = makePointWkb3DZ(0.0, 0.0, -999999.0);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Assertions.assertEquals(-999999.0, geom.asPoint().getZ(), 1e-10, "Z
coordinate (no bounds)");
+ }
+
+ @Test
+ public void testGeographyPoint3DZ_ValidAtBoundaryWithZ() {
+ // POINT Z (180 90 100000) - at boundary with large Z
+ byte[] wkb = makePointWkb3DZ(180.0, 90.0, 100000.0);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Point point = geom.asPoint();
+ Assertions.assertEquals(180.0, point.getX(), 1e-10);
+ Assertions.assertEquals(90.0, point.getY(), 1e-10);
+ Assertions.assertEquals(100000.0, point.getZ(), 1e-10);
+ }
+
+ @Test
+ public void testGeographyPoint3DZ_InvalidLongitude() {
+ // POINT Z (200 0 5) - longitude out of bounds, Z irrelevant
+ byte[] wkb = makePointWkb3DZ(200.0, 0.0, 5.0);
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ @Test
+ public void testGeographyPoint3DZ_InvalidLatitude() {
+ // POINT Z (0 100 5) - latitude out of bounds, Z irrelevant
+ byte[] wkb = makePointWkb3DZ(0.0, 100.0, 5.0);
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ // ========== Geography 3DM Point Tests (M has no bounds) ==========
+
+ @Test
+ public void testGeographyPoint3DM_ValidWithLargeM() {
+ // POINT M (0 0 999999) - M has no bounds in geography mode
+ byte[] wkb = makePointWkb3DM(0.0, 0.0, 999999.0);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Assertions.assertFalse(geom.asPoint().isEmpty(), "Point should not be
empty");
+ }
+
+ @Test
+ public void testGeographyPoint3DM_ValidWithNegativeM() {
+ // POINT M (0 0 -999999) - negative M has no bounds
+ byte[] wkb = makePointWkb3DM(0.0, 0.0, -999999.0);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Assertions.assertFalse(geom.asPoint().isEmpty(), "Point should not be
empty");
+ }
+
+ @Test
+ public void testGeographyPoint3DM_InvalidLongitude() {
+ // POINT M (200 0 5) - longitude out of bounds
+ byte[] wkb = makePointWkb3DM(200.0, 0.0, 5.0);
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ @Test
+ public void testGeographyPoint3DM_InvalidLatitude() {
+ // POINT M (0 100 5) - latitude out of bounds
+ byte[] wkb = makePointWkb3DM(0.0, 100.0, 5.0);
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ // ========== Geography 4D Point Tests (Z and M have no bounds) ==========
+
+ @Test
+ public void testGeographyPoint4D_ValidWithLargeZM() {
+ // POINT ZM (0 0 999999 -999999) - Z and M have no bounds
+ byte[] wkb = makePointWkb4D(0.0, 0.0, 999999.0, -999999.0);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Point point = geom.asPoint();
+ Assertions.assertEquals(999999.0, point.getZ(), 1e-10, "Z (no bounds)");
+ Assertions.assertEquals(-999999.0, point.getM(), 1e-10, "M (no bounds)");
+ }
+
+ @Test
+ public void testGeographyPoint4D_ValidAtBoundaryWithZM() {
+ // POINT ZM (-180 -90 -100000 100000) - at boundary with arbitrary Z and M
+ byte[] wkb = makePointWkb4D(-180.0, -90.0, -100000.0, 100000.0);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Point point = geom.asPoint();
+ Assertions.assertEquals(-180.0, point.getX(), 1e-10);
+ Assertions.assertEquals(-90.0, point.getY(), 1e-10);
+ Assertions.assertEquals(-100000.0, point.getZ(), 1e-10);
+ Assertions.assertEquals(100000.0, point.getM(), 1e-10);
+ }
+
+ @Test
+ public void testGeographyPoint4D_InvalidLongitude() {
+ // POINT ZM (200 0 5 10) - longitude out of bounds
+ byte[] wkb = makePointWkb4D(200.0, 0.0, 5.0, 10.0);
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ @Test
+ public void testGeographyPoint4D_InvalidLatitude() {
+ // POINT ZM (0 100 5 10) - latitude out of bounds
+ byte[] wkb = makePointWkb4D(0.0, 100.0, 5.0, 10.0);
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ // ========== Geography LineString Tests ==========
+
+ @Test
+ public void testGeographyLineStringValid() {
+ // LINESTRING(0 0, 10 10) - valid geography coordinates
+ byte[] wkb = makeLineStringWkb2D(new double[][]{{0.0, 0.0}, {10.0, 10.0}});
+ GeometryModel geom = geographyReader1().read(wkb);
+ Assertions.assertTrue(geom instanceof LineString, "Should be a
LineString");
+ Assertions.assertFalse(geom.isEmpty(), "Should not be empty");
+ }
+
+ @Test
+ public void testGeographyLineStringValidAtBoundary() {
+ // LINESTRING(-180 -90, 180 90) - at boundary
+ byte[] wkb = makeLineStringWkb2D(new double[][]{{-180.0, -90.0}, {180.0,
90.0}});
+ GeometryModel geom = geographyReader1().read(wkb);
+ Assertions.assertTrue(geom instanceof LineString);
+ }
+
+ @Test
+ public void testGeographyLineStringEmpty() {
+ // LINESTRING EMPTY - valid in geography mode
+ String wkbHex = "010200000000000000";
+ byte[] wkb = hexToBytes(wkbHex);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Assertions.assertTrue(geom instanceof LineString);
+ Assertions.assertTrue(geom.isEmpty(), "Should be empty");
+ }
+
+ @Test
+ public void testGeographyLineStringInvalidSecondPoint() {
+ // LINESTRING(0 0, 200 0) - second point longitude out of bounds
+ byte[] wkb = makeLineStringWkb2D(new double[][]{{0.0, 0.0}, {200.0, 0.0}});
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ @Test
+ public void testGeographyLineStringInvalidFirstPoint() {
+ // LINESTRING(-200 0, 0 0) - first point longitude out of bounds
+ byte[] wkb = makeLineStringWkb2D(new double[][]{{-200.0, 0.0}, {0.0,
0.0}});
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ @Test
+ public void testGeographyLineStringInvalidLatitude() {
+ // LINESTRING(0 0, 0 100) - second point latitude out of bounds
+ byte[] wkb = makeLineStringWkb2D(new double[][]{{0.0, 0.0}, {0.0, 100.0}});
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ // ========== Geography Polygon Tests ==========
+
+ @Test
+ public void testGeographyPolygonValid() {
+ // POLYGON((0 0, 10 0, 0 10, 0 0)) - valid geography coordinates
+ byte[] wkb = makePolygonWkb2D(new double[][][]{
+ {{0.0, 0.0}, {10.0, 0.0}, {0.0, 10.0}, {0.0, 0.0}}
+ });
+ GeometryModel geom = geographyReader1().read(wkb);
+ Assertions.assertTrue(geom instanceof Polygon, "Should be a Polygon");
+ }
+
+ @Test
+ public void testGeographyPolygonEmpty() {
+ // POLYGON EMPTY - valid in geography mode
+ String wkbHex = "010300000000000000";
+ byte[] wkb = hexToBytes(wkbHex);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Assertions.assertTrue(geom instanceof Polygon);
+ Assertions.assertTrue(geom.isEmpty(), "Should be empty");
+ }
+
+ @Test
+ public void testGeographyPolygonInvalidVertex() {
+ // POLYGON with a vertex at (200, 0) - longitude out of bounds
+ byte[] wkb = makePolygonWkb2D(new double[][][]{
+ {{0.0, 0.0}, {200.0, 0.0}, {0.0, 10.0}, {0.0, 0.0}}
+ });
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ @Test
+ public void testGeographyPolygonInvalidLatitude() {
+ // POLYGON with a vertex at (0, 100) - latitude out of bounds
+ byte[] wkb = makePolygonWkb2D(new double[][][]{
+ {{0.0, 0.0}, {10.0, 0.0}, {0.0, 100.0}, {0.0, 0.0}}
+ });
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ // ========== Geography MultiPoint Tests ==========
+
+ @Test
+ public void testGeographyMultiPointValid() {
+ // MULTIPOINT((0 0), (10 20)) - valid geography
+ byte[] wkb = makeMultiPointWkb2D(new double[][]{{0.0, 0.0}, {10.0, 20.0}});
+ GeometryModel geom = geographyReader1().read(wkb);
+ Assertions.assertTrue(geom instanceof MultiPoint, "Should be a
MultiPoint");
+ }
+
+ @Test
+ public void testGeographyMultiPointInvalidSecondPoint() {
+ // MULTIPOINT((0 0), (200 0)) - second point longitude out of bounds
+ byte[] wkb = makeMultiPointWkb2D(new double[][]{{0.0, 0.0}, {200.0, 0.0}});
+ Assertions.assertThrows(WkbParseException.class, () ->
geographyReader1().read(wkb));
+ }
+
+ @Test
+ public void testGeographyMultiPointEmpty() {
+ // MULTIPOINT EMPTY - valid
+ String wkbHex = "010400000000000000";
+ byte[] wkb = hexToBytes(wkbHex);
+ GeometryModel geom = geographyReader1().read(wkb);
+ Assertions.assertTrue(geom instanceof MultiPoint);
+ Assertions.assertTrue(geom.isEmpty(), "Should be empty");
+ }
+
+ // ========== Validation Level 0 Bypasses Geography Bounds Check ==========
+
+ /** Creates a geography-mode WkbReader with no validation (level 0). */
+ private WkbReader geographyReader0() {
+ return new WkbReader(0, true);
+ }
+
+ @Test
+ public void testGeographyNoValidation_OutOfBoundsAccepted() {
+ // With validation level 0, out-of-bounds coordinates should be accepted
+ byte[] wkb = makePointWkb2D(200.0, 100.0);
+ GeometryModel geom = geographyReader0().read(wkb);
+ Assertions.assertTrue(geom.isPoint(), "Should be a Point");
+ Point point = geom.asPoint();
+ Assertions.assertEquals(200.0, point.getX(), 1e-10, "X coordinate
preserved");
+ Assertions.assertEquals(100.0, point.getY(), 1e-10, "Y coordinate
preserved");
+ }
+
+ @Test
+ public void testGeographyNoValidation_LineStringOutOfBoundsAccepted() {
+ // With validation level 0, out-of-bounds linestring accepted
+ byte[] wkb = makeLineStringWkb2D(new double[][]{{0.0, 0.0}, {200.0, 0.0}});
+ GeometryModel geom = geographyReader0().read(wkb);
+ Assertions.assertTrue(geom instanceof LineString);
+ }
+
+ // ========== Geometry Mode Does NOT Apply Geography Bounds ==========
+
+ @Test
+ public void testGeometryModeAcceptsOutOfBoundsCoordinates() {
+ // In geometry mode (non-geography), coordinates outside geography bounds
are valid
+ WkbReader geometryReader = new WkbReader(1, false);
+ byte[] wkb = makePointWkb2D(200.0, 100.0);
+ GeometryModel geom = geometryReader.read(wkb);
+ Assertions.assertTrue(geom.isPoint(), "Should be a Point");
+ Point point = geom.asPoint();
+ Assertions.assertEquals(200.0, point.getX(), 1e-10, "X coordinate");
+ Assertions.assertEquals(100.0, point.getY(), 1e-10, "Y coordinate");
+ }
+
+ @Test
+ public void testGeometryModeLargeCoordinatesAccepted() {
+ // In geometry mode, large coordinates are valid
+ WkbReader geometryReader = new WkbReader();
+ byte[] wkb = makePointWkb2D(1000000.0, 2000000.0);
+ GeometryModel geom = geometryReader.read(wkb);
+ Assertions.assertTrue(geom.isPoint(), "Should be a Point");
+ Assertions.assertEquals(1000000.0, geom.asPoint().getX(), 1e-10);
+ Assertions.assertEquals(2000000.0, geom.asPoint().getY(), 1e-10);
+ }
+
+ // ========== Error Message Tests ==========
+
+ @Test
+ public void testGeographyErrorMessageContainsBoundsInfo() {
+ // Verify error message mentions longitude/latitude bounds
+ byte[] wkb = makePointWkb2D(200.0, 0.0);
+ WkbParseException ex = Assertions.assertThrows(
+ WkbParseException.class, () -> geographyReader1().read(wkb));
+ String msg = ex.getMessage();
+ Assertions.assertTrue(msg.contains("Invalid coordinate value"));
+ }
+}
diff --git
a/sql/catalyst/src/test/java/org/apache/spark/sql/catalyst/util/geo/WkbReaderWriterAdvancedTest.java
b/sql/catalyst/src/test/java/org/apache/spark/sql/catalyst/util/geo/WkbReaderWriterAdvancedTest.java
index 1cc999112808..98bec81ca011 100644
---
a/sql/catalyst/src/test/java/org/apache/spark/sql/catalyst/util/geo/WkbReaderWriterAdvancedTest.java
+++
b/sql/catalyst/src/test/java/org/apache/spark/sql/catalyst/util/geo/WkbReaderWriterAdvancedTest.java
@@ -17,6 +17,7 @@
package org.apache.spark.sql.catalyst.util.geo;
+import org.apache.spark.sql.catalyst.util.Geography;
import org.apache.spark.sql.catalyst.util.Geometry;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -88,10 +89,15 @@ public class WkbReaderWriterAdvancedTest extends
WkbTestBase {
* Test helper to verify WKB round-trip (write and read back)
*/
private void checkWkbRoundTrip(String wkbHexLittle, String wkbHexBig) {
+ checkGeometryWkbRoundTrip(wkbHexLittle, wkbHexBig);
+ checkGeographyWkbRoundTrip(wkbHexLittle, wkbHexBig);
+ }
+
+ private void checkGeometryWkbRoundTrip(String wkbHexLittle, String
wkbHexBig) {
byte[] wkbLittle = hexToBytes(wkbHexLittle);
byte[] wkbBig = hexToBytes(wkbHexBig);
- // Parse the WKB (little)
+ // Parse the geometry WKB (little endian).
WkbReader reader = new WkbReader();
GeometryModel model = reader.read(wkbLittle, 0);
WkbWriter writer = new WkbWriter();
@@ -102,7 +108,7 @@ public class WkbReaderWriterAdvancedTest extends
WkbTestBase {
Assertions.assertEquals(wkbHexBig, bytesToHex(writtenBigFromModelLittle),
"WKB big endian round-trip failed");
- // Parse the WKB (big)
+ // Parse the geometry WKB (big endian).
GeometryModel geomFromBig = reader.read(wkbBig, 0);
byte[] writtenLittleFromModelBig = writer.write(geomFromBig,
ByteOrder.LITTLE_ENDIAN);
byte[] writtenBigFromModelBig = writer.write(geomFromBig,
ByteOrder.BIG_ENDIAN);
@@ -111,7 +117,7 @@ public class WkbReaderWriterAdvancedTest extends
WkbTestBase {
Assertions.assertEquals(wkbHexBig, bytesToHex(writtenBigFromModelBig),
"WKB big endian round-trip from big endian failed");
- // Use Geometry.fromWkb (little)
+ // Use Geometry.fromWkb (little endian).
Geometry geometryFromLittle = Geometry.fromWkb(wkbLittle, 0);
byte[] wkbLittleFromGeometryLittle =
geometryFromLittle.toWkb(ByteOrder.LITTLE_ENDIAN);
Assertions.assertEquals(wkbHexLittle,
bytesToHex(wkbLittleFromGeometryLittle),
@@ -120,7 +126,7 @@ public class WkbReaderWriterAdvancedTest extends
WkbTestBase {
Assertions.assertEquals(wkbHexBig, bytesToHex(wkbBigFromGeometryLittle),
"Geometry.fromWKB big endian round-trip failed");
- // Use Geometry.fromWkb (big)
+ // Use Geometry.fromWkb (big endian).
Geometry geometryFromBig = Geometry.fromWkb(writtenBigFromModelLittle, 0);
byte[] wkbLittleFromGeometryBig =
geometryFromBig.toWkb(ByteOrder.LITTLE_ENDIAN);
Assertions.assertEquals(wkbHexLittle, bytesToHex(wkbLittleFromGeometryBig),
@@ -128,7 +134,49 @@ public class WkbReaderWriterAdvancedTest extends
WkbTestBase {
byte[] wkbBigFromGeometryBig = geometryFromBig.toWkb(ByteOrder.BIG_ENDIAN);
Assertions.assertEquals(wkbHexBig, bytesToHex(wkbBigFromGeometryBig),
"Geometry.fromWKB big endian round-trip from big endian failed");
+ }
+
+ private void checkGeographyWkbRoundTrip(String wkbHexLittle, String
wkbHexBig) {
+ byte[] wkbLittle = hexToBytes(wkbHexLittle);
+ byte[] wkbBig = hexToBytes(wkbHexBig);
+
+ // Parse the geography WKB (little endian).
+ WkbReader reader = new WkbReader(true);
+ GeometryModel model = reader.read(wkbLittle, 0);
+ WkbWriter writer = new WkbWriter();
+ byte[] writtenLittleFromModelLittle = writer.write(model,
ByteOrder.LITTLE_ENDIAN);
+ byte[] writtenBigFromModelLittle = writer.write(model,
ByteOrder.BIG_ENDIAN);
+ Assertions.assertEquals(wkbHexLittle,
bytesToHex(writtenLittleFromModelLittle),
+ "WKB little endian round-trip failed");
+ Assertions.assertEquals(wkbHexBig, bytesToHex(writtenBigFromModelLittle),
+ "WKB big endian round-trip failed");
+
+ // Parse the geography WKB (big endian).
+ GeometryModel geomFromBig = reader.read(wkbBig, 0);
+ byte[] writtenLittleFromModelBig = writer.write(geomFromBig,
ByteOrder.LITTLE_ENDIAN);
+ byte[] writtenBigFromModelBig = writer.write(geomFromBig,
ByteOrder.BIG_ENDIAN);
+ Assertions.assertEquals(wkbHexLittle,
bytesToHex(writtenLittleFromModelBig),
+ "WKB little endian round-trip from big endian failed");
+ Assertions.assertEquals(wkbHexBig, bytesToHex(writtenBigFromModelBig),
+ "WKB big endian round-trip from big endian failed");
+
+ // Use Geography.fromWkb (little endian).
+ Geography geometryFromLittle = Geography.fromWkb(wkbLittle, 0);
+ byte[] wkbLittleFromGeometryLittle =
geometryFromLittle.toWkb(ByteOrder.LITTLE_ENDIAN);
+ Assertions.assertEquals(wkbHexLittle,
bytesToHex(wkbLittleFromGeometryLittle),
+ "Geography.fromWKB little endian round-trip failed");
+ byte[] wkbBigFromGeometryLittle =
geometryFromLittle.toWkb(ByteOrder.BIG_ENDIAN);
+ Assertions.assertEquals(wkbHexBig, bytesToHex(wkbBigFromGeometryLittle),
+ "Geography.fromWKB big endian round-trip failed");
+ // Use Geography.fromWkb (big endian).
+ Geography geometryFromBig = Geography.fromWkb(writtenBigFromModelLittle,
0);
+ byte[] wkbLittleFromGeometryBig =
geometryFromBig.toWkb(ByteOrder.LITTLE_ENDIAN);
+ Assertions.assertEquals(wkbHexLittle, bytesToHex(wkbLittleFromGeometryBig),
+ "Geography.fromWKB little endian round-trip from big endian failed");
+ byte[] wkbBigFromGeometryBig = geometryFromBig.toWkb(ByteOrder.BIG_ENDIAN);
+ Assertions.assertEquals(wkbHexBig, bytesToHex(wkbBigFromGeometryBig),
+ "Geography.fromWKB big endian round-trip from big endian failed");
}
// ========== Point Tests (2D) ==========
@@ -1127,9 +1175,75 @@ public class WkbReaderWriterAdvancedTest extends
WkbTestBase {
public void testSridPreservation() {
String wkbLe = "0101000000000000000000f03f0000000000000040";
byte[] wkb = hexToBytes(wkbLe);
- WkbReader reader = new WkbReader();
- GeometryModel geom = reader.read(wkb, 4326);
+ WkbReader geomReader = new WkbReader();
+ GeometryModel geom = geomReader.read(wkb, 4326);
Assertions.assertEquals(4326, geom.srid());
+
+ WkbReader geogReader = new WkbReader(true);
+ GeometryModel geog = geogReader.read(wkb, 4326);
+ Assertions.assertEquals(4326, geog.srid());
+ }
+
+ // ========== Geography Coordinate Bounds Validation Tests ==========
+
+ /**
+ * Test helper to verify that geography bounds validation rejects
out-of-bounds coordinates.
+ */
+ private void checkGeographyBoundsError(String wkbHexLe, String wkbHexBe) {
+ for (String wkbHex : new String[]{wkbHexLe, wkbHexBe}) {
+ byte[] wkb = hexToBytes(wkbHex);
+
+ // Geography mode with validation should reject non-geographic
coordinate values.
+ WkbReader geographyReader = new WkbReader(true);
+ WkbParseException ex = Assertions.assertThrows(
+ WkbParseException.class, () -> geographyReader.read(wkb, 0));
+ Assertions.assertTrue(ex.getMessage().contains("Invalid coordinate
value"));
+ // Geography mode without validation should accept non-geographic
coordinate values.
+ WkbReader noValidateGeographyReader = new WkbReader(0, true);
+ Assertions.assertDoesNotThrow(() -> noValidateGeographyReader.read(wkb,
0));
+
+ // Geometry mode should always accept non-geographic coordinate values.
+ WkbReader geometryReader = new WkbReader();
+ Assertions.assertDoesNotThrow(() -> geometryReader.read(wkb, 0));
+ WkbReader noValidateGeometryReader = new WkbReader(0);
+ Assertions.assertDoesNotThrow(() -> noValidateGeometryReader.read(wkb,
0));
+ }
+ }
+
+ @Test
+ public void testGeographyBoundsLongitudeTooHigh() {
+ // WKB values for: POINT(200 0); longitude > 180.
+ checkGeographyBoundsError(
+ "010100000000000000000069400000000000000000",
+ "000000000140690000000000000000000000000000"
+ );
+ }
+
+ @Test
+ public void testGeographyBoundsLongitudeTooLow() {
+ // WKB values for: POINT(-200 0); longitude < -180.
+ checkGeographyBoundsError(
+ "010100000000000000000069c00000000000000000",
+ "0000000001c0690000000000000000000000000000"
+ );
+ }
+
+ @Test
+ public void testGeographyBoundsLatitudeTooHigh() {
+ // WKB values for: POINT(0 100); latitude > 90.
+ checkGeographyBoundsError(
+ "010100000000000000000000000000000000005940",
+ "000000000100000000000000004059000000000000"
+ );
+ }
+
+ @Test
+ public void testGeographyBoundsLatitudeTooLow() {
+ // WKB values for: POINT(0 -100); latitude < -90.
+ checkGeographyBoundsError(
+ "0101000000000000000000000000000000000059c0",
+ "00000000010000000000000000c059000000000000"
+ );
}
}
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/util/GeographyExecutionSuite.java
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/util/GeographyExecutionSuite.java
index 078ee2a3dbfb..bd1ecf7e8c10 100644
---
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/util/GeographyExecutionSuite.java
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/util/GeographyExecutionSuite.java
@@ -20,6 +20,7 @@ package org.apache.spark.sql.catalyst.util;
import org.apache.spark.unsafe.types.GeographyVal;
import org.junit.jupiter.api.Test;
+import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.HexFormat;
@@ -82,11 +83,20 @@ class GeographyExecutionSuite {
/** Tests for Geography WKB parsing. */
+ // Helper method to create a simple WKB for POINT(0, 1).
+ private byte[] getTestWKBPoint() {
+ ByteBuffer bb = ByteBuffer.allocate(1 + 4 + 8 + 8);
+ bb.order(ByteOrder.LITTLE_ENDIAN);
+ bb.put((byte) 1); // byte order (LE)
+ bb.putInt(1); // type = 1 (Point)
+ bb.putDouble(0.0); // X = 0
+ bb.putDouble(1.0); // Y = 0
+ return bb.array();
+ }
+
@Test
void testFromWkbWithSridRudimentary() {
- byte[] wkb = new byte[]{1, 2, 3};
- // Note: This is a rudimentary WKB handling test; actual WKB parsing is
not yet implemented.
- // Once we implement the appropriate parsing logic, this test should be
updated accordingly.
+ byte[] wkb = getTestWKBPoint();
Geography geography = Geography.fromWkb(wkb, 4326);
assertNotNull(geography);
assertArrayEquals(wkb, geography.toWkb());
@@ -95,9 +105,7 @@ class GeographyExecutionSuite {
@Test
void testFromWkbNoSridRudimentary() {
- byte[] wkb = new byte[]{1, 2, 3};
- // Note: This is a rudimentary WKB handling test; actual WKB parsing is
not yet implemented.
- // Once we implement the appropriate parsing logic, this test should be
updated accordingly.
+ byte[] wkb = getTestWKBPoint();
Geography geography = Geography.fromWkb(wkb);
assertNotNull(geography);
assertArrayEquals(wkb, geography.toWkb());
@@ -171,11 +179,9 @@ class GeographyExecutionSuite {
@Test
void testToWkbEndiannessXDR() {
Geography geography = Geography.fromBytes(testGeographyVal);
- UnsupportedOperationException exception = assertThrows(
- UnsupportedOperationException.class,
- () -> geography.toWkb(ByteOrder.BIG_ENDIAN)
- );
- assertEquals("Geography WKB endianness is not yet supported.",
exception.getMessage());
+ // WKB value (endianness: XDR) corresponding to WKT: POINT(1 2).
+ byte[] wkb =
HexFormat.of().parseHex("00000000013FF00000000000004000000000000000");
+ assertArrayEquals(wkb, geography.toWkb(ByteOrder.BIG_ENDIAN));
}
@Test
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]