1use geo::Rect as GeoRect;
7use vortex_array::expr::BoundExpression;
8use vortex_array::scalar_fn::ScalarFnId;
9use vortex_array::scalar_fn::ScalarFnVTable;
10use vortex_array::scalar_fn::fns::binary::Binary;
11use vortex_array::scalar_fn::fns::literal::Literal;
12use vortex_array::scalar_fn::fns::operators::Operator;
13use vortex_array::stats::rewrite::StatsRewriteCtx;
14use vortex_array::stats::rewrite::StatsRewriteRule;
15use vortex_error::VortexResult;
16
17use super::aabb_stat;
18use super::geometry_and_constant;
19use super::gt;
20use super::gt_eq;
21use super::lit;
22use super::lt;
23use super::lt_eq;
24use super::max_dist_sq;
25use super::min_dist_sq;
26use super::query_aabb;
27use crate::scalar_fn::distance::GeoDistance;
28
29#[derive(Debug)]
35pub struct GeoDistancePrune;
36
37impl StatsRewriteRule for GeoDistancePrune {
38 fn scalar_fn_id(&self) -> ScalarFnId {
39 Binary.id()
41 }
42
43 fn falsify(
44 &self,
45 expr: &BoundExpression,
46 ctx: &StatsRewriteCtx<'_>,
47 ) -> VortexResult<Option<BoundExpression>> {
48 let op = *expr.as_::<Binary>();
53 if !matches!(
54 op,
55 Operator::Lte | Operator::Lt | Operator::Gte | Operator::Gt
56 ) {
57 return Ok(None);
58 }
59
60 let distance = expr.child(0);
62 if distance.as_opt::<GeoDistance>().is_none() {
63 return Ok(None);
64 }
65 let Some(radius) = expr.child(1).as_opt::<Literal>() else {
66 return Ok(None);
67 };
68 let Ok(radius) = f64::try_from(radius) else {
72 return Ok(None);
73 };
74 if radius.is_nan() {
77 return Ok(None);
78 }
79
80 let Some((geom, constant)) = geometry_and_constant(distance, ctx)? else {
81 return Ok(None);
82 };
83 let Some(query) = query_aabb(constant, ctx)? else {
84 return Ok(None);
85 };
86 Ok(distance_prune_proof(geom, query, op, radius))
87 }
88}
89
90fn distance_prune_proof(
97 geom: &BoundExpression,
98 query: GeoRect<f64>,
99 op: Operator,
100 radius: f64,
101) -> Option<BoundExpression> {
102 match op {
104 Operator::Lte if radius < 0.0 => return Some(lit(true)),
106 Operator::Lt if radius <= 0.0 => return Some(lit(true)),
107 Operator::Gte if radius <= 0.0 => return None,
109 Operator::Gt if radius < 0.0 => return None,
110 _ => {}
111 }
112 let aabb = aabb_stat(geom);
114 let r2 = lit(radius * radius);
115 Some(match op {
116 Operator::Lte => gt(min_dist_sq(&aabb, query), r2),
118 Operator::Lt => gt_eq(min_dist_sq(&aabb, query), r2),
119 Operator::Gte => lt(max_dist_sq(&aabb, query), r2),
121 Operator::Gt => lt_eq(max_dist_sq(&aabb, query), r2),
122 _ => return None,
123 })
124}
125
126#[cfg(test)]
127mod tests {
128 use rstest::rstest;
129 use vortex_array::VortexSessionExecute;
130 use vortex_array::dtype::DType;
131 use vortex_array::dtype::Nullability;
132 use vortex_array::dtype::PType;
133 use vortex_array::expr::BoundExpression;
134 use vortex_array::expr::gt_eq;
135 use vortex_array::expr::lit;
136 use vortex_array::expr::lt_eq;
137 use vortex_array::expr::root;
138 use vortex_array::scalar::Scalar;
139 use vortex_array::scalar_fn::EmptyOptions;
140 use vortex_array::scalar_fn::ScalarFnVTableExt;
141 use vortex_array::scalar_fn::fns::binary::Binary;
142 use vortex_array::scalar_fn::fns::operators::Operator;
143 use vortex_array::stats::rewrite::StatsRewriteCtx;
144 use vortex_array::stats::rewrite::StatsRewriteRule;
145 use vortex_error::VortexResult;
146
147 use super::GeoDistancePrune;
148 use crate::prune::test_harness::aabb_zone_map;
149 use crate::prune::test_harness::empty_zone_map;
150 use crate::scalar_fn::distance::GeoDistance;
151 use crate::test_harness::geo_session;
152 use crate::test_harness::point_column;
153
154 fn falsify_distance(
158 operator: Operator,
159 geom_first: bool,
160 radius: impl Into<Scalar>,
161 ) -> VortexResult<Option<BoundExpression>> {
162 let session = geo_session();
163 let mut ctx = session.create_execution_ctx();
164
165 let scope = point_column(vec![0.0], vec![0.0])?.dtype().clone();
166 let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
167 let operands = if geom_first {
168 [root(), lit(origin)]
169 } else {
170 [lit(origin), root()]
171 };
172 let distance = GeoDistance.new_expr(EmptyOptions, operands);
173 let predicate = Binary
174 .new_expr(operator, [distance, lit(radius.into())])
175 .bind(&scope)?;
176
177 GeoDistancePrune.falsify(&predicate, &StatsRewriteCtx::new(&session))
178 }
179
180 #[test]
183 fn null_literal_is_not_pruned() -> VortexResult<()> {
184 let session = geo_session();
185
186 let scope = point_column(vec![0.0], vec![0.0])?.dtype().clone();
187 let null_query = Scalar::null(scope.as_nullable());
188 let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(null_query)]);
189 let predicate = Binary
190 .new_expr(Operator::Lte, [distance, lit(0.5f64)])
191 .bind(&scope)?;
192
193 let ctx = StatsRewriteCtx::new(&session);
194 assert!(GeoDistancePrune.falsify(&predicate, &ctx)?.is_none());
195 Ok(())
196 }
197
198 #[rstest]
201 #[case(Operator::Lte, true)]
202 #[case(Operator::Lt, true)]
203 #[case(Operator::Gt, true)]
204 #[case(Operator::Gte, true)]
205 #[case(Operator::Eq, false)]
206 #[case(Operator::NotEq, false)]
207 fn prunes_distance_comparisons(
208 #[case] operator: Operator,
209 #[case] prunes: bool,
210 ) -> VortexResult<()> {
211 assert_eq!(falsify_distance(operator, true, 0.5)?.is_some(), prunes);
212 Ok(())
213 }
214
215 #[rstest]
217 #[case(true)]
218 #[case(false)]
219 fn falsifies_either_operand_order(#[case] geom_first: bool) -> VortexResult<()> {
220 assert!(falsify_distance(Operator::Lte, geom_first, 0.5)?.is_some());
221 Ok(())
222 }
223
224 #[test]
226 fn nan_radius_never_prunes() -> VortexResult<()> {
227 assert!(falsify_distance(Operator::Lte, true, f64::NAN)?.is_none());
228 Ok(())
229 }
230
231 #[test]
233 fn negative_radius_prunes_vacuously() -> VortexResult<()> {
234 assert!(falsify_distance(Operator::Lte, true, -0.5)?.is_some());
235 Ok(())
236 }
237
238 #[test]
240 fn uncoerced_integer_radius_fails_to_bind() -> VortexResult<()> {
241 assert!(falsify_distance(Operator::Lte, true, 10i64).is_err());
242 Ok(())
243 }
244
245 #[test]
247 fn null_radius_never_prunes() -> VortexResult<()> {
248 let radius = Scalar::null(DType::Primitive(PType::F64, Nullability::Nullable));
249 assert!(falsify_distance(Operator::Lte, true, radius)?.is_none());
250 Ok(())
251 }
252
253 #[test]
256 fn extension_radius_never_prunes() -> VortexResult<()> {
257 let session = geo_session();
258 let mut ctx = session.create_execution_ctx();
259
260 let geometry = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
261 assert!(falsify_distance(Operator::Lte, true, geometry)?.is_none());
262 Ok(())
263 }
264
265 #[test]
267 fn unsupported_scope_is_not_pruned() -> VortexResult<()> {
268 let session = geo_session();
269 let mut ctx = session.create_execution_ctx();
270
271 let scope = DType::Primitive(PType::F64, Nullability::NonNullable);
272 let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
273 let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(origin)]);
274 let predicate = lt_eq(distance, lit(0.5f64));
275 assert!(predicate.bind(&scope).is_err());
276 Ok(())
277 }
278
279 #[test]
281 fn ignores_non_distance_comparison() -> VortexResult<()> {
282 let session = geo_session();
283 let scope = point_column(vec![0.0], vec![0.0])?.dtype().clone();
284
285 let predicate = lt_eq(lit(1.0f64), lit(2.0f64)).bind(&scope)?;
286 let ctx = StatsRewriteCtx::new(&session);
287 assert!(GeoDistancePrune.falsify(&predicate, &ctx)?.is_none());
288 Ok(())
289 }
290
291 #[test]
293 fn prunes_far_chunk_keeps_near() -> VortexResult<()> {
294 let session = geo_session();
295 let mut ctx = session.create_execution_ctx();
296
297 let point_dtype = point_column(vec![0.0], vec![0.0])?.dtype().clone();
298 let zone_map = aabb_zone_map(
300 &point_dtype,
301 &[[0.0, 0.0, 1.0, 1.0], [100.0, 100.0, 101.0, 101.0]],
302 )?;
303
304 let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
305 let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(origin)]);
306 let predicate = lt_eq(distance, lit(0.5f64));
307 let proof = predicate
308 .bind(&point_dtype)?
309 .falsify(&session)?
310 .expect("distance filter should be falsifiable");
311
312 let mask = zone_map.prune(&proof, &session)?;
314 assert_eq!(mask.iter().collect::<Vec<bool>>(), vec![false, true]);
315 Ok(())
316 }
317
318 #[test]
321 fn prunes_diagonally_distant_chunk() -> VortexResult<()> {
322 let session = geo_session();
323 let mut ctx = session.create_execution_ctx();
324
325 let point_dtype = point_column(vec![0.0], vec![0.0])?.dtype().clone();
326 let zone_map = aabb_zone_map(&point_dtype, &[[0.8, 0.8, 0.9, 0.9]])?;
329
330 let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
331 let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(origin)]);
332 let predicate = lt_eq(distance, lit(1.0f64));
333 let proof = predicate
334 .bind(&point_dtype)?
335 .falsify(&session)?
336 .expect("distance filter should be falsifiable");
337
338 assert_eq!(
339 zone_map
340 .prune(&proof, &session)?
341 .iter()
342 .collect::<Vec<bool>>(),
343 vec![true],
344 );
345 Ok(())
346 }
347
348 #[test]
351 fn prunes_within_chunk_for_far_filter() -> VortexResult<()> {
352 let session = geo_session();
353 let mut ctx = session.create_execution_ctx();
354
355 let point_dtype = point_column(vec![0.0], vec![0.0])?.dtype().clone();
356 let zone_map = aabb_zone_map(
359 &point_dtype,
360 &[[0.0, 0.0, 0.5, 0.5], [100.0, 100.0, 101.0, 101.0]],
361 )?;
362
363 let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
364 let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(origin)]);
365 let proof = gt_eq(distance, lit(2.0f64))
366 .bind(&point_dtype)?
367 .falsify(&session)?
368 .expect("distance filter should be falsifiable");
369
370 let mask = zone_map.prune(&proof, &session)?;
372 assert_eq!(mask.iter().collect::<Vec<bool>>(), vec![true, false]);
373 Ok(())
374 }
375
376 #[test]
379 fn missing_aabb_stat_keeps_all_zones() -> VortexResult<()> {
380 let session = geo_session();
381 let mut ctx = session.create_execution_ctx();
382
383 let point_dtype = point_column(vec![0.0], vec![0.0])?.dtype().clone();
384 let zone_map = empty_zone_map(&point_dtype)?;
385
386 let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
387 let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(origin)]);
388 let proof = lt_eq(distance, lit(0.5f64))
389 .bind(&point_dtype)?
390 .falsify(&session)?
391 .expect("distance filter should be falsifiable");
392
393 let mask = zone_map.prune(&proof, &session)?;
394 assert_eq!(mask.iter().collect::<Vec<bool>>(), vec![false, false]);
395 Ok(())
396 }
397}