Skip to main content

vortex_geo/extension/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4pub(crate) mod coordinate;
5mod linestring;
6mod multilinestring;
7mod multipoint;
8mod multipolygon;
9mod point;
10mod polygon;
11mod rect;
12mod wkb;
13
14use std::fmt::Display;
15use std::sync::Arc;
16
17use ::wkb::reader::GeometryType;
18use arrow_array::BinaryArray;
19use geo_types::Geometry;
20use geoarrow::array::GenericWkbArray;
21use geoarrow::array::GeoArrowArray;
22use geoarrow::datatypes::CoordType;
23use geoarrow::datatypes::Crs;
24use geoarrow::datatypes::Dimension;
25use geoarrow::datatypes::GeoArrowType;
26use geoarrow::datatypes::LineStringType;
27use geoarrow::datatypes::Metadata;
28use geoarrow::datatypes::MultiLineStringType;
29use geoarrow::datatypes::MultiPointType;
30use geoarrow::datatypes::MultiPolygonType;
31use geoarrow::datatypes::PointType;
32use geoarrow::datatypes::PolygonType;
33use geoarrow::datatypes::WkbType;
34use geoarrow_cast::cast::cast;
35pub use linestring::*;
36pub use multilinestring::*;
37pub use multipoint::*;
38pub use multipolygon::*;
39pub use point::*;
40pub use polygon::*;
41pub use rect::*;
42use vortex_array::ArrayRef;
43use vortex_array::Canonical;
44use vortex_array::ExecutionCtx;
45use vortex_array::IntoArray;
46use vortex_array::arrays::ConstantArray;
47use vortex_array::arrays::ExtensionArray;
48use vortex_array::arrays::StructArray;
49use vortex_array::arrays::extension::ExtensionArrayExt;
50use vortex_array::arrays::listview::ListViewArrayExt;
51use vortex_array::dtype::DType;
52use vortex_array::dtype::extension::ExtDType;
53use vortex_array::dtype::extension::ExtVTable;
54use vortex_array::scalar::Scalar;
55use vortex_arrow::FromArrowArray;
56use vortex_error::VortexResult;
57use vortex_error::vortex_bail;
58use vortex_error::vortex_ensure;
59use vortex_error::vortex_err;
60pub use wkb::*;
61
62/// Whether `dtype` is one of the native geometry extension types the geo kernels operate on.
63pub(crate) fn is_native_geometry(dtype: &DType) -> bool {
64    dtype.as_extension_opt().is_some_and(|ext| {
65        ext.is::<Point>()
66            || ext.is::<LineString>()
67            || ext.is::<MultiPoint>()
68            || ext.is::<Polygon>()
69            || ext.is::<MultiLineString>()
70            || ext.is::<MultiPolygon>()
71            || ext.is::<Rect>()
72    })
73}
74
75/// Validate the operands of a geo scalar function: each must be a native geometry type (so the
76/// kernel can decode it) and non-nullable (geometry arrays never carry nulls).
77pub(crate) fn validate_geometry_operands(dtypes: &[DType]) -> VortexResult<()> {
78    for dtype in dtypes {
79        vortex_ensure!(
80            is_native_geometry(dtype),
81            "geo: operand {dtype} is not a native geometry type"
82        );
83        vortex_ensure!(
84            !dtype.is_nullable(),
85            "geo: nullable operand {dtype} is unsupported"
86        );
87    }
88    Ok(())
89}
90
91/// Flatten a native geometry column into a single coordinate `Struct<x, y, ...>` containing
92/// every vertex of every geometry.
93pub(crate) fn flatten_coordinates(
94    array: &ArrayRef,
95    ctx: &mut ExecutionCtx,
96) -> VortexResult<StructArray> {
97    if !is_native_geometry(array.dtype()) {
98        vortex_bail!(
99            "geo: operand is not a native geometry extension type, was {}",
100            array.dtype()
101        );
102    }
103    let mut node = array
104        .clone()
105        .execute::<ExtensionArray>(ctx)?
106        .storage_array()
107        .clone();
108    while matches!(node.dtype(), DType::List(..)) {
109        node = node
110            .execute::<Canonical>(ctx)?
111            .into_listview()
112            .elements()
113            .clone();
114    }
115    node.execute::<StructArray>(ctx)
116}
117
118/// Decode a native geometry column to `geo_types`. A non-geometry operand is an error.
119pub(crate) fn geometries(
120    array: &ArrayRef,
121    ctx: &mut ExecutionCtx,
122) -> VortexResult<Vec<Geometry<f64>>> {
123    let Some(ext) = array.dtype().as_extension_opt() else {
124        vortex_bail!(
125            "geo: operand is not a geometry extension type, was {}",
126            array.dtype()
127        );
128    };
129    let storage = array
130        .clone()
131        .execute::<ExtensionArray>(ctx)?
132        .storage_array()
133        .clone();
134    if ext.is::<Point>() {
135        point_geometries(&storage, ctx)
136    } else if ext.is::<LineString>() {
137        linestring_geometries(&storage, ctx)
138    } else if ext.is::<MultiPoint>() {
139        multipoint_geometries(&storage, ctx)
140    } else if ext.is::<Polygon>() {
141        polygon_geometries(&storage, ctx)
142    } else if ext.is::<MultiLineString>() {
143        multilinestring_geometries(&storage, ctx)
144    } else if ext.is::<MultiPolygon>() {
145        multipolygon_geometries(&storage, ctx)
146    } else if ext.is::<Rect>() {
147        rect_geometries(&storage, ctx)
148    } else {
149        vortex_bail!("geo: unsupported geometry extension {}", array.dtype())
150    }
151}
152
153/// Decode a constant operand scalar to one geo geometry, a constant of any
154/// supported geometry type is decoded exactly like a column.
155pub(crate) fn single_geometry(
156    scalar: &Scalar,
157    ctx: &mut ExecutionCtx,
158) -> VortexResult<Geometry<f64>> {
159    let array = ConstantArray::new(scalar.clone(), 1).into_array();
160    geometries(&array, ctx)?
161        .pop()
162        .ok_or_else(|| vortex_err!("geo: constant operand decoded to no geometry"))
163}
164
165/// Decode a WKB geometry literal (DuckDB's wire form for `GEOMETRY` constants) to its native
166/// `Point`/`Polygon`/`MultiPolygon` scalar. `None` for unsupported types. Plan-time, one value only.
167pub fn native_geometry_scalar_from_wkb(bytes: &[u8]) -> VortexResult<Option<Scalar>> {
168    let metadata = geoarrow_metadata(&GeoMetadata::default());
169    let binary = BinaryArray::from(vec![Some(bytes)]);
170    let wkb = GenericWkbArray::<i32>::try_from((
171        &binary as &dyn arrow_array::Array,
172        WkbType::new(Arc::clone(&metadata)),
173    ))
174    .map_err(|e| vortex_err!("failed to read WKB literal: {e}"))?;
175
176    // Cast the WKB value to `target`, import its native storage as a Vortex array.
177    let to_storage = |target: &GeoArrowType| -> VortexResult<ArrayRef> {
178        let native =
179            cast(&wkb, target).map_err(|e| vortex_err!("failed to cast WKB literal: {e}"))?;
180        ArrayRef::from_arrow(native.to_array_ref().as_ref(), false)
181    };
182
183    let scalar = match Wkb::try_from_bytes(bytes)?.geometry_type() {
184        GeometryType::Point => {
185            let target = GeoArrowType::Point(
186                PointType::new(Dimension::XY, metadata).with_coord_type(CoordType::Separated),
187            );
188            geo_ext_scalar(Point, to_storage(&target)?)?
189        }
190        GeometryType::LineString => {
191            let target = GeoArrowType::LineString(
192                LineStringType::new(Dimension::XY, metadata).with_coord_type(CoordType::Separated),
193            );
194            geo_ext_scalar(LineString, to_storage(&target)?)?
195        }
196        GeometryType::Polygon => {
197            let target = GeoArrowType::Polygon(
198                PolygonType::new(Dimension::XY, metadata).with_coord_type(CoordType::Separated),
199            );
200            geo_ext_scalar(Polygon, to_storage(&target)?)?
201        }
202        GeometryType::MultiPoint => {
203            let target = GeoArrowType::MultiPoint(
204                MultiPointType::new(Dimension::XY, metadata).with_coord_type(CoordType::Separated),
205            );
206            geo_ext_scalar(MultiPoint, to_storage(&target)?)?
207        }
208        GeometryType::MultiLineString => {
209            let target = GeoArrowType::MultiLineString(
210                MultiLineStringType::new(Dimension::XY, metadata)
211                    .with_coord_type(CoordType::Separated),
212            );
213            geo_ext_scalar(MultiLineString, to_storage(&target)?)?
214        }
215        GeometryType::MultiPolygon => {
216            let target = GeoArrowType::MultiPolygon(
217                MultiPolygonType::new(Dimension::XY, metadata)
218                    .with_coord_type(CoordType::Separated),
219            );
220            geo_ext_scalar(MultiPolygon, to_storage(&target)?)?
221        }
222        _ => return Ok(None),
223    };
224    Ok(Some(scalar))
225}
226
227/// Wrap cast-from-WKB `storage` in its `vtable` extension type and pull out the single scalar.
228// `scalar_at` is deprecated for `execute_scalar`, but there is no execution context at plan time.
229#[allow(deprecated)]
230fn geo_ext_scalar<V: ExtVTable<Metadata = GeoMetadata>>(
231    vtable: V,
232    storage: ArrayRef,
233) -> VortexResult<Scalar> {
234    let ext = ExtDType::try_with_vtable(vtable, GeoMetadata::default(), storage.dtype().clone())?
235        .erased();
236    ExtensionArray::try_new(ext, storage)?
237        .into_array()
238        .scalar_at(0)
239}
240
241/// Extension metadata that is common to all the geospatial extension types.
242///
243/// Currently, this is just the coordinate reference system (CRS).
244/// We may wish to add a second field for edges interpretation in the future similar to
245/// the GeoArrow standard.
246#[derive(Clone, PartialEq, Eq, Hash, prost::Message)]
247pub struct GeoMetadata {
248    #[prost(optional, string, tag = "1")]
249    pub crs: Option<String>,
250}
251
252impl Display for GeoMetadata {
253    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254        match self.crs.as_ref() {
255            Some(crs) => write!(f, "Geometry(crs={crs})"),
256            None => write!(f, "Geometry(unreferenced)"),
257        }
258    }
259}
260
261/// The GeoArrow [`Metadata`] equivalent of `geo_metadata`.
262pub(crate) fn geoarrow_metadata(geo_metadata: &GeoMetadata) -> Arc<Metadata> {
263    Arc::new(Metadata::new(
264        geo_metadata
265            .crs
266            .as_ref()
267            .map(|crs| Crs::from_unknown_crs_type(crs.to_string()))
268            .unwrap_or_default(),
269        None,
270    ))
271}
272
273/// Serialize a native geometry array to WKB (a `WkbView` array) via geoarrow's cast.
274/// Shared by the `to_wkb` methods on the geometry extension types.
275pub(crate) fn geoarrow_to_wkb(geo_array: &dyn GeoArrowArray) -> VortexResult<ArrayRef> {
276    let wkb_type = GeoArrowType::WkbView(WkbType::new(geoarrow_metadata(&GeoMetadata::default())));
277    let wkb = cast(geo_array, &wkb_type)
278        .map_err(|e| vortex_err!("failed to cast geometry to WKB: {e}"))?;
279    ArrayRef::from_arrow(wkb.to_array_ref().as_ref(), false)
280}
281
282/// Recover [`GeoMetadata`] from GeoArrow metadata.
283pub(crate) fn geo_metadata_from_arrow(metadata: &Metadata) -> GeoMetadata {
284    let crs = metadata.crs().crs_value().map(|value| {
285        // `Crs::from_unknown_crs_type` stores the user's string verbatim as a JSON string
286        // value, so prefer the raw string when available to round-trip cleanly. For other
287        // CRS encodings (PROJJSON object, etc.), fall back to the JSON-encoded form.
288        value
289            .as_str()
290            .map(str::to_string)
291            .unwrap_or_else(|| value.to_string())
292    });
293    GeoMetadata { crs }
294}
295
296#[cfg(test)]
297mod tests {
298    use prost::Message;
299    use vortex_array::dtype::DType;
300    use vortex_error::VortexResult;
301    use vortex_error::vortex_err;
302
303    use super::LineString;
304    use super::MultiLineString;
305    use super::MultiPoint;
306    use super::Point;
307    use super::Polygon;
308    use super::native_geometry_scalar_from_wkb;
309    use crate::extension::GeoMetadata;
310
311    #[test]
312    fn test_metadata() {
313        let meta = GeoMetadata {
314            crs: Some("EPSG:4326".to_string()),
315        };
316
317        assert_eq!(meta.to_string(), "Geometry(crs=EPSG:4326)");
318        // round trip
319        let bytes = meta.encode_to_vec();
320        let decoded = GeoMetadata::decode(bytes.as_slice()).unwrap();
321        assert_eq!(decoded, meta);
322    }
323
324    /// A little-endian WKB `POINT` literal decodes to the native `Point` extension scalar.
325    #[test]
326    fn decodes_wkb_point_to_native() -> VortexResult<()> {
327        let mut wkb = vec![1u8]; // little-endian byte order
328        wkb.extend_from_slice(&1u32.to_le_bytes()); // geometry type: point
329        wkb.extend_from_slice(&1.0f64.to_le_bytes()); // x
330        wkb.extend_from_slice(&2.0f64.to_le_bytes()); // y
331
332        let scalar = native_geometry_scalar_from_wkb(&wkb)?.expect("a point scalar");
333        let DType::Extension(ext) = scalar.dtype() else {
334            panic!("expected an extension dtype, got {}", scalar.dtype());
335        };
336        assert!(ext.is::<Point>());
337        Ok(())
338    }
339
340    /// A little-endian WKB `POLYGON` literal decodes to the native `Polygon` extension scalar.
341    #[test]
342    fn decodes_wkb_polygon_to_native() -> VortexResult<()> {
343        let ring = [(0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0)];
344        let mut wkb = vec![1u8]; // little-endian byte order
345        wkb.extend_from_slice(&3u32.to_le_bytes()); // geometry type: polygon
346        wkb.extend_from_slice(&1u32.to_le_bytes()); // one ring
347        let ring_len = u32::try_from(ring.len()).map_err(|e| vortex_err!("{e}"))?;
348        wkb.extend_from_slice(&ring_len.to_le_bytes());
349        for (x, y) in ring {
350            wkb.extend_from_slice(&f64::to_le_bytes(x));
351            wkb.extend_from_slice(&f64::to_le_bytes(y));
352        }
353
354        let scalar = native_geometry_scalar_from_wkb(&wkb)?.expect("a polygon scalar");
355        let DType::Extension(ext) = scalar.dtype() else {
356            panic!("expected an extension dtype, got {}", scalar.dtype());
357        };
358        assert!(ext.is::<Polygon>());
359        Ok(())
360    }
361
362    /// A little-endian WKB `LINESTRING` literal decodes to the native `LineString` extension scalar.
363    #[test]
364    fn decodes_wkb_linestring_to_native() -> VortexResult<()> {
365        let points = [(0.0, 0.0), (1.0, 1.0)];
366        let mut wkb = vec![1u8]; // little-endian byte order
367        wkb.extend_from_slice(&2u32.to_le_bytes()); // geometry type: linestring
368        let len = u32::try_from(points.len()).map_err(|e| vortex_err!("{e}"))?;
369        wkb.extend_from_slice(&len.to_le_bytes());
370        for (x, y) in points {
371            wkb.extend_from_slice(&f64::to_le_bytes(x));
372            wkb.extend_from_slice(&f64::to_le_bytes(y));
373        }
374
375        let scalar = native_geometry_scalar_from_wkb(&wkb)?.expect("a linestring scalar");
376        let DType::Extension(ext) = scalar.dtype() else {
377            panic!("expected an extension dtype, got {}", scalar.dtype());
378        };
379        assert!(ext.is::<LineString>());
380        Ok(())
381    }
382
383    /// A little-endian WKB `MULTIPOINT` literal decodes to the native `MultiPoint` extension scalar.
384    #[test]
385    fn decodes_wkb_multipoint_to_native() -> VortexResult<()> {
386        let points = [(0.0, 0.0), (1.0, 1.0)];
387        let mut wkb = vec![1u8]; // little-endian byte order
388        wkb.extend_from_slice(&4u32.to_le_bytes()); // geometry type: multipoint
389        let len = u32::try_from(points.len()).map_err(|e| vortex_err!("{e}"))?;
390        wkb.extend_from_slice(&len.to_le_bytes());
391        for (x, y) in points {
392            // each member is a full WKB point
393            wkb.push(1u8);
394            wkb.extend_from_slice(&1u32.to_le_bytes());
395            wkb.extend_from_slice(&f64::to_le_bytes(x));
396            wkb.extend_from_slice(&f64::to_le_bytes(y));
397        }
398
399        let scalar = native_geometry_scalar_from_wkb(&wkb)?.expect("a multipoint scalar");
400        let DType::Extension(ext) = scalar.dtype() else {
401            panic!("expected an extension dtype, got {}", scalar.dtype());
402        };
403        assert!(ext.is::<MultiPoint>());
404        Ok(())
405    }
406
407    /// A little-endian WKB `MULTILINESTRING` literal decodes to the native `MultiLineString` scalar.
408    #[test]
409    fn decodes_wkb_multilinestring_to_native() -> VortexResult<()> {
410        let lines = [[(0.0, 0.0), (1.0, 1.0)], [(2.0, 2.0), (3.0, 3.0)]];
411        let mut wkb = vec![1u8]; // little-endian byte order
412        wkb.extend_from_slice(&5u32.to_le_bytes()); // geometry type: multilinestring
413        let num_lines = u32::try_from(lines.len()).map_err(|e| vortex_err!("{e}"))?;
414        wkb.extend_from_slice(&num_lines.to_le_bytes());
415        for line in lines {
416            // each member is a full WKB linestring
417            wkb.push(1u8);
418            wkb.extend_from_slice(&2u32.to_le_bytes());
419            let len = u32::try_from(line.len()).map_err(|e| vortex_err!("{e}"))?;
420            wkb.extend_from_slice(&len.to_le_bytes());
421            for (x, y) in line {
422                wkb.extend_from_slice(&f64::to_le_bytes(x));
423                wkb.extend_from_slice(&f64::to_le_bytes(y));
424            }
425        }
426
427        let scalar = native_geometry_scalar_from_wkb(&wkb)?.expect("a multilinestring scalar");
428        let DType::Extension(ext) = scalar.dtype() else {
429            panic!("expected an extension dtype, got {}", scalar.dtype());
430        };
431        assert!(ext.is::<MultiLineString>());
432        Ok(())
433    }
434}