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