66use geo:: Contains ;
77use vortex_array:: ArrayRef ;
88use vortex_array:: ExecutionCtx ;
9- use vortex_array:: IntoArray ;
10- use vortex_array:: arrays:: BoolArray ;
11- use vortex_array:: arrays:: Constant ;
12- use vortex_array:: arrays:: ConstantArray ;
139use vortex_array:: arrays:: ScalarFnArray ;
1410use vortex_array:: dtype:: DType ;
1511use vortex_array:: dtype:: Nullability ;
16- use vortex_array:: scalar:: Scalar ;
1712use vortex_array:: scalar_fn:: Arity ;
1813use vortex_array:: scalar_fn:: ChildName ;
1914use vortex_array:: scalar_fn:: EmptyOptions ;
@@ -22,13 +17,11 @@ use vortex_array::scalar_fn::ScalarFnId;
2217use vortex_array:: scalar_fn:: ScalarFnVTable ;
2318use vortex_array:: scalar_fn:: TypedScalarFnInstance ;
2419use vortex_error:: VortexResult ;
25- use vortex_error:: vortex_ensure_eq;
2620use vortex_session:: VortexSession ;
2721use vortex_session:: registry:: CachedId ;
2822
29- use crate :: extension:: geometries;
30- use crate :: extension:: single_geometry;
3123use crate :: extension:: validate_geometry_operands;
24+ use crate :: scalar_fn:: execute_null_propagating;
3225
3326/// OGC `ST_Contains` between two native geometry operands, each a column or a constant
3427/// literal: true where operand `b` lies completely inside operand `a` (boundary contact alone
@@ -77,7 +70,8 @@ impl ScalarFnVTable for GeoContains {
7770
7871 fn return_dtype ( & self , _: & Self :: Options , dtypes : & [ DType ] ) -> VortexResult < DType > {
7972 validate_geometry_operands ( dtypes) ?;
80- Ok ( DType :: Bool ( Nullability :: NonNullable ) )
73+ let nullability = Nullability :: from ( dtypes. iter ( ) . any ( DType :: is_nullable) ) ;
74+ Ok ( DType :: Bool ( nullability) )
8175 }
8276
8377 fn execute (
@@ -89,59 +83,10 @@ impl ScalarFnVTable for GeoContains {
8983 let a = args. get ( 0 ) ?;
9084 let b = args. get ( 1 ) ?;
9185 // Containment is not symmetric: `a` is always the container and `b` the contained.
92- match ( a. as_opt :: < Constant > ( ) , b. as_opt :: < Constant > ( ) ) {
93- ( Some ( qa) , Some ( qb) ) => {
94- let ga = single_geometry ( qa. scalar ( ) , ctx) ?;
95- let gb = single_geometry ( qb. scalar ( ) , ctx) ?;
96- Ok ( ConstantArray :: new (
97- Scalar :: bool ( ga. contains ( & gb) , Nullability :: NonNullable ) ,
98- a. len ( ) ,
99- )
100- . into_array ( ) )
101- }
102- ( Some ( qa) , None ) => constant_contains_column ( qa. scalar ( ) , & b, ctx) ,
103- ( None , Some ( qb) ) => column_contains_constant ( & a, qb. scalar ( ) , ctx) ,
104- ( None , None ) => {
105- vortex_ensure_eq ! (
106- a. len( ) ,
107- b. len( ) ,
108- "geo contains: operand length mismatch {} vs {}" ,
109- a. len( ) ,
110- b. len( )
111- ) ;
112- let ag = geometries ( & a, ctx) ?;
113- let bg = geometries ( & b, ctx) ?;
114- let hits = ag. iter ( ) . zip ( & bg) . map ( |( x, y) | x. contains ( y) ) ;
115- Ok ( BoolArray :: from_iter ( hits) . into_array ( ) )
116- }
117- }
86+ execute_null_propagating ( & a, & b, |a, b| a. contains ( b) , ctx)
11887 }
11988}
12089
121- /// Whether the constant `container` contains each row of `contained`.
122- fn constant_contains_column (
123- container : & Scalar ,
124- contained : & ArrayRef ,
125- ctx : & mut ExecutionCtx ,
126- ) -> VortexResult < ArrayRef > {
127- let container = single_geometry ( container, ctx) ?;
128- let geoms = geometries ( contained, ctx) ?;
129- let hits = geoms. iter ( ) . map ( |g| container. contains ( g) ) ;
130- Ok ( BoolArray :: from_iter ( hits) . into_array ( ) )
131- }
132-
133- /// Whether each row of `container` contains the constant `contained`.
134- fn column_contains_constant (
135- container : & ArrayRef ,
136- contained : & Scalar ,
137- ctx : & mut ExecutionCtx ,
138- ) -> VortexResult < ArrayRef > {
139- let contained = single_geometry ( contained, ctx) ?;
140- let geoms = geometries ( container, ctx) ?;
141- let hits = geoms. iter ( ) . map ( |g| g. contains ( & contained) ) ;
142- Ok ( BoolArray :: from_iter ( hits) . into_array ( ) )
143- }
144-
14590#[ cfg( test) ]
14691mod tests {
14792 use geo_types:: Geometry ;
@@ -160,13 +105,17 @@ mod tests {
160105 use vortex_array:: dtype:: DType ;
161106 use vortex_array:: dtype:: Nullability ;
162107 use vortex_array:: dtype:: PType ;
108+ use vortex_array:: scalar:: Scalar ;
163109 use vortex_array:: scalar_fn:: EmptyOptions ;
164110 use vortex_array:: scalar_fn:: ScalarFnVTable ;
111+ use vortex_array:: validity:: Validity ;
112+ use vortex_buffer:: BitBuffer ;
165113 use vortex_error:: VortexResult ;
166114 use vortex_error:: vortex_err;
167115 use wkb:: writer:: WriteOptions ;
168116
169117 use super :: GeoContains ;
118+ use crate :: test_harness:: nullable_point_column;
170119 use crate :: test_harness:: point_column;
171120
172121 /// A rectangle polygon with corners `(x0, y0)` and `(x1, y1)`, no holes.
@@ -286,12 +235,53 @@ mod tests {
286235 assert_contains ( polygons, points, [ true , false ] )
287236 }
288237
289- /// Geometry arrays are never nullable, so a nullable operand dtype is rejected.
238+ /// Output nullability mirrors the operands: nullable if any operand is nullable, otherwise
239+ /// non-nullable.
290240 #[ test]
291- fn nullable_operand_is_rejected ( ) -> VortexResult < ( ) > {
241+ fn output_nullability_mirrors_operands ( ) -> VortexResult < ( ) > {
292242 let dtype = point_column ( vec ! [ 0.0 ] , vec ! [ 0.0 ] ) ?. dtype ( ) . clone ( ) ;
293- let result = GeoContains . return_dtype ( & EmptyOptions , & [ dtype. as_nullable ( ) , dtype] ) ;
294- assert ! ( result. is_err( ) ) ;
243+ let non_nullable =
244+ GeoContains . return_dtype ( & EmptyOptions , & [ dtype. clone ( ) , dtype. clone ( ) ] ) ?;
245+ assert ! ( !non_nullable. is_nullable( ) ) ;
246+ let nullable = GeoContains . return_dtype ( & EmptyOptions , & [ dtype. as_nullable ( ) , dtype] ) ?;
247+ assert ! ( nullable. is_nullable( ) ) ;
248+ Ok ( ( ) )
249+ }
250+
251+ /// A null row in the contained operand yields a null verdict; valid rows keep their verdict
252+ /// (a strictly interior point is contained, an outside point is not).
253+ #[ test]
254+ fn contains_propagates_null_rows ( ) -> VortexResult < ( ) > {
255+ let session = vortex_array:: array_session ( ) ;
256+ let mut ctx = session. create_execution_ctx ( ) ;
257+
258+ let container = geometry_constant ( & Geometry :: Polygon ( rect_polygon ( 0.0 , 0.0 , 4.0 , 4.0 ) ) , 3 ) ?;
259+ let points = nullable_point_column ( vec ! [ Some ( ( 2.0 , 2.0 ) ) , None , Some ( ( 10.0 , 10.0 ) ) ] ) ?;
260+ let contains = GeoContains :: try_new_array ( container, points) ?. into_array ( ) ;
261+
262+ let expected = BoolArray :: new (
263+ BitBuffer :: from_iter ( [ true , false , false ] ) ,
264+ Validity :: from_iter ( [ true , false , true ] ) ,
265+ )
266+ . into_array ( ) ;
267+ assert_arrays_eq ! ( contains, expected, & mut ctx) ;
268+ Ok ( ( ) )
269+ }
270+
271+ /// A constant-null operand produces an all-null output.
272+ #[ test]
273+ fn contains_constant_null_is_all_null ( ) -> VortexResult < ( ) > {
274+ let session = vortex_array:: array_session ( ) ;
275+ let mut ctx = session. create_execution_ctx ( ) ;
276+
277+ let point_dtype = point_column ( vec ! [ 0.0 ] , vec ! [ 0.0 ] ) ?. dtype ( ) . as_nullable ( ) ;
278+ let null_const = ConstantArray :: new ( Scalar :: null ( point_dtype) , 2 ) . into_array ( ) ;
279+ let points = point_column ( vec ! [ 2.0 , 10.0 ] , vec ! [ 2.0 , 10.0 ] ) ?;
280+ let contains = GeoContains :: try_new_array ( null_const, points) ?. into_array ( ) ;
281+
282+ let expected =
283+ BoolArray :: new ( BitBuffer :: from_iter ( [ false , false ] ) , Validity :: AllInvalid ) . into_array ( ) ;
284+ assert_arrays_eq ! ( contains, expected, & mut ctx) ;
295285 Ok ( ( ) )
296286 }
297287
0 commit comments