@@ -47,6 +47,7 @@ use vortex::scalar::PrimitiveScalar;
4747use vortex:: scalar:: Scalar ;
4848use vortex:: scalar:: ScalarValue ;
4949use vortex:: scalar:: Utf8Scalar ;
50+ use vortex_geo:: extension:: WellKnownBinary ;
5051
5152use crate :: convert:: dtype:: FromLogicalType ;
5253use crate :: duckdb:: LogicalType ;
@@ -169,9 +170,22 @@ impl ToDuckDBScalar for BinaryScalar<'_> {
169170}
170171
171172impl ToDuckDBScalar for ExtScalar < ' _ > {
172- /// Converts an extension scalar (primarily temporal types) to a DuckDB value.
173+ /// Converts an extension scalar (temporal types or `WellKnownBinary` geometries) to a DuckDB
174+ /// value.
173175 fn try_to_duckdb_scalar ( & self ) -> VortexResult < Value > {
174176 let logical_type = LogicalType :: try_from ( & DType :: Extension ( self . ext_dtype ( ) . clone ( ) ) ) ?;
177+
178+ if let Some ( wkb) = self . ext_dtype ( ) . metadata_opt :: < WellKnownBinary > ( ) {
179+ let storage = self . to_storage_scalar ( ) ;
180+ let binary = storage
181+ . as_binary_opt ( )
182+ . ok_or_else ( || vortex_err ! ( "WellKnownBinary storage must be a binary scalar" ) ) ?;
183+ return Ok ( match binary. value ( ) {
184+ Some ( bytes) => Value :: new_geometry ( bytes. as_slice ( ) , wkb. crs . as_deref ( ) ) ?,
185+ None => Value :: null ( & logical_type) ,
186+ } ) ;
187+ }
188+
175189 let Some ( temporal) = self . ext_dtype ( ) . metadata_opt :: < AnyTemporal > ( ) else {
176190 vortex_bail ! ( "Cannot convert non-temporal extension scalar to duckdb value" ) ;
177191 } ;
@@ -266,7 +280,14 @@ impl<'a> TryFrom<&'a ValueRef> for Scalar {
266280 ExtractedValue :: Float ( v) => Ok ( Scalar :: primitive ( v, Nullable ) ) ,
267281 ExtractedValue :: Double ( v) => Ok ( Scalar :: primitive ( v, Nullable ) ) ,
268282 ExtractedValue :: Varchar ( s) => Ok ( Scalar :: utf8 ( s, Nullable ) ) ,
269- ExtractedValue :: Blob ( b) => Ok ( Scalar :: binary ( b, Nullable ) ) ,
283+ ExtractedValue :: Blob ( b) => match & dtype {
284+ DType :: Binary ( _) => Ok ( Scalar :: binary ( b, Nullable ) ) ,
285+ DType :: Extension ( ext) if ext. is :: < WellKnownBinary > ( ) => Ok ( Scalar :: extension_ref (
286+ ext. clone ( ) ,
287+ Scalar :: binary ( b, Nullable ) ,
288+ ) ) ,
289+ _ => vortex_bail ! ( "Cannot convert DuckDB blob to Vortex scalar of dtype {dtype}" ) ,
290+ } ,
270291 ExtractedValue :: Date ( days) => Ok ( Scalar :: extension :: < Date > (
271292 TimeUnit :: Days ,
272293 Scalar :: try_new (
@@ -350,11 +371,19 @@ impl<'a> TryFrom<&'a ValueRef> for Scalar {
350371
351372#[ cfg( test) ]
352373mod tests {
374+ use rstest:: rstest;
375+ use vortex:: dtype:: DType ;
376+ use vortex:: dtype:: Nullability ;
377+ use vortex:: dtype:: extension:: ExtDType ;
353378 use vortex:: extension:: datetime:: Timestamp ;
354379 use vortex:: extension:: datetime:: TimestampOptions ;
355380 use vortex:: scalar:: Scalar ;
381+ use vortex_geo:: extension:: GeoMetadata ;
382+ use vortex_geo:: extension:: WellKnownBinary ;
356383
357384 use crate :: convert:: ToDuckDBScalar ;
385+ use crate :: cpp:: DUCKDB_TYPE ;
386+ use crate :: duckdb:: Value ;
358387
359388 #[ test]
360389 fn test_scalar_round_trip ( ) {
@@ -413,4 +442,83 @@ mod tests {
413442 assert_eq ! ( original_scalar, roundtrip_scalar) ;
414443 }
415444 }
445+
446+ /// Sample WKB bytes for `POINT(1 2)` little-endian.
447+ fn sample_wkb ( ) -> Vec < u8 > {
448+ vec ! [
449+ 0x01 , // little-endian
450+ 0x01 , 0x00 , 0x00 , 0x00 , // type = 1 (Point)
451+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0xf0 , 0x3f , // x = 1.0
452+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x40 , // y = 2.0
453+ ]
454+ }
455+
456+ fn wkb_scalar ( crs : Option < & str > , bytes : & [ u8 ] ) -> Scalar {
457+ Scalar :: extension :: < WellKnownBinary > (
458+ GeoMetadata {
459+ crs : crs. map ( str:: to_string) ,
460+ } ,
461+ Scalar :: binary ( bytes. to_vec ( ) , Nullability :: Nullable ) ,
462+ )
463+ }
464+
465+ #[ rstest]
466+ #[ case:: with_crs( Some ( "EPSG:4326" ) ) ]
467+ #[ case:: no_crs( None ) ]
468+ #[ case:: empty_crs( Some ( "" ) ) ]
469+ fn test_geometry_value_extract_round_trip ( #[ case] crs : Option < & str > ) {
470+ let bytes = sample_wkb ( ) ;
471+ let value = Value :: new_geometry ( & bytes, crs) . unwrap ( ) ;
472+
473+ // The constructed value must be a GEOMETRY logical type.
474+ assert_eq ! (
475+ value. logical_type( ) . as_type_id( ) ,
476+ DUCKDB_TYPE :: DUCKDB_TYPE_GEOMETRY
477+ ) ;
478+
479+ // Extract back: bytes round-trip exactly.
480+ let scalar: Scalar = ( & * value) . try_into ( ) . unwrap ( ) ;
481+ let ext = scalar. as_extension ( ) ;
482+ let storage = ext. to_storage_scalar ( ) ;
483+ let storage_binary = storage. as_binary ( ) ;
484+ assert_eq ! ( storage_binary. value( ) . unwrap( ) . as_slice( ) , bytes. as_slice( ) ) ;
485+
486+ // The extension dtype should be `WellKnownBinary` and CRS should round-trip,
487+ // with the documented quirk that `Some("")` collapses to `None` through DuckDB.
488+ let metadata = ext. ext_dtype ( ) . metadata :: < WellKnownBinary > ( ) ;
489+ match crs {
490+ Some ( "" ) | None => assert_eq ! ( metadata. crs, None ) ,
491+ Some ( s) => assert_eq ! ( metadata. crs. as_deref( ) , Some ( s) ) ,
492+ }
493+ }
494+
495+ #[ test]
496+ fn test_geometry_to_duckdb_scalar_round_trip ( ) {
497+ let bytes = sample_wkb ( ) ;
498+ let original = wkb_scalar ( Some ( "EPSG:4326" ) , & bytes) ;
499+
500+ let duckdb_value = original. try_to_duckdb_scalar ( ) . unwrap ( ) ;
501+ let roundtrip: Scalar = duckdb_value. try_into ( ) . unwrap ( ) ;
502+
503+ assert_eq ! ( original, roundtrip) ;
504+ }
505+
506+ #[ test]
507+ fn test_null_geometry_to_duckdb_scalar ( ) {
508+ let dtype = ExtDType :: < WellKnownBinary > :: try_new (
509+ GeoMetadata {
510+ crs : Some ( "EPSG:4326" . to_string ( ) ) ,
511+ } ,
512+ DType :: Binary ( Nullability :: Nullable ) ,
513+ )
514+ . unwrap ( )
515+ . erased ( ) ;
516+ let original = Scalar :: null ( DType :: Extension ( dtype) ) ;
517+
518+ let duckdb_value = original. try_to_duckdb_scalar ( ) . unwrap ( ) ;
519+ let roundtrip: Scalar = duckdb_value. try_into ( ) . unwrap ( ) ;
520+
521+ assert ! ( roundtrip. is_null( ) ) ;
522+ assert_eq ! ( roundtrip. dtype( ) , original. dtype( ) ) ;
523+ }
416524}
0 commit comments