use geo::Rect as GeoRect;
use vortex_array::expr::Expression;
use vortex_array::expr::gt;
use vortex_array::expr::gt_eq;
use vortex_array::expr::lit;
use vortex_array::expr::lt;
use vortex_array::expr::lt_eq;
use vortex_array::scalar_fn::ScalarFnId;
use vortex_array::scalar_fn::ScalarFnVTable;
use vortex_array::scalar_fn::fns::binary::Binary;
use vortex_array::scalar_fn::fns::literal::Literal;
use vortex_array::scalar_fn::fns::operators::Operator;
use vortex_array::stats::rewrite::StatsRewriteCtx;
use vortex_array::stats::rewrite::StatsRewriteRule;
use vortex_error::VortexResult;
use super::aabb_stat;
use super::geometry_and_constant;
use super::max_dist_sq;
use super::min_dist_sq;
use super::query_aabb;
use crate::scalar_fn::distance::GeoDistance;
#[derive(Debug)]
pub struct GeoDistancePrune;
impl StatsRewriteRule for GeoDistancePrune {
fn scalar_fn_id(&self) -> ScalarFnId {
Binary.id()
}
fn falsify(
&self,
expr: &Expression,
ctx: &StatsRewriteCtx<'_>,
) -> VortexResult<Option<Expression>> {
let op = *expr.as_::<Binary>();
if !matches!(
op,
Operator::Lte | Operator::Lt | Operator::Gte | Operator::Gt
) {
return Ok(None);
}
let distance = expr.child(0);
if distance.as_opt::<GeoDistance>().is_none() {
return Ok(None);
}
let Some(radius) = expr.child(1).as_opt::<Literal>() else {
return Ok(None);
};
let Ok(radius) = f64::try_from(radius) else {
return Ok(None);
};
if radius.is_nan() {
return Ok(None);
}
let Some((geom, constant)) = geometry_and_constant(distance, ctx)? else {
return Ok(None);
};
let Some(query) = query_aabb(constant, ctx)? else {
return Ok(None);
};
Ok(distance_prune_proof(geom, query, op, radius))
}
}
fn distance_prune_proof(
geom: &Expression,
query: GeoRect<f64>,
op: Operator,
radius: f64,
) -> Option<Expression> {
match op {
Operator::Lte if radius < 0.0 => return Some(lit(true)),
Operator::Lt if radius <= 0.0 => return Some(lit(true)),
Operator::Gte if radius <= 0.0 => return None,
Operator::Gt if radius < 0.0 => return None,
_ => {}
}
let aabb = aabb_stat(geom);
let r2 = lit(radius * radius);
Some(match op {
Operator::Lte => gt(min_dist_sq(&aabb, query), r2),
Operator::Lt => gt_eq(min_dist_sq(&aabb, query), r2),
Operator::Gte => lt(max_dist_sq(&aabb, query), r2),
Operator::Gt => lt_eq(max_dist_sq(&aabb, query), r2),
_ => return None,
})
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use vortex_array::VortexSessionExecute;
use vortex_array::dtype::DType;
use vortex_array::dtype::Nullability;
use vortex_array::dtype::PType;
use vortex_array::expr::Expression;
use vortex_array::expr::gt_eq;
use vortex_array::expr::lit;
use vortex_array::expr::lt_eq;
use vortex_array::expr::root;
use vortex_array::scalar::Scalar;
use vortex_array::scalar_fn::EmptyOptions;
use vortex_array::scalar_fn::ScalarFnVTableExt;
use vortex_array::scalar_fn::fns::binary::Binary;
use vortex_array::scalar_fn::fns::operators::Operator;
use vortex_array::stats::rewrite::StatsRewriteCtx;
use vortex_array::stats::rewrite::StatsRewriteRule;
use vortex_error::VortexResult;
use super::GeoDistancePrune;
use crate::prune::test_harness::aabb_zone_map;
use crate::prune::test_harness::empty_zone_map;
use crate::scalar_fn::distance::GeoDistance;
use crate::test_harness::geo_session;
use crate::test_harness::point_column;
fn falsify_distance(
operator: Operator,
geom_first: bool,
radius: impl Into<Scalar>,
) -> VortexResult<Option<Expression>> {
let session = geo_session();
let mut ctx = session.create_execution_ctx();
let scope = point_column(vec![0.0], vec![0.0])?.dtype().clone();
let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
let operands = if geom_first {
[root(), lit(origin)]
} else {
[lit(origin), root()]
};
let distance = GeoDistance.new_expr(EmptyOptions, operands);
let predicate = Binary.new_expr(operator, [distance, lit(radius.into())]);
GeoDistancePrune.falsify(&predicate, &StatsRewriteCtx::new(&session, &scope))
}
#[rstest]
#[case(Operator::Lte, true)]
#[case(Operator::Lt, true)]
#[case(Operator::Gt, true)]
#[case(Operator::Gte, true)]
#[case(Operator::Eq, false)]
#[case(Operator::NotEq, false)]
fn prunes_distance_comparisons(
#[case] operator: Operator,
#[case] prunes: bool,
) -> VortexResult<()> {
assert_eq!(falsify_distance(operator, true, 0.5)?.is_some(), prunes);
Ok(())
}
#[rstest]
#[case(true)]
#[case(false)]
fn falsifies_either_operand_order(#[case] geom_first: bool) -> VortexResult<()> {
assert!(falsify_distance(Operator::Lte, geom_first, 0.5)?.is_some());
Ok(())
}
#[test]
fn nan_radius_never_prunes() -> VortexResult<()> {
assert!(falsify_distance(Operator::Lte, true, f64::NAN)?.is_none());
Ok(())
}
#[test]
fn negative_radius_prunes_vacuously() -> VortexResult<()> {
assert!(falsify_distance(Operator::Lte, true, -0.5)?.is_some());
Ok(())
}
#[test]
fn integer_radius_prunes() -> VortexResult<()> {
assert!(falsify_distance(Operator::Lte, true, 10i64)?.is_some());
Ok(())
}
#[test]
fn null_radius_never_prunes() -> VortexResult<()> {
let radius = Scalar::null(DType::Primitive(PType::F64, Nullability::Nullable));
assert!(falsify_distance(Operator::Lte, true, radius)?.is_none());
Ok(())
}
#[test]
fn extension_radius_never_prunes() -> VortexResult<()> {
let session = geo_session();
let mut ctx = session.create_execution_ctx();
let geometry = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
assert!(falsify_distance(Operator::Lte, true, geometry)?.is_none());
Ok(())
}
#[test]
fn unsupported_scope_is_not_pruned() -> VortexResult<()> {
let session = geo_session();
let mut ctx = session.create_execution_ctx();
let scope = DType::Primitive(PType::F64, Nullability::NonNullable);
let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(origin)]);
let predicate = lt_eq(distance, lit(0.5f64));
let ctx = StatsRewriteCtx::new(&session, &scope);
assert!(GeoDistancePrune.falsify(&predicate, &ctx)?.is_none());
Ok(())
}
#[test]
fn ignores_non_distance_comparison() -> VortexResult<()> {
let session = geo_session();
let scope = point_column(vec![0.0], vec![0.0])?.dtype().clone();
let predicate = lt_eq(lit(1.0f64), lit(2.0f64));
let ctx = StatsRewriteCtx::new(&session, &scope);
assert!(GeoDistancePrune.falsify(&predicate, &ctx)?.is_none());
Ok(())
}
#[test]
fn prunes_far_chunk_keeps_near() -> VortexResult<()> {
let session = geo_session();
let mut ctx = session.create_execution_ctx();
let point_dtype = point_column(vec![0.0], vec![0.0])?.dtype().clone();
let zone_map = aabb_zone_map(
&point_dtype,
&[[0.0, 0.0, 1.0, 1.0], [100.0, 100.0, 101.0, 101.0]],
)?;
let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(origin)]);
let predicate = lt_eq(distance, lit(0.5f64));
let proof = predicate
.falsify(&point_dtype, &session)?
.expect("distance filter should be falsifiable");
let mask = zone_map.prune(&proof, &session)?;
assert_eq!(mask.iter().collect::<Vec<bool>>(), vec![false, true]);
Ok(())
}
#[test]
fn prunes_diagonally_distant_chunk() -> VortexResult<()> {
let session = geo_session();
let mut ctx = session.create_execution_ctx();
let point_dtype = point_column(vec![0.0], vec![0.0])?.dtype().clone();
let zone_map = aabb_zone_map(&point_dtype, &[[0.8, 0.8, 0.9, 0.9]])?;
let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(origin)]);
let predicate = lt_eq(distance, lit(1.0f64));
let proof = predicate
.falsify(&point_dtype, &session)?
.expect("distance filter should be falsifiable");
assert_eq!(
zone_map
.prune(&proof, &session)?
.iter()
.collect::<Vec<bool>>(),
vec![true],
);
Ok(())
}
#[test]
fn prunes_within_chunk_for_far_filter() -> VortexResult<()> {
let session = geo_session();
let mut ctx = session.create_execution_ctx();
let point_dtype = point_column(vec![0.0], vec![0.0])?.dtype().clone();
let zone_map = aabb_zone_map(
&point_dtype,
&[[0.0, 0.0, 0.5, 0.5], [100.0, 100.0, 101.0, 101.0]],
)?;
let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(origin)]);
let proof = gt_eq(distance, lit(2.0f64))
.falsify(&point_dtype, &session)?
.expect("distance filter should be falsifiable");
let mask = zone_map.prune(&proof, &session)?;
assert_eq!(mask.iter().collect::<Vec<bool>>(), vec![true, false]);
Ok(())
}
#[test]
fn missing_aabb_stat_keeps_all_zones() -> VortexResult<()> {
let session = geo_session();
let mut ctx = session.create_execution_ctx();
let point_dtype = point_column(vec![0.0], vec![0.0])?.dtype().clone();
let zone_map = empty_zone_map(&point_dtype)?;
let origin = point_column(vec![0.0], vec![0.0])?.execute_scalar(0, &mut ctx)?;
let distance = GeoDistance.new_expr(EmptyOptions, [root(), lit(origin)]);
let proof = lt_eq(distance, lit(0.5f64))
.falsify(&point_dtype, &session)?
.expect("distance filter should be falsifiable");
let mask = zone_map.prune(&proof, &session)?;
assert_eq!(mask.iter().collect::<Vec<bool>>(), vec![false, false]);
Ok(())
}
}