vortex_geo/extension/
multilinestring.rs1use 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#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
61pub struct MultiLineString;
62
63impl ExtVTable for MultiLineString {
64 type Metadata = GeoMetadata;
65 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
93pub(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
101pub(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
114fn multilinestring_type(geo_metadata: &GeoMetadata, dimension: Dimension) -> MultiLineStringType {
116 MultiLineStringType::new(dimension.into(), geoarrow_metadata(geo_metadata))
117}
118
119pub(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
135fn 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
150pub 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 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 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 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 #[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 #[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 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}