1use geo::Rect as GeoRect;
7use vortex_array::expr::Expression;
8use vortex_array::expr::gt;
9use vortex_array::expr::gt_eq;
10use vortex_array::expr::lit;
11use vortex_array::expr::lt;
12use vortex_array::expr::lt_eq;
13use vortex_array::scalar_fn::ScalarFnId;
14use vortex_array::scalar_fn::ScalarFnVTable;
15use vortex_array::scalar_fn::fns::binary::Binary;
16use vortex_array::scalar_fn::fns::literal::Literal;
17use vortex_array::scalar_fn::fns::operators::Operator;
18use vortex_array::stats::rewrite::StatsRewriteCtx;
19use vortex_array::stats::rewrite::StatsRewriteRule;
20use vortex_error::VortexResult;
21
22use super::aabb_stat;
23use super::geometry_and_constant;
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: &Expression,
46 ctx: &StatsRewriteCtx<'_>,
47 ) -> VortexResult<Option<Expression>> {
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: &Expression,
98 query: GeoRect<f64>,
99 op: Operator,
100 radius: f64,
101) -> Option<Expression> {
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::Expression;
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<Expression>> {
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.new_expr(operator, [distance, lit(radius.into())]);
174
175 GeoDistancePrune.falsify(&predicate, &StatsRewriteCtx::new(&session, &scope))
176 }
177
178 #[test]
181 fn null_literal_is_not_pruned() -> VortexResult<()> {
182 let session = geo_session();
183
184 let scope = point_column(vec![0.0], vec![0.0])?.dtype().clone();
185 let null_query = Scalar::null(scope.as_nullable());
186 let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(null_query)]);
187 let predicate = Binary.new_expr(Operator::Lte, [distance, lit(0.5f64)]);
188
189 let ctx = StatsRewriteCtx::new(&session, &scope);
190 assert!(GeoDistancePrune.falsify(&predicate, &ctx)?.is_none());
191 Ok(())
192 }
193
194 #[rstest]
197 #[case(Operator::Lte, true)]
198 #[case(Operator::Lt, true)]
199 #[case(Operator::Gt, true)]
200 #[case(Operator::Gte, true)]
201 #[case(Operator::Eq, false)]
202 #[case(Operator::NotEq, false)]
203 fn prunes_distance_comparisons(
204 #[case] operator: Operator,
205 #[case] prunes: bool,
206 ) -> VortexResult<()> {
207 assert_eq!(falsify_distance(operator, true, 0.5)?.is_some(), prunes);
208 Ok(())
209 }
210
211 #[rstest]
213 #[case(true)]
214 #[case(false)]
215 fn falsifies_either_operand_order(#[case] geom_first: bool) -> VortexResult<()> {
216 assert!(falsify_distance(Operator::Lte, geom_first, 0.5)?.is_some());
217 Ok(())
218 }
219
220 #[test]
222 fn nan_radius_never_prunes() -> VortexResult<()> {
223 assert!(falsify_distance(Operator::Lte, true, f64::NAN)?.is_none());
224 Ok(())
225 }
226
227 #[test]
229 fn negative_radius_prunes_vacuously() -> VortexResult<()> {
230 assert!(falsify_distance(Operator::Lte, true, -0.5)?.is_some());
231 Ok(())
232 }
233
234 #[test]
237 fn integer_radius_prunes() -> VortexResult<()> {
238 assert!(falsify_distance(Operator::Lte, true, 10i64)?.is_some());
239 Ok(())
240 }
241
242 #[test]
244 fn null_radius_never_prunes() -> VortexResult<()> {
245 let radius = Scalar::null(DType::Primitive(PType::F64, Nullability::Nullable));
246 assert!(falsify_distance(Operator::Lte, true, radius)?.is_none());
247 Ok(())
248 }
249
250 #[test]
253 fn extension_radius_never_prunes() -> VortexResult<()> {
254 let session = geo_session();
255 let mut ctx = session.create_execution_ctx();
256
257 let geometry = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
258 assert!(falsify_distance(Operator::Lte, true, geometry)?.is_none());
259 Ok(())
260 }
261
262 #[test]
265 fn unsupported_scope_is_not_pruned() -> VortexResult<()> {
266 let session = geo_session();
267 let mut ctx = session.create_execution_ctx();
268
269 let scope = DType::Primitive(PType::F64, Nullability::NonNullable);
270 let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
271 let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(origin)]);
272 let predicate = lt_eq(distance, lit(0.5f64));
273
274 let ctx = StatsRewriteCtx::new(&session, &scope);
275 assert!(GeoDistancePrune.falsify(&predicate, &ctx)?.is_none());
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));
286 let ctx = StatsRewriteCtx::new(&session, &scope);
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 .falsify(&point_dtype, &session)?
309 .expect("distance filter should be falsifiable");
310
311 let mask = zone_map.prune(&proof, &session)?;
313 assert_eq!(mask.iter().collect::<Vec<bool>>(), vec![false, true]);
314 Ok(())
315 }
316
317 #[test]
320 fn prunes_diagonally_distant_chunk() -> VortexResult<()> {
321 let session = geo_session();
322 let mut ctx = session.create_execution_ctx();
323
324 let point_dtype = point_column(vec![0.0], vec![0.0])?.dtype().clone();
325 let zone_map = aabb_zone_map(&point_dtype, &[[0.8, 0.8, 0.9, 0.9]])?;
328
329 let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
330 let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(origin)]);
331 let predicate = lt_eq(distance, lit(1.0f64));
332 let proof = predicate
333 .falsify(&point_dtype, &session)?
334 .expect("distance filter should be falsifiable");
335
336 assert_eq!(
337 zone_map
338 .prune(&proof, &session)?
339 .iter()
340 .collect::<Vec<bool>>(),
341 vec![true],
342 );
343 Ok(())
344 }
345
346 #[test]
349 fn prunes_within_chunk_for_far_filter() -> VortexResult<()> {
350 let session = geo_session();
351 let mut ctx = session.create_execution_ctx();
352
353 let point_dtype = point_column(vec![0.0], vec![0.0])?.dtype().clone();
354 let zone_map = aabb_zone_map(
357 &point_dtype,
358 &[[0.0, 0.0, 0.5, 0.5], [100.0, 100.0, 101.0, 101.0]],
359 )?;
360
361 let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
362 let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(origin)]);
363 let proof = gt_eq(distance, lit(2.0f64))
364 .falsify(&point_dtype, &session)?
365 .expect("distance filter should be falsifiable");
366
367 let mask = zone_map.prune(&proof, &session)?;
369 assert_eq!(mask.iter().collect::<Vec<bool>>(), vec![true, false]);
370 Ok(())
371 }
372
373 #[test]
376 fn missing_aabb_stat_keeps_all_zones() -> VortexResult<()> {
377 let session = geo_session();
378 let mut ctx = session.create_execution_ctx();
379
380 let point_dtype = point_column(vec![0.0], vec![0.0])?.dtype().clone();
381 let zone_map = empty_zone_map(&point_dtype)?;
382
383 let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
384 let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(origin)]);
385 let proof = lt_eq(distance, lit(0.5f64))
386 .falsify(&point_dtype, &session)?
387 .expect("distance filter should be falsifiable");
388
389 let mask = zone_map.prune(&proof, &session)?;
390 assert_eq!(mask.iter().collect::<Vec<bool>>(), vec![false, false]);
391 Ok(())
392 }
393}