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