Skip to main content

vortex_geo/extension/
wkb.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Display;
5use std::ops::Deref;
6use std::sync::Arc;
7
8use arrow_array::ArrayRef as ArrowArrayRef;
9use arrow_schema::DataType;
10use arrow_schema::Field;
11use arrow_schema::extension::ExtensionType;
12use geoarrow::array::GenericWkbArray;
13use geoarrow::array::IntoArrow;
14use geoarrow::array::WkbViewArray;
15use geoarrow::datatypes::WkbType;
16use prost::Message;
17use vortex_array::ArrayRef;
18use vortex_array::ExecutionCtx;
19use vortex_array::IntoArray;
20use vortex_array::arrays::ExtensionArray;
21use vortex_array::arrays::extension::ExtensionArrayExt;
22use vortex_array::arrow::ArrowExport;
23use vortex_array::arrow::ArrowExportVTable;
24use vortex_array::arrow::ArrowImport;
25use vortex_array::arrow::ArrowImportVTable;
26use vortex_array::arrow::ArrowSession;
27use vortex_array::arrow::ArrowSessionExt;
28use vortex_array::arrow::FromArrowArray;
29use vortex_array::dtype::DType;
30use vortex_array::dtype::extension::ExtDType;
31use vortex_array::dtype::extension::ExtId;
32use vortex_array::dtype::extension::ExtVTable;
33use vortex_array::scalar::ScalarValue;
34use vortex_error::VortexError;
35use vortex_error::VortexResult;
36use vortex_error::vortex_ensure;
37use vortex_error::vortex_err;
38use vortex_session::registry::CachedId;
39use vortex_session::registry::Id;
40use wkb::reader::GeometryType;
41
42use crate::extension::GeoMetadata;
43use crate::extension::geo_metadata_from_arrow;
44use crate::extension::geoarrow_metadata;
45
46/// A typed handle to an [`ExtensionArray`] that contains WKB-encoded data.
47///
48/// You can construct this safely using `WellKnownBinaryData::try_from(ExtensionArray)`.
49#[derive(Debug, Clone)]
50pub struct WellKnownBinaryData {
51    ext: ExtensionArray,
52}
53
54impl WellKnownBinaryData {
55    /// A reference to the array that holds the Well-Known Binary scalar values.
56    pub fn wkb_values(&self) -> &ArrayRef {
57        self.ext.storage_array()
58    }
59
60    /// A reference to the [geospatial metadata][GeoMetadata].
61    pub fn geo_metadata(&self) -> &GeoMetadata {
62        self.ext
63            .dtype()
64            .as_extension()
65            .metadata::<WellKnownBinary>()
66    }
67}
68
69impl TryFrom<ExtensionArray> for WellKnownBinaryData {
70    type Error = VortexError;
71
72    fn try_from(ext: ExtensionArray) -> Result<Self, Self::Error> {
73        vortex_ensure!(
74            ext.ext_dtype().is::<WellKnownBinary>(),
75            "array extension dtype {} is not a WKB",
76            ext.ext_dtype()
77        );
78
79        Ok(Self { ext })
80    }
81}
82
83/// An [extension type][ExtVTable] for OGC Well-known Binary (WKB) data format.
84///
85/// This is one of the most common formats for sharing of geometry data between analytic systems,
86/// used by DuckDB, PostGIS and GeoParquet.
87#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
88pub struct WellKnownBinary;
89
90/// A reference to a value of well-known binary encoded geometry data.
91///
92/// Interpreting this value is dependent on the [geospatial metadata][GeoMetadata] in the extension
93/// schema for the array where this scalar is taken from.
94pub struct Wkb<'a>(wkb::reader::Wkb<'a>);
95
96impl<'a> Wkb<'a> {
97    /// Attempt to decode a well-known binary value from a byte slice.
98    ///
99    /// This will not cause any data allocations or copies, but it will perform a one-pass
100    /// validation on the structure of the WKB.
101    pub fn try_from_bytes(bytes: &'a [u8]) -> VortexResult<Self> {
102        wkb::reader::Wkb::try_new(bytes)
103            .map_err(|e| vortex_err!("failed parsing WKB: {e}"))
104            .map(Wkb)
105    }
106}
107
108impl<'a> Display for Wkb<'a> {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        let geometry_kind = match self.0.geometry_type() {
111            GeometryType::Point => "point",
112            GeometryType::LineString => "linestring",
113            GeometryType::Polygon => "polygon",
114            GeometryType::MultiPoint => "multipoint",
115            GeometryType::MultiLineString => "multilinestring",
116            GeometryType::MultiPolygon => "multipolygon",
117            GeometryType::GeometryCollection => "geometrycollection",
118            _ => "unknown",
119        };
120        // TODO(aduffy): make this more useful
121        write!(f, "WKB({geometry_kind})")
122    }
123}
124
125impl<'a> Deref for Wkb<'a> {
126    type Target = wkb::reader::Wkb<'a>;
127
128    fn deref(&self) -> &wkb::reader::Wkb<'a> {
129        &self.0
130    }
131}
132
133impl ExtVTable for WellKnownBinary {
134    type Metadata = GeoMetadata;
135
136    type NativeValue<'a> = Wkb<'a>;
137
138    fn id(&self) -> ExtId {
139        static ID: CachedId = CachedId::new("vortex.geo.wkb");
140        *ID
141    }
142
143    fn serialize_metadata(&self, metadata: &Self::Metadata) -> VortexResult<Vec<u8>> {
144        Ok(metadata.encode_to_vec())
145    }
146
147    fn deserialize_metadata(&self, metadata: &[u8]) -> VortexResult<Self::Metadata> {
148        Ok(GeoMetadata::decode(metadata)?)
149    }
150
151    fn validate_dtype(ext_dtype: &ExtDType<Self>) -> VortexResult<()> {
152        vortex_ensure!(
153            ext_dtype.storage_dtype().is_binary(),
154            "geo.wkb must have binary storage type, was {}",
155            ext_dtype.storage_dtype()
156        );
157
158        Ok(())
159    }
160
161    fn unpack_native<'a>(
162        _ext_dtype: &'a ExtDType<Self>,
163        storage_value: &'a ScalarValue,
164    ) -> VortexResult<Self::NativeValue<'a>> {
165        Wkb::try_from_bytes(storage_value.as_binary().as_slice())
166    }
167}
168
169static ARROW_WKB: CachedId = CachedId::new(WkbType::NAME);
170
171impl ArrowExportVTable for WellKnownBinary {
172    fn arrow_ext_id(&self) -> Id {
173        *ARROW_WKB
174    }
175
176    fn vortex_id(&self) -> Id {
177        self.id()
178    }
179
180    fn to_arrow_field(
181        &self,
182        name: &str,
183        dtype: &DType,
184        session: &ArrowSession,
185    ) -> VortexResult<Option<Field>> {
186        let ext_type = dtype.as_extension();
187        let geo_metadata = ext_type.metadata::<WellKnownBinary>();
188
189        let mut field = session.to_arrow_field(name, ext_type.storage_dtype())?;
190        field.try_with_extension_type(wkb_type(geo_metadata))?;
191
192        Ok(Some(field))
193    }
194
195    fn execute_arrow(
196        &self,
197        array: ArrayRef,
198        target: &Field,
199        ctx: &mut ExecutionCtx,
200    ) -> VortexResult<ArrowExport> {
201        let is_wkb = array
202            .dtype()
203            .as_extension_opt()
204            .map(|ext| ext.is::<WellKnownBinary>())
205            .unwrap_or(false);
206        if !is_wkb {
207            return Ok(ArrowExport::Unsupported(array));
208        }
209
210        let Ok(wkb_meta) = target.try_extension_type::<WkbType>() else {
211            return Ok(ArrowExport::Unsupported(array));
212        };
213
214        let executed = array.execute::<ExtensionArray>(ctx)?;
215        let storage = executed.storage_array().clone();
216
217        let storage_field = Field::new(
218            String::new(),
219            target.data_type().clone(),
220            target.is_nullable(),
221        );
222        let session = ctx.session().clone();
223        let arrow_storage = session
224            .arrow()
225            .execute_arrow(storage, Some(&storage_field), ctx)?;
226
227        // Round-trip through the GeoArrow WKB array types: this validates that the storage
228        // is a binary-family Arrow array and produces the canonical physical representation
229        // expected for a `WkbType` extension field.
230        let arrow_ref: ArrowArrayRef = match target.data_type() {
231            DataType::Binary => Arc::new(
232                GenericWkbArray::<i32>::try_from((arrow_storage.as_ref(), wkb_meta))
233                    .map_err(|e| vortex_err!("failed to construct WkbArray: {e}"))?
234                    .into_arrow(),
235            ),
236            DataType::LargeBinary => Arc::new(
237                GenericWkbArray::<i64>::try_from((arrow_storage.as_ref(), wkb_meta))
238                    .map_err(|e| vortex_err!("failed to construct LargeWkbArray: {e}"))?
239                    .into_arrow(),
240            ),
241            DataType::BinaryView => Arc::new(
242                WkbViewArray::try_from((arrow_storage.as_ref(), wkb_meta))
243                    .map_err(|e| vortex_err!("failed to construct WkbViewArray: {e}"))?
244                    .into_arrow(),
245            ),
246            _ => unreachable!("target data type was validated above"),
247        };
248
249        Ok(ArrowExport::Exported(arrow_ref))
250    }
251}
252
253impl ArrowImportVTable for WellKnownBinary {
254    fn arrow_ext_id(&self) -> Id {
255        *ARROW_WKB
256    }
257
258    fn from_arrow_field(&self, field: &Field) -> VortexResult<Option<DType>> {
259        let Ok(wkb_meta) = field.try_extension_type::<WkbType>() else {
260            return Ok(None);
261        };
262
263        let storage_dtype = DType::Binary(field.is_nullable().into());
264        Ok(Some(DType::Extension(
265            ExtDType::try_with_vtable(WellKnownBinary, geo_metadata(&wkb_meta), storage_dtype)?
266                .erased(),
267        )))
268    }
269
270    fn from_arrow_array(
271        &self,
272        array: ArrowArrayRef,
273        field: &Field,
274        dtype: &DType,
275    ) -> VortexResult<ArrowImport> {
276        let Some(ext_dtype) = dtype.as_extension_opt() else {
277            return Ok(ArrowImport::Unsupported(array));
278        };
279        if !ext_dtype.is::<WellKnownBinary>()
280            || field.try_extension_type::<WkbType>().is_err()
281            || !matches!(
282                array.data_type(),
283                DataType::Binary | DataType::LargeBinary | DataType::BinaryView
284            )
285        {
286            return Ok(ArrowImport::Unsupported(array));
287        }
288
289        let storage = ArrayRef::from_arrow(array.as_ref(), field.is_nullable())?;
290        Ok(ArrowImport::Imported(
291            ExtensionArray::new(ext_dtype.clone(), storage).into_array(),
292        ))
293    }
294}
295
296fn wkb_type(geo_metadata: &GeoMetadata) -> WkbType {
297    WkbType::new(geoarrow_metadata(geo_metadata))
298}
299
300fn geo_metadata(wkb_type: &WkbType) -> GeoMetadata {
301    geo_metadata_from_arrow(wkb_type.metadata())
302}