#![cfg(feature = "spatial")]
use geo::{
algorithm::{relate::Relate, Intersects},
Contains, Distance, Euclidean, Haversine,
};
use vibesql_types::SqlValue;
use super::{sql_value_to_geometry, Geometry};
use crate::errors::ExecutorError;
fn wkt_to_geo(wkt_str: &str) -> Result<geo::Geometry<f64>, ExecutorError> {
let sql_value = SqlValue::Varchar(arcstr::ArcStr::from(wkt_str));
let geom_with_srid = sql_value_to_geometry(&sql_value)?;
to_geo_geometry(&geom_with_srid.geometry)
}
fn to_geo_geometry(geom: &Geometry) -> Result<geo::Geometry<f64>, ExecutorError> {
match geom {
Geometry::Point { x, y } => Ok(geo::Geometry::Point(geo::Point::new(*x, *y))),
Geometry::LineString { points } => {
let coords: Vec<geo::Coord<f64>> =
points.iter().map(|(x, y)| geo::Coord { x: *x, y: *y }).collect();
Ok(geo::Geometry::LineString(geo::LineString(coords)))
}
Geometry::Polygon { rings } => {
if rings.is_empty() {
return Err(ExecutorError::SpatialGeometryError {
function_name: "to_geo_geometry".to_string(),
message: "Empty polygon".to_string(),
});
}
let exterior: Vec<geo::Coord<f64>> =
rings[0].iter().map(|(x, y)| geo::Coord { x: *x, y: *y }).collect();
let exterior_ring = geo::LineString(exterior);
let interiors: Vec<geo::LineString<f64>> = rings[1..]
.iter()
.map(|ring| {
let coords: Vec<geo::Coord<f64>> =
ring.iter().map(|(x, y)| geo::Coord { x: *x, y: *y }).collect();
geo::LineString(coords)
})
.collect();
Ok(geo::Geometry::Polygon(geo::Polygon::new(exterior_ring, interiors)))
}
_ => Err(ExecutorError::UnsupportedFeature(format!(
"Geometry type {} not yet fully supported for spatial predicates",
geom.geometry_type()
))),
}
}
pub fn st_contains(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
if args.len() != 2 {
return Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Contains".to_string(),
expected: "exactly 2 arguments".to_string(),
actual: format!("{} arguments", args.len()),
});
}
match (&args[0], &args[1]) {
(SqlValue::Null, _) | (_, SqlValue::Null) => Ok(SqlValue::Null),
(
SqlValue::Varchar(wkt1) | SqlValue::Character(wkt1),
SqlValue::Varchar(wkt2) | SqlValue::Character(wkt2),
) => {
let geom1 = wkt_to_geo(wkt1)?;
let geom2 = wkt_to_geo(wkt2)?;
let result = geom1.contains(&geom2);
Ok(SqlValue::Boolean(result))
}
_ => Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Contains".to_string(),
expected: "VARCHAR geometry arguments".to_string(),
actual: format!("{:?}, {:?}", args[0].type_name(), args[1].type_name()),
}),
}
}
pub fn st_within(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
if args.len() != 2 {
return Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Within".to_string(),
expected: "exactly 2 arguments".to_string(),
actual: format!("{} arguments", args.len()),
});
}
match (&args[0], &args[1]) {
(SqlValue::Null, _) | (_, SqlValue::Null) => Ok(SqlValue::Null),
(
SqlValue::Varchar(wkt1) | SqlValue::Character(wkt1),
SqlValue::Varchar(wkt2) | SqlValue::Character(wkt2),
) => {
let geom1 = wkt_to_geo(wkt1)?;
let geom2 = wkt_to_geo(wkt2)?;
let result = geom2.contains(&geom1);
Ok(SqlValue::Boolean(result))
}
_ => Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Within".to_string(),
expected: "VARCHAR geometry arguments".to_string(),
actual: format!("{:?}, {:?}", args[0].type_name(), args[1].type_name()),
}),
}
}
pub fn st_intersects(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
if args.len() != 2 {
return Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Intersects".to_string(),
expected: "exactly 2 arguments".to_string(),
actual: format!("{} arguments", args.len()),
});
}
match (&args[0], &args[1]) {
(SqlValue::Null, _) | (_, SqlValue::Null) => Ok(SqlValue::Null),
(
SqlValue::Varchar(wkt1) | SqlValue::Character(wkt1),
SqlValue::Varchar(wkt2) | SqlValue::Character(wkt2),
) => {
let geom1 = wkt_to_geo(wkt1)?;
let geom2 = wkt_to_geo(wkt2)?;
let result = geom1.intersects(&geom2);
Ok(SqlValue::Boolean(result))
}
_ => Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Intersects".to_string(),
expected: "VARCHAR geometry arguments".to_string(),
actual: format!("{:?}, {:?}", args[0].type_name(), args[1].type_name()),
}),
}
}
pub fn st_disjoint(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
if args.len() != 2 {
return Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Disjoint".to_string(),
expected: "exactly 2 arguments".to_string(),
actual: format!("{} arguments", args.len()),
});
}
match (&args[0], &args[1]) {
(SqlValue::Null, _) | (_, SqlValue::Null) => Ok(SqlValue::Null),
(
SqlValue::Varchar(wkt1) | SqlValue::Character(wkt1),
SqlValue::Varchar(wkt2) | SqlValue::Character(wkt2),
) => {
let geom1 = wkt_to_geo(wkt1)?;
let geom2 = wkt_to_geo(wkt2)?;
let result = !geom1.intersects(&geom2);
Ok(SqlValue::Boolean(result))
}
_ => Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Disjoint".to_string(),
expected: "VARCHAR geometry arguments".to_string(),
actual: format!("{:?}, {:?}", args[0].type_name(), args[1].type_name()),
}),
}
}
pub fn st_equals(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
if args.len() != 2 {
return Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Equals".to_string(),
expected: "exactly 2 arguments".to_string(),
actual: format!("{} arguments", args.len()),
});
}
match (&args[0], &args[1]) {
(SqlValue::Null, _) | (_, SqlValue::Null) => Ok(SqlValue::Null),
(
SqlValue::Varchar(wkt1) | SqlValue::Character(wkt1),
SqlValue::Varchar(wkt2) | SqlValue::Character(wkt2),
) => {
let geom1 = wkt_to_geo(wkt1)?;
let geom2 = wkt_to_geo(wkt2)?;
let result = geom1 == geom2;
Ok(SqlValue::Boolean(result))
}
_ => Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Equals".to_string(),
expected: "VARCHAR geometry arguments".to_string(),
actual: format!("{:?}, {:?}", args[0].type_name(), args[1].type_name()),
}),
}
}
pub fn st_touches(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
if args.len() != 2 {
return Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Touches".to_string(),
expected: "exactly 2 arguments".to_string(),
actual: format!("{} arguments", args.len()),
});
}
match (&args[0], &args[1]) {
(SqlValue::Null, _) | (_, SqlValue::Null) => Ok(SqlValue::Null),
(
SqlValue::Varchar(wkt1) | SqlValue::Character(wkt1),
SqlValue::Varchar(wkt2) | SqlValue::Character(wkt2),
) => {
let geom1 = wkt_to_geo(wkt1)?;
let geom2 = wkt_to_geo(wkt2)?;
let relate_matrix = geom1.relate(&geom2);
let result = relate_matrix.is_touches();
Ok(SqlValue::Boolean(result))
}
_ => Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Touches".to_string(),
expected: "VARCHAR geometry arguments".to_string(),
actual: format!("{:?}, {:?}", args[0].type_name(), args[1].type_name()),
}),
}
}
pub fn st_crosses(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
if args.len() != 2 {
return Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Crosses".to_string(),
expected: "exactly 2 arguments".to_string(),
actual: format!("{} arguments", args.len()),
});
}
match (&args[0], &args[1]) {
(SqlValue::Null, _) | (_, SqlValue::Null) => Ok(SqlValue::Null),
(
SqlValue::Varchar(wkt1) | SqlValue::Character(wkt1),
SqlValue::Varchar(wkt2) | SqlValue::Character(wkt2),
) => {
let geom1 = wkt_to_geo(wkt1)?;
let geom2 = wkt_to_geo(wkt2)?;
let relate_matrix = geom1.relate(&geom2);
let result = relate_matrix.is_crosses();
Ok(SqlValue::Boolean(result))
}
_ => Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Crosses".to_string(),
expected: "VARCHAR geometry arguments".to_string(),
actual: format!("{:?}, {:?}", args[0].type_name(), args[1].type_name()),
}),
}
}
pub fn st_overlaps(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
if args.len() != 2 {
return Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Overlaps".to_string(),
expected: "exactly 2 arguments".to_string(),
actual: format!("{} arguments", args.len()),
});
}
match (&args[0], &args[1]) {
(SqlValue::Null, _) | (_, SqlValue::Null) => Ok(SqlValue::Null),
(
SqlValue::Varchar(wkt1) | SqlValue::Character(wkt1),
SqlValue::Varchar(wkt2) | SqlValue::Character(wkt2),
) => {
let geom1 = wkt_to_geo(wkt1)?;
let geom2 = wkt_to_geo(wkt2)?;
let relate_matrix = geom1.relate(&geom2);
let result = relate_matrix.is_overlaps();
Ok(SqlValue::Boolean(result))
}
_ => Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Overlaps".to_string(),
expected: "VARCHAR geometry arguments".to_string(),
actual: format!("{:?}, {:?}", args[0].type_name(), args[1].type_name()),
}),
}
}
pub fn st_covers(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
if args.len() != 2 {
return Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Covers".to_string(),
expected: "exactly 2 arguments".to_string(),
actual: format!("{} arguments", args.len()),
});
}
match (&args[0], &args[1]) {
(SqlValue::Null, _) | (_, SqlValue::Null) => Ok(SqlValue::Null),
(
SqlValue::Varchar(wkt1) | SqlValue::Character(wkt1),
SqlValue::Varchar(wkt2) | SqlValue::Character(wkt2),
) => {
let geom1 = wkt_to_geo(wkt1)?;
let geom2 = wkt_to_geo(wkt2)?;
let result = geom1.contains(&geom2);
Ok(SqlValue::Boolean(result))
}
_ => Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Covers".to_string(),
expected: "VARCHAR geometry arguments".to_string(),
actual: format!("{:?}, {:?}", args[0].type_name(), args[1].type_name()),
}),
}
}
pub fn st_coveredby(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
if args.len() != 2 {
return Err(ExecutorError::SpatialArgumentError {
function_name: "ST_CoveredBy".to_string(),
expected: "exactly 2 arguments".to_string(),
actual: format!("{} arguments", args.len()),
});
}
match (&args[0], &args[1]) {
(SqlValue::Null, _) | (_, SqlValue::Null) => Ok(SqlValue::Null),
(
SqlValue::Varchar(wkt1) | SqlValue::Character(wkt1),
SqlValue::Varchar(wkt2) | SqlValue::Character(wkt2),
) => {
let geom1 = wkt_to_geo(wkt1)?;
let geom2 = wkt_to_geo(wkt2)?;
let result = geom2.contains(&geom1);
Ok(SqlValue::Boolean(result))
}
_ => Err(ExecutorError::SpatialArgumentError {
function_name: "ST_CoveredBy".to_string(),
expected: "VARCHAR geometry arguments".to_string(),
actual: format!("{:?}, {:?}", args[0].type_name(), args[1].type_name()),
}),
}
}
pub fn st_dwithin(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
if args.len() != 3 {
return Err(ExecutorError::SpatialArgumentError {
function_name: "ST_DWithin".to_string(),
expected: "exactly 3 arguments".to_string(),
actual: format!("{} arguments", args.len()),
});
}
let distance = match &args[2] {
SqlValue::Double(d) => *d,
SqlValue::Numeric(d) => *d,
SqlValue::Integer(i) => *i as f64,
SqlValue::Float(f) => *f as f64,
SqlValue::Null => return Ok(SqlValue::Null),
_ => {
return Err(ExecutorError::SpatialArgumentError {
function_name: "ST_DWithin".to_string(),
expected: "numeric distance".to_string(),
actual: format!("{:?}", args[2].type_name()),
})
}
};
if distance < 0.0 {
return Ok(SqlValue::Boolean(false));
}
match (&args[0], &args[1]) {
(SqlValue::Null, _) | (_, SqlValue::Null) => Ok(SqlValue::Null),
(
SqlValue::Varchar(wkt1) | SqlValue::Character(wkt1),
SqlValue::Varchar(wkt2) | SqlValue::Character(wkt2),
) => {
let geom1 = wkt_to_geo(wkt1)?;
let geom2 = wkt_to_geo(wkt2)?;
let dist = match (&geom1, &geom2) {
(geo::Geometry::Point(p1), geo::Geometry::Point(p2)) => {
Haversine.distance(*p1, *p2)
}
_ => {
Euclidean.distance(&geom1, &geom2)
}
};
let result = dist <= distance;
Ok(SqlValue::Boolean(result))
}
_ => Err(ExecutorError::SpatialArgumentError {
function_name: "ST_DWithin".to_string(),
expected: "(VARCHAR, VARCHAR, NUMERIC) arguments".to_string(),
actual: format!(
"{:?}, {:?}, {:?}",
args[0].type_name(),
args[1].type_name(),
args[2].type_name()
),
}),
}
}
pub fn st_relate(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
if args.len() < 2 || args.len() > 3 {
return Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Relate".to_string(),
expected: "2 or 3 arguments".to_string(),
actual: format!("{} arguments", args.len()),
});
}
match (&args[0], &args[1]) {
(SqlValue::Null, _) | (_, SqlValue::Null) => Ok(SqlValue::Null),
(
SqlValue::Varchar(wkt1) | SqlValue::Character(wkt1),
SqlValue::Varchar(wkt2) | SqlValue::Character(wkt2),
) => {
let geom1 = wkt_to_geo(wkt1)?;
let geom2 = wkt_to_geo(wkt2)?;
if args.len() == 2 {
if !geom1.intersects(&geom2) {
Ok(SqlValue::Varchar(arcstr::ArcStr::from("FF*FF****")))
} else if geom1.contains(&geom2) && !geom2.contains(&geom1) {
Ok(SqlValue::Varchar(arcstr::ArcStr::from("T*F**F***")))
} else if geom2.contains(&geom1) && !geom1.contains(&geom2) {
Ok(SqlValue::Varchar(arcstr::ArcStr::from("F*T**F***")))
} else if geom1 == geom2 {
Ok(SqlValue::Varchar(arcstr::ArcStr::from("T*F**FFF*")))
} else {
Ok(SqlValue::Varchar(arcstr::ArcStr::from("T*T***T**")))
}
} else {
Err(ExecutorError::UnsupportedFeature(
"ST_Relate with pattern matching requires full DE-9IM implementation (Phase 4)"
.to_string(),
))
}
}
_ => Err(ExecutorError::SpatialArgumentError {
function_name: "ST_Relate".to_string(),
expected: "VARCHAR geometry arguments".to_string(),
actual: format!("{:?}, {:?}", args[0].type_name(), args[1].type_name()),
}),
}
}