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 #[rstest]
181 #[case(Operator::Lte, true)]
182 #[case(Operator::Lt, true)]
183 #[case(Operator::Gt, true)]
184 #[case(Operator::Gte, true)]
185 #[case(Operator::Eq, false)]
186 #[case(Operator::NotEq, false)]
187 fn prunes_distance_comparisons(
188 #[case] operator: Operator,
189 #[case] prunes: bool,
190 ) -> VortexResult<()> {
191 assert_eq!(falsify_distance(operator, true, 0.5)?.is_some(), prunes);
192 Ok(())
193 }
194
195 #[rstest]
197 #[case(true)]
198 #[case(false)]
199 fn falsifies_either_operand_order(#[case] geom_first: bool) -> VortexResult<()> {
200 assert!(falsify_distance(Operator::Lte, geom_first, 0.5)?.is_some());
201 Ok(())
202 }
203
204 #[test]
206 fn nan_radius_never_prunes() -> VortexResult<()> {
207 assert!(falsify_distance(Operator::Lte, true, f64::NAN)?.is_none());
208 Ok(())
209 }
210
211 #[test]
213 fn negative_radius_prunes_vacuously() -> VortexResult<()> {
214 assert!(falsify_distance(Operator::Lte, true, -0.5)?.is_some());
215 Ok(())
216 }
217
218 #[test]
221 fn integer_radius_prunes() -> VortexResult<()> {
222 assert!(falsify_distance(Operator::Lte, true, 10i64)?.is_some());
223 Ok(())
224 }
225
226 #[test]
228 fn null_radius_never_prunes() -> VortexResult<()> {
229 let radius = Scalar::null(DType::Primitive(PType::F64, Nullability::Nullable));
230 assert!(falsify_distance(Operator::Lte, true, radius)?.is_none());
231 Ok(())
232 }
233
234 #[test]
237 fn extension_radius_never_prunes() -> VortexResult<()> {
238 let session = geo_session();
239 let mut ctx = session.create_execution_ctx();
240
241 let geometry = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
242 assert!(falsify_distance(Operator::Lte, true, geometry)?.is_none());
243 Ok(())
244 }
245
246 #[test]
249 fn unsupported_scope_is_not_pruned() -> VortexResult<()> {
250 let session = geo_session();
251 let mut ctx = session.create_execution_ctx();
252
253 let scope = DType::Primitive(PType::F64, Nullability::NonNullable);
254 let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
255 let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(origin)]);
256 let predicate = lt_eq(distance, lit(0.5f64));
257
258 let ctx = StatsRewriteCtx::new(&session, &scope);
259 assert!(GeoDistancePrune.falsify(&predicate, &ctx)?.is_none());
260 Ok(())
261 }
262
263 #[test]
265 fn ignores_non_distance_comparison() -> VortexResult<()> {
266 let session = geo_session();
267 let scope = point_column(vec![0.0], vec![0.0])?.dtype().clone();
268
269 let predicate = lt_eq(lit(1.0f64), lit(2.0f64));
270 let ctx = StatsRewriteCtx::new(&session, &scope);
271 assert!(GeoDistancePrune.falsify(&predicate, &ctx)?.is_none());
272 Ok(())
273 }
274
275 #[test]
277 fn prunes_far_chunk_keeps_near() -> VortexResult<()> {
278 let session = geo_session();
279 let mut ctx = session.create_execution_ctx();
280
281 let point_dtype = point_column(vec![0.0], vec![0.0])?.dtype().clone();
282 let zone_map = aabb_zone_map(
284 &point_dtype,
285 &[[0.0, 0.0, 1.0, 1.0], [100.0, 100.0, 101.0, 101.0]],
286 )?;
287
288 let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
289 let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(origin)]);
290 let predicate = lt_eq(distance, lit(0.5f64));
291 let proof = predicate
292 .falsify(&point_dtype, &session)?
293 .expect("distance filter should be falsifiable");
294
295 let mask = zone_map.prune(&proof, &session)?;
297 assert_eq!(mask.iter().collect::<Vec<bool>>(), vec![false, true]);
298 Ok(())
299 }
300
301 #[test]
304 fn prunes_diagonally_distant_chunk() -> VortexResult<()> {
305 let session = geo_session();
306 let mut ctx = session.create_execution_ctx();
307
308 let point_dtype = point_column(vec![0.0], vec![0.0])?.dtype().clone();
309 let zone_map = aabb_zone_map(&point_dtype, &[[0.8, 0.8, 0.9, 0.9]])?;
312
313 let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
314 let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(origin)]);
315 let predicate = lt_eq(distance, lit(1.0f64));
316 let proof = predicate
317 .falsify(&point_dtype, &session)?
318 .expect("distance filter should be falsifiable");
319
320 assert_eq!(
321 zone_map
322 .prune(&proof, &session)?
323 .iter()
324 .collect::<Vec<bool>>(),
325 vec![true],
326 );
327 Ok(())
328 }
329
330 #[test]
333 fn prunes_within_chunk_for_far_filter() -> VortexResult<()> {
334 let session = geo_session();
335 let mut ctx = session.create_execution_ctx();
336
337 let point_dtype = point_column(vec![0.0], vec![0.0])?.dtype().clone();
338 let zone_map = aabb_zone_map(
341 &point_dtype,
342 &[[0.0, 0.0, 0.5, 0.5], [100.0, 100.0, 101.0, 101.0]],
343 )?;
344
345 let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
346 let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(origin)]);
347 let proof = gt_eq(distance, lit(2.0f64))
348 .falsify(&point_dtype, &session)?
349 .expect("distance filter should be falsifiable");
350
351 let mask = zone_map.prune(&proof, &session)?;
353 assert_eq!(mask.iter().collect::<Vec<bool>>(), vec![true, false]);
354 Ok(())
355 }
356
357 #[test]
360 fn missing_aabb_stat_keeps_all_zones() -> VortexResult<()> {
361 let session = geo_session();
362 let mut ctx = session.create_execution_ctx();
363
364 let point_dtype = point_column(vec![0.0], vec![0.0])?.dtype().clone();
365 let zone_map = empty_zone_map(&point_dtype)?;
366
367 let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
368 let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(origin)]);
369 let proof = lt_eq(distance, lit(0.5f64))
370 .falsify(&point_dtype, &session)?
371 .expect("distance filter should be falsifiable");
372
373 let mask = zone_map.prune(&proof, &session)?;
374 assert_eq!(mask.iter().collect::<Vec<bool>>(), vec![false, false]);
375 Ok(())
376 }
377}