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]


Reply via email to