Skip to main content

vortex_geo/extension/
multilinestring.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! The [`MultiLineString`] extension type (`vortex.geo.multilinestring`), stored as
5//! `List<List<Struct<x, y[, z][, m]>>>` (line strings → coordinates) and tagged with
6//! [`GeoMetadata`]. The storage layout matches [`Polygon`](super::Polygon); the two are
7//! distinguished by their GeoArrow extension name, not their shape.
8
9use std::sync::Arc;
10
11use arrow_array::ArrayRef as ArrowArrayRef;
12use arrow_schema::DataType;
13use arrow_schema::Field;
14use arrow_schema::extension::ExtensionType;
15use geo_traits::to_geo::ToGeoGeometry;
16use geo_types::Geometry;
17use geoarrow::array::GeoArrowArrayAccessor;
18use geoarrow::array::IntoArrow;
19use geoarrow::array::MultiLineStringArray;
20use geoarrow::datatypes::CoordType;
21use geoarrow::datatypes::MultiLineStringType;
22use prost::Message;
23use vortex_array::ArrayRef;
24use vortex_array::ExecutionCtx;
25use vortex_array::IntoArray;
26use vortex_array::arrays::ExtensionArray;
27use vortex_array::arrays::extension::ExtensionArrayExt;
28use vortex_array::arrow::ArrowExport;
29use vortex_array::arrow::ArrowExportVTable;
30use vortex_array::arrow::ArrowImport;
31use vortex_array::arrow::ArrowImportVTable;
32use vortex_array::arrow::ArrowSession;
33use vortex_array::arrow::ArrowSessionExt;
34use vortex_array::arrow::FromArrowArray;
35use vortex_array::dtype::DType;
36use vortex_array::dtype::Nullability;
37use vortex_array::dtype::arrow::FromArrowType;
38use vortex_array::dtype::extension::ExtDType;
39use vortex_array::dtype::extension::ExtId;
40use vortex_array::dtype::extension::ExtVTable;
41use vortex_array::scalar::ScalarValue;
42use vortex_error::VortexError;
43use vortex_error::VortexResult;
44use vortex_error::vortex_bail;
45use vortex_error::vortex_ensure;
46use vortex_error::vortex_err;
47use vortex_session::registry::CachedId;
48use vortex_session::registry::Id;
49
50use super::GeoMetadata;
51use super::coordinate::Dimension;
52use super::coordinate::coordinate_dimension;
53use super::coordinate::coordinate_storage_dtype;
54use super::geo_metadata_from_arrow;
55use super::geoarrow_metadata;
56use super::geoarrow_to_wkb;
57
58/// A multilinestring: `geoarrow.multilinestring`, stored as `List<List<Struct<x, y[, z][, m]>>>`
59/// (line strings of vertices).
60#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
61pub struct MultiLineString;
62
63impl ExtVTable for MultiLineString {
64    type Metadata = GeoMetadata;
65    // No cheap owned value like Point's `Coordinate`; expose the raw storage scalar.
66    type NativeValue<'a> = &'a ScalarValue;
67
68    fn id(&self) -> ExtId {
69        static ID: CachedId = CachedId::new("vortex.geo.multilinestring");
70        *ID
71    }
72
73    fn serialize_metadata(&self, metadata: &Self::Metadata) -> VortexResult<Vec<u8>> {
74        Ok(metadata.encode_to_vec())
75    }
76
77    fn deserialize_metadata(&self, metadata: &[u8]) -> VortexResult<Self::Metadata> {
78        Ok(GeoMetadata::decode(metadata)?)
79    }
80
81    fn validate_dtype(ext_dtype: &ExtDType<Self>) -> VortexResult<()> {
82        multilinestring_dimension(ext_dtype.storage_dtype()).map(|_| ())
83    }
84
85    fn unpack_native<'a>(
86        _ext_dtype: &'a ExtDType<Self>,
87        storage_value: &'a ScalarValue,
88    ) -> VortexResult<&'a ScalarValue> {
89        Ok(storage_value)
90    }
91}
92
93/// Canonical multilinestring storage: an outer list of line strings, each a list of the coordinate
94/// `Struct`.
95pub(crate) fn multilinestring_storage_dtype(dim: Dimension, nullability: Nullability) -> DType {
96    let coords = coordinate_storage_dtype(dim, Nullability::NonNullable);
97    let line = DType::List(Arc::new(coords), Nullability::NonNullable);
98    DType::List(Arc::new(line), nullability)
99}
100
101/// Validate `dtype` is `List<List<coordinate-struct>>` and return its [`Dimension`].
102pub(crate) fn multilinestring_dimension(dtype: &DType) -> VortexResult<Dimension> {
103    let DType::List(line, _) = dtype else {
104        vortex_bail!("multilinestring storage must be a List of line strings, was {dtype}");
105    };
106    let DType::List(coords, _) = line.as_ref() else {
107        vortex_bail!("multilinestring line storage must be a List of coordinates, was {line}");
108    };
109    coordinate_dimension(coords)
110}
111
112static ARROW_MULTILINESTRING: CachedId = CachedId::new(MultiLineStringType::NAME);
113
114/// The `geoarrow.multilinestring` type for `dimension`, with separated (struct) coordinates.
115fn multilinestring_type(geo_metadata: &GeoMetadata, dimension: Dimension) -> MultiLineStringType {
116    MultiLineStringType::new(dimension.into(), geoarrow_metadata(geo_metadata))
117}
118
119/// Decode storage to `geo_types` for the geo scalar functions (CRS is irrelevant to planar ops).
120pub(crate) fn multilinestring_geometries(
121    storage: &ArrayRef,
122    ctx: &mut ExecutionCtx,
123) -> VortexResult<Vec<Geometry<f64>>> {
124    multilinestring_array(storage, ctx)?
125        .iter()
126        .map(|geometry| -> VortexResult<Geometry<f64>> {
127            Ok(geometry
128                .ok_or_else(|| vortex_err!("geo: null geometry is not supported"))?
129                .map_err(|e| vortex_err!("geo: geometry access failed: {e}"))?
130                .to_geometry())
131        })
132        .collect()
133}
134
135/// Build a geoarrow `MultiLineStringArray` from the `MultiLineString` storage.
136fn multilinestring_array(
137    storage: &ArrayRef,
138    ctx: &mut ExecutionCtx,
139) -> VortexResult<MultiLineStringArray> {
140    let multilinestring_type = multilinestring_type(
141        &GeoMetadata::default(),
142        multilinestring_dimension(storage.dtype())?,
143    );
144    let session = ctx.session().clone();
145    let arrow = session.arrow().execute_arrow(storage.clone(), None, ctx)?;
146    MultiLineStringArray::try_from((arrow.as_ref(), multilinestring_type))
147        .map_err(|e| vortex_err!("failed to construct MultiLineStringArray: {e}"))
148}
149
150/// A validated `MultiLineString` array (`try_from` checks the extension type).
151pub struct MultiLineStringData(ExtensionArray);
152
153impl TryFrom<ExtensionArray> for MultiLineStringData {
154    type Error = VortexError;
155
156    fn try_from(ext: ExtensionArray) -> Result<Self, Self::Error> {
157        vortex_ensure!(
158            ext.ext_dtype().is::<MultiLineString>(),
159            "expected a MultiLineString extension array"
160        );
161        Ok(MultiLineStringData(ext))
162    }
163}
164
165impl MultiLineStringData {
166    /// Serialize multilinestrings to WKB (a view array) — the form DuckDB `GEOMETRY` takes.
167    pub fn to_wkb(&self, ctx: &mut ExecutionCtx) -> VortexResult<ArrayRef> {
168        geoarrow_to_wkb(&multilinestring_array(self.0.storage_array(), ctx)?)
169    }
170}
171
172impl ArrowExportVTable for MultiLineString {
173    fn arrow_ext_id(&self) -> Id {
174        *ARROW_MULTILINESTRING
175    }
176
177    fn vortex_id(&self) -> Id {
178        self.id()
179    }
180
181    fn to_arrow_field(
182        &self,
183        name: &str,
184        dtype: &DType,
185        session: &ArrowSession,
186    ) -> VortexResult<Option<Field>> {
187        let ext_type = dtype.as_extension();
188        let geo_metadata = ext_type.metadata::<MultiLineString>();
189        let dimension = multilinestring_dimension(ext_type.storage_dtype())?;
190
191        let mut field = session.to_arrow_field(name, ext_type.storage_dtype())?;
192        field.try_with_extension_type(multilinestring_type(geo_metadata, dimension))?;
193
194        Ok(Some(field))
195    }
196
197    fn execute_arrow(
198        &self,
199        array: ArrayRef,
200        target: &Field,
201        ctx: &mut ExecutionCtx,
202    ) -> VortexResult<ArrowExport> {
203        let is_multilinestring = array
204            .dtype()
205            .as_extension_opt()
206            .map(|ext| ext.is::<MultiLineString>())
207            .unwrap_or(false);
208        if !is_multilinestring {
209            return Ok(ArrowExport::Unsupported(array));
210        }
211
212        let Ok(multilinestring_meta) = target.try_extension_type::<MultiLineStringType>() else {
213            return Ok(ArrowExport::Unsupported(array));
214        };
215        if multilinestring_meta.coord_type() != CoordType::Separated {
216            return Ok(ArrowExport::Unsupported(array));
217        }
218
219        let executed = array.execute::<ExtensionArray>(ctx)?;
220        let storage = executed.storage_array().clone();
221
222        let storage_field = Field::new(
223            String::new(),
224            target.data_type().clone(),
225            target.is_nullable(),
226        );
227        let session = ctx.session().clone();
228        let arrow_storage = session
229            .arrow()
230            .execute_arrow(storage, Some(&storage_field), ctx)?;
231
232        let multilinestrings =
233            MultiLineStringArray::try_from((arrow_storage.as_ref(), multilinestring_meta))
234                .map_err(|e| vortex_err!("failed to construct MultiLineStringArray: {e}"))?;
235
236        Ok(ArrowExport::Exported(Arc::new(
237            multilinestrings.into_arrow(),
238        )))
239    }
240}
241
242impl ArrowImportVTable for MultiLineString {
243    fn arrow_ext_id(&self) -> Id {
244        *ARROW_MULTILINESTRING
245    }
246
247    /// Import a `geoarrow.multilinestring` field (matched by GeoArrow name). Accepts the full
248    /// `MultiLineStringType`, or a metadata-less literal (name only), inferring the dimension.
249    fn from_arrow_field(&self, field: &Field) -> VortexResult<Option<DType>> {
250        let (dimension, metadata) =
251            if let Ok(multilinestring_meta) = field.try_extension_type::<MultiLineStringType>() {
252                vortex_ensure!(
253                    multilinestring_meta.coord_type() == CoordType::Separated,
254                    "geoarrow.multilinestring with interleaved coordinates is not supported; \
255                 re-encode with separated (struct) coordinates"
256                );
257                (
258                    multilinestring_meta.dimension().into(),
259                    geo_metadata_from_arrow(multilinestring_meta.metadata()),
260                )
261            } else {
262                // Literal: peel the two `List` layers to the coordinate struct and read its dimension
263                // from the field names (the canonical check rejects nullable coordinates).
264                if field.extension_type_name() != Some(MultiLineStringType::NAME) {
265                    return Ok(None);
266                }
267                let DType::List(line, _) = DType::from_arrow(field) else {
268                    return Ok(None);
269                };
270                let DType::List(coords, _) = line.as_ref() else {
271                    return Ok(None);
272                };
273                let DType::Struct(fields, _) = coords.as_ref() else {
274                    return Ok(None);
275                };
276                let Ok(dimension) = Dimension::from_field_names(fields.names()) else {
277                    return Ok(None);
278                };
279                (dimension, GeoMetadata::default())
280            };
281
282        let storage_dtype = multilinestring_storage_dtype(dimension, field.is_nullable().into());
283        Ok(Some(DType::Extension(
284            ExtDType::try_with_vtable(MultiLineString, metadata, storage_dtype)?.erased(),
285        )))
286    }
287
288    fn from_arrow_array(
289        &self,
290        array: ArrowArrayRef,
291        field: &Field,
292        dtype: &DType,
293    ) -> VortexResult<ArrowImport> {
294        let Some(ext_dtype) = dtype.as_extension_opt() else {
295            return Ok(ArrowImport::Unsupported(array));
296        };
297        if !ext_dtype.is::<MultiLineString>()
298            || field.try_extension_type::<MultiLineStringType>().is_err()
299            || !matches!(array.data_type(), DataType::List(_))
300        {
301            return Ok(ArrowImport::Unsupported(array));
302        }
303
304        let storage = ArrayRef::from_arrow(array.as_ref(), field.is_nullable())?;
305        Ok(ArrowImport::Imported(
306            ExtensionArray::try_new(ext_dtype.clone(), storage)?.into_array(),
307        ))
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use std::sync::Arc;
314
315    use rstest::rstest;
316    use vortex_array::dtype::DType;
317    use vortex_array::dtype::Nullability;
318    use vortex_array::dtype::PType;
319    use vortex_array::dtype::extension::ExtDType;
320    use vortex_error::VortexResult;
321
322    use super::MultiLineString;
323    use super::multilinestring_storage_dtype;
324    use crate::extension::GeoMetadata;
325    use crate::extension::coordinate::Dimension;
326    use crate::extension::coordinate::coordinate_storage_dtype;
327
328    fn geo_meta() -> GeoMetadata {
329        GeoMetadata {
330            crs: Some("EPSG:4326".to_string()),
331        }
332    }
333
334    /// `MultiLineString` accepts the canonical `List<List<coordinate-struct>>` storage of every
335    /// dimension.
336    #[rstest]
337    #[case::xy(Dimension::Xy)]
338    #[case::xyz(Dimension::Xyz)]
339    #[case::xym(Dimension::Xym)]
340    #[case::xyzm(Dimension::Xyzm)]
341    fn multilinestring_validates_every_dimension(#[case] dim: Dimension) -> VortexResult<()> {
342        let storage = multilinestring_storage_dtype(dim, Nullability::NonNullable);
343        ExtDType::<MultiLineString>::try_new(geo_meta(), storage)?;
344        Ok(())
345    }
346
347    /// Non-multilinestring storage is rejected: a bare coordinate struct (point) fails.
348    #[test]
349    fn multilinestring_rejects_invalid_storage() -> VortexResult<()> {
350        let primitive = DType::Primitive(PType::F64, Nullability::NonNullable);
351        assert!(ExtDType::<MultiLineString>::try_new(geo_meta(), primitive).is_err());
352
353        // A bare list of coordinates is a single line string, not a multilinestring.
354        let coords = coordinate_storage_dtype(Dimension::Xy, Nullability::NonNullable);
355        let line = DType::List(Arc::new(coords), Nullability::NonNullable);
356        assert!(ExtDType::<MultiLineString>::try_new(geo_meta(), line).is_err());
357        Ok(())
358    }
359}