1use std::sync::Arc;
13
14use arrow_array::ArrayRef as ArrowArrayRef;
15use arrow_schema::DataType;
16use arrow_schema::Field;
17use arrow_schema::extension::ExtensionType;
18use geo_traits::to_geo::ToGeoGeometry;
19use geo_types::Geometry;
20use geoarrow::array::GeoArrowArrayAccessor;
21use geoarrow::array::IntoArrow;
22use geoarrow::array::RectArray;
23use geoarrow::datatypes::BoxType;
24use prost::Message;
25use vortex_array::ArrayRef;
26use vortex_array::ExecutionCtx;
27use vortex_array::IntoArray;
28use vortex_array::arrays::ExtensionArray;
29use vortex_array::arrays::extension::ExtensionArrayExt;
30use vortex_array::dtype::DType;
31use vortex_array::dtype::FieldNames;
32use vortex_array::dtype::Nullability;
33use vortex_array::dtype::PType;
34use vortex_array::dtype::StructFields;
35use vortex_array::dtype::extension::ExtDType;
36use vortex_array::dtype::extension::ExtId;
37use vortex_array::dtype::extension::ExtVTable;
38use vortex_array::scalar::ScalarValue;
39use vortex_arrow::ArrowExport;
40use vortex_arrow::ArrowExportVTable;
41use vortex_arrow::ArrowImport;
42use vortex_arrow::ArrowImportVTable;
43use vortex_arrow::ArrowSession;
44use vortex_arrow::ArrowSessionExt;
45use vortex_arrow::FromArrowArray;
46use vortex_arrow::FromArrowType;
47use vortex_error::VortexResult;
48use vortex_error::vortex_bail;
49use vortex_error::vortex_ensure;
50use vortex_error::vortex_err;
51use vortex_session::registry::CachedId;
52use vortex_session::registry::Id;
53
54use super::GeoMetadata;
55use super::coordinate::Dimension;
56use super::geo_metadata_from_arrow;
57use super::geoarrow_metadata;
58
59#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
62pub struct Rect;
63
64impl ExtVTable for Rect {
65 type Metadata = GeoMetadata;
66 type NativeValue<'a> = &'a ScalarValue;
68
69 fn id(&self) -> ExtId {
70 static ID: CachedId = CachedId::new("vortex.geo.box");
71 *ID
72 }
73
74 fn serialize_metadata(&self, metadata: &Self::Metadata) -> VortexResult<Vec<u8>> {
75 Ok(metadata.encode_to_vec())
76 }
77
78 fn deserialize_metadata(&self, metadata: &[u8]) -> VortexResult<Self::Metadata> {
79 Ok(GeoMetadata::decode(metadata)?)
80 }
81
82 fn validate_dtype(ext_dtype: &ExtDType<Self>) -> VortexResult<()> {
83 box_dimension(ext_dtype.storage_dtype()).map(|_| ())
84 }
85
86 fn unpack_native<'a>(
87 _ext_dtype: &'a ExtDType<Self>,
88 storage_value: &'a ScalarValue,
89 ) -> VortexResult<&'a ScalarValue> {
90 Ok(storage_value)
91 }
92}
93
94fn box_field_names(dim: Dimension) -> &'static [&'static str] {
97 match dim {
98 Dimension::Xy => &["xmin", "ymin", "xmax", "ymax"],
99 Dimension::Xyz => &["xmin", "ymin", "zmin", "xmax", "ymax", "zmax"],
100 Dimension::Xym => &["xmin", "ymin", "mmin", "xmax", "ymax", "mmax"],
101 Dimension::Xyzm => &[
102 "xmin", "ymin", "zmin", "mmin", "xmax", "ymax", "zmax", "mmax",
103 ],
104 }
105}
106
107fn box_dimension_from_names(names: &FieldNames) -> Option<Dimension> {
109 [
110 Dimension::Xy,
111 Dimension::Xyz,
112 Dimension::Xym,
113 Dimension::Xyzm,
114 ]
115 .into_iter()
116 .find(|&dim| {
117 names
118 .iter()
119 .map(AsRef::as_ref)
120 .eq(box_field_names(dim).iter().copied())
121 })
122}
123
124pub(crate) fn box_storage_dtype(dim: Dimension, nullability: Nullability) -> DType {
127 let names = box_field_names(dim);
128 let fields = std::iter::repeat_n(
129 DType::Primitive(PType::F64, Nullability::NonNullable),
130 names.len(),
131 )
132 .collect::<Vec<_>>();
133 DType::Struct(
134 StructFields::new(FieldNames::from(names), fields),
135 nullability,
136 )
137}
138
139pub(crate) fn box_dimension(dtype: &DType) -> VortexResult<Dimension> {
142 let DType::Struct(fields, _) = dtype else {
143 vortex_bail!("box storage must be a Struct, was {dtype}");
144 };
145 for (name, field) in fields.names().iter().zip(fields.fields()) {
146 vortex_ensure!(
147 matches!(
148 field,
149 DType::Primitive(PType::F64, Nullability::NonNullable)
150 ),
151 "box field {name} must be non-nullable f64, was {field}"
152 );
153 }
154 box_dimension_from_names(fields.names())
155 .ok_or_else(|| vortex_err!("not a valid geoarrow.box dimension: {:?}", fields.names()))
156}
157
158static ARROW_BOX: CachedId = CachedId::new(BoxType::NAME);
159
160fn box_type(geo_metadata: &GeoMetadata, dimension: Dimension) -> BoxType {
162 BoxType::new(dimension.into(), geoarrow_metadata(geo_metadata))
163}
164
165fn rect_array(storage: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<RectArray> {
167 let box_type = box_type(&GeoMetadata::default(), box_dimension(storage.dtype())?);
168 let session = ctx.session().clone();
169 let arrow = session.arrow().execute_arrow(storage.clone(), None, ctx)?;
170 RectArray::try_from((arrow.as_ref(), box_type))
171 .map_err(|e| vortex_err!("failed to construct RectArray: {e}"))
172}
173
174pub(crate) fn rect_geometries(
176 storage: &ArrayRef,
177 ctx: &mut ExecutionCtx,
178) -> VortexResult<Vec<Geometry<f64>>> {
179 rect_array(storage, ctx)?
180 .iter()
181 .map(|geometry| -> VortexResult<Geometry<f64>> {
182 Ok(geometry
183 .ok_or_else(|| vortex_err!("geo: null geometry is not supported"))?
184 .map_err(|e| vortex_err!("geo: geometry access failed: {e}"))?
185 .to_geometry())
186 })
187 .collect()
188}
189
190impl ArrowExportVTable for Rect {
191 fn arrow_ext_id(&self) -> Id {
192 *ARROW_BOX
193 }
194
195 fn vortex_id(&self) -> Id {
196 self.id()
197 }
198
199 fn to_arrow_field(
200 &self,
201 name: &str,
202 dtype: &DType,
203 session: &ArrowSession,
204 ) -> VortexResult<Option<Field>> {
205 let ext_type = dtype.as_extension();
206 let geo_metadata = ext_type.metadata::<Rect>();
207 let dimension = box_dimension(ext_type.storage_dtype())?;
208
209 let mut field = session.to_arrow_field(name, ext_type.storage_dtype())?;
210 field.try_with_extension_type(box_type(geo_metadata, dimension))?;
211
212 Ok(Some(field))
213 }
214
215 fn execute_arrow(
216 &self,
217 array: ArrayRef,
218 target: &Field,
219 ctx: &mut ExecutionCtx,
220 ) -> VortexResult<ArrowExport> {
221 let is_box = array
222 .dtype()
223 .as_extension_opt()
224 .map(|ext| ext.is::<Rect>())
225 .unwrap_or(false);
226 if !is_box {
227 return Ok(ArrowExport::Unsupported(array));
228 }
229
230 let Ok(box_meta) = target.try_extension_type::<BoxType>() else {
231 return Ok(ArrowExport::Unsupported(array));
232 };
233
234 let executed = array.execute::<ExtensionArray>(ctx)?;
235 let storage = executed.storage_array().clone();
236
237 let storage_field = Field::new(
238 String::new(),
239 target.data_type().clone(),
240 target.is_nullable(),
241 );
242 let session = ctx.session().clone();
243 let arrow_storage = session
244 .arrow()
245 .execute_arrow(storage, Some(&storage_field), ctx)?;
246
247 let rects = RectArray::try_from((arrow_storage.as_ref(), box_meta))
249 .map_err(|e| vortex_err!("failed to construct RectArray: {e}"))?;
250
251 Ok(ArrowExport::Exported(Arc::new(rects.into_arrow())))
252 }
253}
254
255impl ArrowImportVTable for Rect {
256 fn arrow_ext_id(&self) -> Id {
257 *ARROW_BOX
258 }
259
260 fn from_arrow_field(&self, field: &Field) -> VortexResult<Option<DType>> {
264 let (dimension, metadata) = if let Ok(box_meta) = field.try_extension_type::<BoxType>() {
265 (
266 box_meta.dimension().into(),
267 geo_metadata_from_arrow(box_meta.metadata()),
268 )
269 } else {
270 if field.extension_type_name() != Some(BoxType::NAME) {
271 return Ok(None);
272 }
273 let DType::Struct(fields, _) = DType::from_arrow(field) else {
276 return Ok(None);
277 };
278 let Some(dimension) = box_dimension_from_names(fields.names()) else {
279 return Ok(None);
280 };
281 (dimension, GeoMetadata::default())
282 };
283
284 let storage_dtype = box_storage_dtype(dimension, field.is_nullable().into());
285 Ok(Some(DType::Extension(
286 ExtDType::try_with_vtable(Rect, metadata, storage_dtype)?.erased(),
287 )))
288 }
289
290 fn from_arrow_array(
291 &self,
292 array: ArrowArrayRef,
293 field: &Field,
294 dtype: &DType,
295 ) -> VortexResult<ArrowImport> {
296 let Some(ext_dtype) = dtype.as_extension_opt() else {
297 return Ok(ArrowImport::Unsupported(array));
298 };
299 if !ext_dtype.is::<Rect>()
300 || field.try_extension_type::<BoxType>().is_err()
301 || !matches!(array.data_type(), DataType::Struct(_))
302 {
303 return Ok(ArrowImport::Unsupported(array));
304 }
305
306 let storage = ArrayRef::from_arrow(array.as_ref(), field.is_nullable())?;
307 Ok(ArrowImport::Imported(
308 ExtensionArray::try_new(ext_dtype.clone(), storage)?.into_array(),
309 ))
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use geo_types::Geometry;
316 use rstest::rstest;
317 use vortex_array::VortexSessionExecute;
318 use vortex_array::arrays::ExtensionArray;
319 use vortex_array::arrays::extension::ExtensionArrayExt;
320 use vortex_array::dtype::DType;
321 use vortex_array::dtype::Nullability;
322 use vortex_array::dtype::PType;
323 use vortex_array::dtype::extension::ExtDType;
324 use vortex_error::VortexResult;
325
326 use super::Rect;
327 use super::box_dimension;
328 use super::box_storage_dtype;
329 use super::rect_geometries;
330 use crate::extension::GeoMetadata;
331 use crate::extension::coordinate::Dimension;
332
333 fn geo_meta() -> GeoMetadata {
334 GeoMetadata {
335 crs: Some("EPSG:4326".to_string()),
336 }
337 }
338
339 #[rstest]
342 #[case::xy(Dimension::Xy)]
343 #[case::xyz(Dimension::Xyz)]
344 #[case::xym(Dimension::Xym)]
345 #[case::xyzm(Dimension::Xyzm)]
346 fn box_validates_every_dimension(#[case] dim: Dimension) -> VortexResult<()> {
347 let storage = box_storage_dtype(dim, Nullability::NonNullable);
348 ExtDType::<Rect>::try_new(geo_meta(), storage.clone())?;
349 assert_eq!(box_dimension(&storage)?, dim);
350 Ok(())
351 }
352
353 #[test]
356 fn box_rejects_invalid_storage() -> VortexResult<()> {
357 let primitive = DType::Primitive(PType::F64, Nullability::NonNullable);
358 assert!(ExtDType::<Rect>::try_new(geo_meta(), primitive).is_err());
359
360 let point_like = box_storage_dtype(Dimension::Xy, Nullability::NonNullable);
361 let DType::Struct(fields, _) = &point_like else {
362 unreachable!("box storage is a struct");
363 };
364 let renamed = DType::Struct(
366 vortex_array::dtype::StructFields::new(
367 ["x", "ymin", "xmax", "ymax"].into(),
368 fields.fields().collect(),
369 ),
370 Nullability::NonNullable,
371 );
372 assert!(ExtDType::<Rect>::try_new(geo_meta(), renamed).is_err());
373 Ok(())
374 }
375
376 #[test]
378 fn box_decodes_to_geo_rect() -> VortexResult<()> {
379 let session = vortex_array::array_session();
380 let mut ctx = session.create_execution_ctx();
381
382 let rects = crate::test_harness::rect_column(vec![(0.0, 0.0, 2.0, 3.0)])?;
383 let storage = rects
384 .execute::<ExtensionArray>(&mut ctx)?
385 .storage_array()
386 .clone();
387
388 let geometries = rect_geometries(&storage, &mut ctx)?;
389 assert!(matches!(geometries.as_slice(), [Geometry::Rect(_)]));
390 Ok(())
391 }
392}