use crate::{geometry::spatial_op, precedence, temporal::temporal_op, Error, Geometry, Validator};
use geo_types::{coord, Geometry as GGeom, Rect};
use json_dotpath::DotPaths;
use like::Like;
use pg_escape::quote_identifier;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{collections::HashSet, fmt::Debug, ops::Add, str::FromStr, sync::OnceLock};
use unaccent::unaccent;
use wkt::TryFromWkt;
pub const BOOLOPS: &[&str] = &["and", "or"];
pub const EQOPS: &[&str] = &["=", "<>"];
pub const CMPOPS: &[&str] = &[">", ">=", "<", "<="];
pub const SPATIALOPS: &[&str] = &[
"s_equals",
"s_intersects",
"s_disjoint",
"s_touches",
"s_within",
"s_overlaps",
"s_crosses",
"s_contains",
];
pub const TEMPORALOPS: &[&str] = &[
"t_before",
"t_after",
"t_meets",
"t_metBy",
"t_overlaps",
"t_overlappedBy",
"t_starts",
"t_startedBy",
"t_during",
"t_contains",
"t_finishes",
"t_finishedBy",
"t_equals",
"t_disjoint",
"t_intersects",
];
pub const ARITHOPS: &[&str] = &["+", "-", "*", "/", "%", "^", "div"];
pub const ARRAYOPS: &[&str] = &["a_equals", "a_contains", "a_containedBy", "a_overlaps"];
const CHAINED_ARITHOPS: &[&str] = &["+", "-", "*", "/", "%"];
const OTHER_OPS: &[&str] = &["not", "like", "between", "in", "isNull", "casei", "accenti"];
fn canonical_ops() -> impl Iterator<Item = &'static str> {
[
BOOLOPS,
EQOPS,
CMPOPS,
SPATIALOPS,
TEMPORALOPS,
ARITHOPS,
ARRAYOPS,
OTHER_OPS,
]
.into_iter()
.flat_map(|ops| ops.iter().copied())
}
pub(crate) fn normalize(expr: Expr) -> Expr {
match expr {
Expr::Operation { op, args } => {
let op = canonical_op(&op);
let args = args.into_iter().map(|arg| Box::new(normalize(*arg)));
if op == "and" || op == "or" {
let mut flat: Vec<Box<Expr>> = Vec::new();
for arg in args {
match *arg {
Expr::Operation {
op: nested,
args: inner,
} if nested == op => flat.extend(inner),
other => flat.push(Box::new(other)),
}
}
Expr::Operation { op, args: flat }
} else {
Expr::Operation {
op,
args: args.collect(),
}
}
}
Expr::Array(items) => {
Expr::Array(items.into_iter().map(|i| Box::new(normalize(*i))).collect())
}
Expr::Timestamp { timestamp } => Expr::Timestamp {
timestamp: Box::new(normalize_instant(*timestamp)),
},
Expr::Interval { interval } => Expr::Interval {
interval: interval
.into_iter()
.map(|bound| Box::new(normalize_instant(*bound)))
.collect(),
},
other => other,
}
}
fn normalize_instant(expr: Expr) -> Expr {
match expr {
Expr::Literal(value) => Expr::Literal(crate::temporal::canonical_timestamp(&value)),
other => normalize(other),
}
}
fn literal(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
const GRAMMAR_RESERVED: &[&str] = &["true", "false", "null", "not"];
fn identifier(name: &str) -> String {
let mut chars = name.chars();
let is_bare = chars.next().is_some_and(|c| c.is_ascii_alphabetic())
&& chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | ':'))
&& !GRAMMAR_RESERVED
.iter()
.any(|reserved| reserved.eq_ignore_ascii_case(name));
if is_bare {
name.to_string()
} else {
quote_identifier(name).to_string()
}
}
fn temporal_literal(value: &str) -> Expr {
let literal = Box::new(Expr::Literal(value.to_string()));
if value.contains('T') || value.contains(' ') {
Expr::Timestamp { timestamp: literal }
} else {
Expr::Date { date: literal }
}
}
fn is_region(expr: &Expr) -> bool {
matches!(expr, Expr::Geometry(_) | Expr::BBox { .. })
}
fn reduce_arity(op: &str) -> Option<usize> {
match op {
"isNull" | "not" | "casei" | "accenti" => Some(1),
"between" => Some(3),
_ => None,
}
}
pub(crate) fn canonical_op(name: &str) -> String {
let aliased = ALIASES
.iter()
.find(|(alias, _)| alias.eq_ignore_ascii_case(name))
.map_or(name, |(_, canonical)| canonical);
canonical_ops()
.find(|canonical| canonical.eq_ignore_ascii_case(aliased))
.map_or_else(|| aliased.to_string(), str::to_string)
}
const ALIASES: &[(&str, &str)] = &[
("!=", "<>"),
("st_equals", "s_equals"),
("st_intersects", "s_intersects"),
("st_disjoint", "s_disjoint"),
("st_touches", "s_touches"),
("st_within", "s_within"),
("st_overlaps", "s_overlaps"),
("st_crosses", "s_crosses"),
("st_contains", "s_contains"),
("intersects", "s_intersects"),
("anyinteracts", "t_intersects"),
];
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, PartialOrd)]
#[serde(untagged, remote = "Self")]
#[allow(missing_docs)]
pub enum Expr {
Operation { op: String, args: Vec<Box<Expr>> },
Interval { interval: Vec<Box<Expr>> },
Timestamp { timestamp: Box<Expr> },
Date { date: Box<Expr> },
Property { property: String },
BBox { bbox: Vec<Box<Expr>> },
Float(f64),
Literal(String),
Bool(bool),
Array(Vec<Box<Expr>>),
Geometry(Geometry),
Null,
}
impl Serialize for Expr {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
Expr::serialize(self, serializer)
}
}
impl<'de> Deserialize<'de> for Expr {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Expr::deserialize(deserializer).map(normalize)
}
}
impl TryFrom<Value> for Expr {
type Error = Error;
fn try_from(v: Value) -> Result<Expr, Error> {
serde_json::from_value(v).map_err(Error::from)
}
}
impl TryFrom<Expr> for Value {
type Error = Error;
fn try_from(v: Expr) -> Result<Value, Error> {
serde_json::to_value(v).map_err(Error::from)
}
}
impl TryFrom<Expr> for f64 {
type Error = Error;
fn try_from(v: Expr) -> Result<f64, Error> {
match v {
Expr::Float(v) => Ok(v),
Expr::Literal(v) => f64::from_str(&v).map_err(Error::from),
_ => Err(Error::ExprToF64(v)),
}
}
}
impl TryFrom<&Expr> for bool {
type Error = Error;
fn try_from(v: &Expr) -> Result<bool, Error> {
match v {
Expr::Bool(v) => Ok(*v),
Expr::Literal(v) => bool::from_str(v).map_err(Error::from),
_ => Err(Error::ExprToBool(v.clone())),
}
}
}
impl TryFrom<Expr> for String {
type Error = Error;
fn try_from(v: Expr) -> Result<String, Error> {
match v {
Expr::Literal(v) => Ok(v),
Expr::Bool(v) => Ok(v.to_string()),
Expr::Float(v) => Ok(v.to_string()),
_ => Err(Error::ExprToBool(v)),
}
}
}
impl TryFrom<Expr> for GGeom {
type Error = Error;
fn try_from(v: Expr) -> Result<GGeom, Error> {
match v {
Expr::Geometry(ref g) => {
GGeom::try_from_wkt_str(&g.to_wkt()?).map_err(|_| Error::ExprToGeom(v.clone()))
}
Expr::BBox { ref bbox } => {
let [minx, miny, maxx, maxy] = match bbox.as_slice() {
[minx, miny, maxx, maxy] => [minx, miny, maxx, maxy],
[minx, miny, _minz, maxx, maxy, _maxz] => [minx, miny, maxx, maxy],
_ => return Err(Error::ExprToGeom(v.clone())),
};
let minx: f64 = minx.as_ref().clone().try_into()?;
let miny: f64 = miny.as_ref().clone().try_into()?;
let maxx: f64 = maxx.as_ref().clone().try_into()?;
let maxy: f64 = maxy.as_ref().clone().try_into()?;
let rec = Rect::new(coord! {x:minx, y:miny}, coord! {x:maxx,y:maxy});
Ok(rec.into())
}
_ => Err(Error::ExprToGeom(v)),
}
}
}
impl TryFrom<Expr> for HashSet<String> {
type Error = Error;
fn try_from(v: Expr) -> Result<HashSet<String>, Error> {
match v {
Expr::Array(v) => {
let mut h = HashSet::new();
for el in v {
let _ = h.insert(el.to_text()?);
}
Ok(h)
}
_ => Err(Error::ExprToGeom(v)),
}
}
}
fn cmp_op<T: PartialEq + PartialOrd>(left: T, right: T, op: &str) -> Result<Expr, Error> {
let out = match op {
"=" => left == right,
"<=" => left <= right,
"<" => left < right,
">=" => left >= right,
">" => left > right,
"<>" => left != right,
_ => return Err(Error::OperationError()),
};
Ok(Expr::Bool(out))
}
fn arith_op(left: Expr, right: Expr, op: &str) -> Result<Expr, Error> {
let left = f64::try_from(left)?;
let right = f64::try_from(right)?;
let out = match op {
"+" => left + right,
"-" => left - right,
"*" => left * right,
"/" => left / right,
"%" => left % right,
"^" => left.powf(right),
"div" => {
if right == 0.0 {
return Err(Error::OperationError());
}
(left / right).trunc()
}
_ => return Err(Error::OperationError()),
};
Ok(Expr::Float(out))
}
fn array_op(left: Expr, right: Expr, op: &str) -> Result<Expr, Error> {
let left: HashSet<String> = left.try_into()?;
let right: HashSet<String> = right.try_into()?;
let out = match op {
"a_equals" => left == right,
"a_contains" => left.is_superset(&right),
"a_containedBy" => left.is_subset(&right),
"a_overlaps" => !left.is_disjoint(&right),
_ => return Err(Error::OperationError()),
};
Ok(Expr::Bool(out))
}
fn is_defined_operator(op: &str) -> bool {
canonical_ops().any(|known| known == op)
}
fn is_unknown(expr: &Expr) -> bool {
match expr {
Expr::Property { .. } | Expr::Operation { .. } => true,
Expr::Interval { interval } => interval.iter().any(|e| is_unknown(e)),
Expr::Date { date } => is_unknown(date),
Expr::Timestamp { timestamp } => is_unknown(timestamp),
Expr::Array(elements) => elements.iter().any(|e| is_unknown(e)),
Expr::BBox { bbox } => bbox.iter().any(|e| is_unknown(e)),
Expr::Float(_) | Expr::Literal(_) | Expr::Bool(_) | Expr::Geometry(_) | Expr::Null => false,
}
}
impl Expr {
pub fn reduce(self, j: Option<&Value>) -> Result<Expr, Error> {
match self {
Expr::Property { ref property } => {
let Some(j) = j else { return Ok(self) };
if let Some(value) = j.dot_get::<Value>(property)? {
Expr::try_from(value)
} else if let Some(value) = j.dot_get::<Value>(&format!("properties.{property}"))? {
Expr::try_from(value)
} else {
Ok(self)
}
}
Expr::Interval { ref interval } => {
let [lo, hi] = interval.as_slice() else {
return Err(Error::InvalidNumberOfArguments {
name: "interval".to_string(),
actual: interval.len(),
expected: 2,
});
};
let start = lo.as_ref().clone().reduce(j)?;
let end = hi.as_ref().clone().reduce(j)?;
Ok(Expr::Interval {
interval: vec![Box::new(start), Box::new(end)],
})
}
Expr::Operation { op, args } => {
let op = canonical_op(&op);
if let Some(expected) = reduce_arity(&op) {
if args.len() != expected {
return Err(Error::InvalidNumberOfArguments {
name: op,
actual: args.len(),
expected,
});
}
}
let args: Vec<Box<Expr>> = args
.into_iter()
.map(|expr| expr.reduce(j).map(Box::new))
.collect::<Result<_, _>>()?;
if op == "isNull" {
if matches!(args[0].as_ref(), Expr::Null) {
Ok(Expr::Bool(true))
} else if is_unknown(args[0].as_ref()) {
if j.is_some() {
Ok(Expr::Bool(true))
} else {
Ok(Expr::Operation {
op: "isNull".to_string(),
args,
})
}
} else {
Ok(Expr::Bool(false))
}
} else if BOOLOPS.contains(&op.as_str()) {
let mut dedupargs: Vec<Box<Expr>> = vec![];
let mut nestedargs: Vec<Box<Expr>> = vec![];
for a in args {
match *a {
Expr::Operation {
op: nested,
args: inner,
} if nested == op => nestedargs.extend(inner),
_ => dedupargs.push(a),
}
}
dedupargs.append(&mut nestedargs);
dedupargs.sort_by(|a, b| {
a.partial_cmp(b)
.unwrap_or_else(|| format!("{a:?}").cmp(&format!("{b:?}")))
});
dedupargs.dedup();
let mut anytrue: bool = false;
let mut anyfalse: bool = false;
let mut anynull: bool = false;
let mut anyexp: bool = false;
for a in dedupargs.iter() {
if matches!(a.as_ref(), Expr::Null) {
anynull = true;
continue;
}
let b = bool::try_from(a.as_ref());
match b {
Ok(true) => {
anytrue = true;
}
Ok(false) => {
anyfalse = true;
}
_ => {
anyexp = true;
}
}
}
if op == "and" && anyfalse {
return Ok(Expr::Bool(false));
}
if op == "or" && anytrue {
return Ok(Expr::Bool(true));
}
if op == "and" && anytrue {
dedupargs.retain(|x| !bool::try_from(x.as_ref()).unwrap_or(false));
}
if dedupargs.len() == 1 {
Ok(*dedupargs.pop().unwrap())
} else if !anyexp && anynull {
Ok(Expr::Null)
} else if !anyexp && op == "or" {
Ok(Expr::Bool(false))
} else if !anyexp && op == "and" {
Ok(Expr::Bool(true))
} else {
Ok(Expr::Operation {
op,
args: dedupargs,
})
}
} else if op == "not" {
match args[0].as_ref() {
Expr::Bool(v) => Ok(Expr::Bool(!v)),
Expr::Null => Ok(Expr::Null),
_ => Ok(Expr::Operation { op, args }),
}
} else if is_defined_operator(&op)
&& args.iter().any(|arg| matches!(arg.as_ref(), Expr::Null))
{
Ok(Expr::Null)
} else if op == "casei" {
match args[0].as_ref() {
Expr::Literal(v) => Ok(Expr::Literal(v.to_lowercase())),
_ => Ok(Expr::Operation { op, args }),
}
} else if op == "accenti" {
match args[0].as_ref() {
Expr::Literal(v) => Ok(Expr::Literal(unaccent(v))),
_ => Ok(Expr::Operation { op, args }),
}
} else if op == "between" {
if args.iter().any(|a| is_unknown(a)) {
Ok(Expr::Operation { op, args })
} else {
Ok(Expr::Bool(args[0] >= args[1] && args[0] <= args[2]))
}
} else if CHAINED_ARITHOPS.contains(&op.as_str()) && args.len() > 2 {
let mut operands = args.iter().map(|arg| arg.as_ref().clone());
let first = operands.next().expect("length checked above");
let folded = operands.try_fold(first, |left, right| {
Expr::Operation {
op: op.clone(),
args: vec![Box::new(left), Box::new(right)],
}
.reduce(j)
})?;
Ok(match folded {
Expr::Operation { .. } => Expr::Operation { op, args },
value => value,
})
} else if args.len() != 2 {
Ok(Expr::Operation { op, args })
} else {
let mut left = args[0].as_ref().clone();
let mut right = args[1].as_ref().clone();
if is_unknown(&left) || is_unknown(&right) {
return Ok(Expr::Operation { op, args });
}
let is_temporal_relation = TEMPORALOPS.contains(&op.as_str());
let is_spatial_relation = SPATIALOPS.contains(&op.as_str());
let is_comparison =
EQOPS.contains(&op.as_str()) || CMPOPS.contains(&op.as_str());
match (&left, &right) {
(Expr::Date { .. }, Expr::Literal(ref v)) if is_temporal_relation => {
right = temporal_literal(v);
}
(Expr::Date { .. }, Expr::Literal(ref v)) => {
right = Expr::Date {
date: Box::new(Expr::Literal(v.clone())),
};
}
(Expr::Timestamp { .. }, Expr::Literal(ref v)) => {
right = Expr::Timestamp {
timestamp: Box::new(Expr::Literal(v.clone())),
};
}
(Expr::Literal(ref v), Expr::Date { .. }) if is_temporal_relation => {
left = temporal_literal(v);
}
(Expr::Literal(ref v), Expr::Date { .. }) => {
left = Expr::Date {
date: Box::new(Expr::Literal(v.clone())),
};
}
(Expr::Literal(ref v), Expr::Timestamp { .. }) => {
left = Expr::Timestamp {
timestamp: Box::new(Expr::Literal(v.clone())),
};
}
_ => {}
}
if is_temporal_relation {
match temporal_op(left, right, &op) {
Ok(reduced) => Ok(reduced),
Err(_) => Ok(Expr::Operation { op, args }),
}
} else if matches!(left, Expr::Date { .. } | Expr::Timestamp { .. })
&& matches!(right, Expr::Date { .. } | Expr::Timestamp { .. })
&& is_comparison
{
let l_dr = crate::temporal::DateRange::try_from(left)?;
let r_dr = crate::temporal::DateRange::try_from(right)?;
cmp_op(l_dr, r_dr, &op)
} else if std::mem::discriminant(&left) == std::mem::discriminant(&right)
|| (is_spatial_relation && is_region(&left) && is_region(&right))
{
if is_spatial_relation {
Ok(spatial_op(left, right, &op)
.unwrap_or_else(|_| Expr::Operation { op, args }))
} else if ARITHOPS.contains(&op.as_str()) {
Ok(arith_op(left, right, &op)
.unwrap_or_else(|_| Expr::Operation { op, args }))
} else if is_comparison {
Ok(cmp_op(left, right, &op)
.unwrap_or_else(|_| Expr::Operation { op, args }))
} else if ARRAYOPS.contains(&op.as_str()) {
Ok(array_op(left, right, &op)
.unwrap_or_else(|_| Expr::Operation { op, args }))
} else if op == "like" {
let l: String = left.try_into()?;
let r: String = right.try_into()?;
let m: bool = Like::<true>::like(l.as_str(), r.as_str())?;
Ok(Expr::Bool(m))
} else {
Ok(Expr::Operation { op, args })
}
} else if op == "in" {
let has_null = matches!(&right, Expr::Array(items)
if items.iter().any(|item| matches!(item.as_ref(), Expr::Null)));
let l: String = left.to_text()?;
let r: HashSet<String> = right.try_into()?;
let isin: bool = r.contains(&l);
Ok(match (isin, has_null) {
(true, _) => Expr::Bool(true),
(false, true) => Expr::Null,
(false, false) => Expr::Bool(false),
})
} else {
Ok(Expr::Operation { op, args })
}
}
}
_ => Ok(self),
}
}
pub fn matches(self, j: Option<&Value>) -> Result<bool, Error> {
let reduced = self.reduce(j)?;
match reduced {
Expr::Bool(v) => Ok(v),
Expr::Null => Ok(false),
_ => Err(Error::NonReduced()),
}
}
pub fn is_true(self) -> bool {
matches!(self, Expr::Bool(true))
}
pub fn filter<'a, I>(&self, items: I) -> Result<Vec<&'a Value>, Error>
where
I: IntoIterator<Item = &'a Value>,
{
let mut filtered = Vec::new();
for item in items {
let e = self.clone().reduce(Some(item))?;
if e.is_true() {
filtered.push(item)
}
}
Ok(filtered)
}
pub fn to_text(&self) -> Result<String, Error> {
macro_rules! check_len {
($name:expr, $args:expr, $len:expr, $text:expr) => {
if $args.len() == $len {
Ok($text)
} else {
Err(Error::InvalidNumberOfArguments {
name: $name.to_string(),
actual: $args.len(),
expected: $len,
})
}
};
}
match self {
Expr::Bool(v) => Ok(v.to_string()),
Expr::Float(v) if !v.is_finite() => Err(Error::NonFiniteNumber(*v)),
Expr::Float(v) => Ok(v.to_string()),
Expr::Literal(v) => Ok(literal(v)),
Expr::Property { property } => Ok(identifier(property)),
Expr::Null => Ok("NULL".to_string()),
Expr::Interval { interval } => {
check_len!(
"interval",
interval,
2,
format!(
"INTERVAL({},{})",
interval[0].to_text()?,
interval[1].to_text()?
)
)
}
Expr::Date { date } => Ok(format!("DATE({})", date.to_text()?)),
Expr::Timestamp { timestamp } => Ok(format!("TIMESTAMP({})", timestamp.to_text()?)),
Expr::Geometry(v) => v.to_wkt(),
Expr::Array(v) => {
let array_els: Vec<String> =
v.iter().map(|a| a.to_text()).collect::<Result<_, _>>()?;
Ok(format!("({})", array_els.join(", ")))
}
Expr::Operation { op, args } => {
let op = canonical_op(op);
let requirement = precedence::operands(&op);
let a: Vec<String> = args
.iter()
.enumerate()
.map(|(index, arg)| {
let text = arg.to_text()?;
Ok(if requirement.needs_parens(index, arg) {
format!("({})", text)
} else {
text
})
})
.collect::<Result<_, Error>>()?;
match op.as_str() {
"and" => Ok(a.join(" AND ")),
"or" => Ok(a.join(" OR ")),
"like" => {
check_len!("like", a, 2, format!("{} LIKE {}", a[0], a[1]))
}
"in" => {
check_len!("in", a, 2, format!("{} IN {}", a[0], a[1]))
}
"between" => {
check_len!(
"between",
a,
3,
format!("{} BETWEEN {} AND {}", a[0], a[1], a[2])
)
}
"not" => {
check_len!("not", a, 1, format!("NOT {}", a[0]))
}
"isNull" => {
check_len!("is null", a, 1, format!("{} IS NULL", a[0]))
}
"+" | "-" | "*" | "/" | "%" => {
if a.len() < 2 {
return Err(Error::InvalidNumberOfArguments {
name: op.to_string(),
actual: a.len(),
expected: 2,
});
}
let paddedop = format!(" {} ", op);
Ok(a.join(&paddedop))
}
"^" | "=" | "<=" | "<" | "<>" | ">" | ">=" => {
check_len!(op, a, 2, format!("{} {} {}", a[0], op, a[1]))
}
_ => Ok(format!("{}({})", identifier(&op), a.join(", "))),
}
}
Expr::BBox { bbox } => {
let array_els: Vec<String> =
bbox.iter().map(|a| a.to_text()).collect::<Result<_, _>>()?;
Ok(format!("BBOX({})", array_els.join(", ")))
}
}
}
pub fn to_json(&self) -> Result<String, Error> {
serde_json::to_string(&self).map_err(Error::from)
}
pub fn to_json_pretty(&self) -> Result<String, Error> {
serde_json::to_string_pretty(&self).map_err(Error::from)
}
pub fn to_value(&self) -> Result<Value, Error> {
serde_json::to_value(self).map_err(Error::from)
}
pub fn is_valid(&self) -> bool {
static VALIDATOR: OnceLock<Validator> = OnceLock::new();
let value = serde_json::to_value(self);
match &value {
Ok(value) => {
let validator = VALIDATOR
.get_or_init(|| Validator::new().expect("Could not create default validator"));
validator.is_valid(value)
}
_ => false,
}
}
}
impl FromStr for Expr {
type Err = Error;
fn from_str(s: &str) -> Result<Expr, Error> {
if s.starts_with('{') {
crate::parse_json(s).map_err(Error::from)
} else {
crate::parse_text(s)
}
}
}
impl Add for Expr {
type Output = Expr;
fn add(self, other: Expr) -> Expr {
Expr::Operation {
op: "and".to_string(),
args: vec![Box::new(self), Box::new(other)],
}
}
}
#[cfg(test)]
mod tests {
use super::{canonical_op, canonical_ops, ALIASES, SPATIALOPS};
use crate::Expr;
use serde_json::Value;
use std::collections::HashSet;
#[test]
fn every_spatial_operator_has_an_st_alias() {
for op in SPATIALOPS {
let st = format!("st_{}", op.trim_start_matches("s_"));
assert_eq!(canonical_op(&st), *op, "'{st}' does not resolve to '{op}'");
}
}
#[test]
fn aliases_resolve_to_canonical_operators() {
let known: HashSet<&str> = canonical_ops().collect();
for (alias, canonical) in ALIASES {
assert!(
known.contains(canonical),
"'{alias}' resolves to '{canonical}', which is not an operator"
);
}
}
#[test]
fn aliases_fold_in_both_encodings() {
for (source, expected) in [
("ST_Intersects(geom, POINT(0 0))", "s_intersects"),
("st_intersects(geom, POINT(0 0))", "s_intersects"),
("INTERSECTS(geom, POINT(0 0))", "s_intersects"),
("AnyInteracts(a, b)", "t_intersects"),
("ST_CONTAINS(geom, POINT(0 0))", "s_contains"),
] {
for text in [
source.to_string(),
{
let name = source.split('(').next().expect("has a name");
format!(r#"{{"op":"{name}","args":[{{"property":"a"}},{{"property":"b"}}]}}"#)
},
] {
let Ok(Expr::Operation { op, .. }) = text.parse::<Expr>() else {
panic!("{text} should parse to an operation");
};
assert_eq!(op, expected, "{text} did not fold to {expected}");
}
}
}
#[test]
fn nary_arithmetic_needs_two_operands() {
for op in ["+", "-", "*", "/", "%"] {
for count in [0, 1] {
let expr = Expr::Operation {
op: op.to_string(),
args: (0..count)
.map(|i| {
Box::new(Expr::Property {
property: format!("a{i}"),
})
})
.collect(),
};
assert!(
matches!(
expr.to_text(),
Err(crate::Error::InvalidNumberOfArguments { .. })
),
"{op} rendered {count} operand(s) as text: {:?}",
expr.to_text()
);
assert!(matches!(
crate::ToSqlAst::to_sql(&expr),
Err(crate::Error::InvalidNumberOfArguments { .. })
));
}
let expr = Expr::Operation {
op: op.to_string(),
args: vec![
Box::new(Expr::Float(1.0)),
Box::new(Expr::Float(2.0)),
Box::new(Expr::Float(3.0)),
],
};
assert_eq!(
expr.to_text().expect("three operands render"),
format!("1 {op} 2 {op} 3")
);
}
}
#[test]
fn non_finite_numbers_have_no_text() {
for value in [f64::INFINITY, f64::NEG_INFINITY, f64::NAN] {
assert!(
matches!(
Expr::Float(value).to_text(),
Err(crate::Error::NonFiniteNumber(_))
),
"{value} rendered as cql2-text"
);
}
let divided: Expr = "1 / 0".parse().unwrap();
assert!(matches!(
divided.reduce(None).unwrap().to_text(),
Err(crate::Error::NonFiniteNumber(_))
));
let expr = Expr::Operation {
op: ">".to_string(),
args: vec![
Box::new(Expr::Property {
property: "a".to_string(),
}),
Box::new(Expr::Float(f64::INFINITY)),
],
};
assert!(matches!(
expr.to_text(),
Err(crate::Error::NonFiniteNumber(_))
));
}
#[test]
fn keep_z() {
let point: Expr = "POINT Z(-105.1019 40.1672 4981)".parse().unwrap();
assert_eq!("POINT Z(-105.1019 40.1672 4981)", point.to_text().unwrap());
}
#[test]
fn implicit_z() {
let point: Expr = "POINT (-105.1019 40.1672 4981)".parse().unwrap();
assert_eq!("POINT Z(-105.1019 40.1672 4981)", point.to_text().unwrap());
}
#[test]
fn keep_m() {
let point: Expr = "POINT M(-105.1019 40.1672 42)".parse().unwrap();
assert_eq!("POINT M(-105.1019 40.1672 42)", point.to_text().unwrap());
}
#[test]
fn keep_zm() {
let point: Expr = "POINT ZM(-105.1019 40.1672 4981 42)".parse().unwrap();
assert_eq!(
"POINT ZM(-105.1019 40.1672 4981 42)",
point.to_text().unwrap()
);
}
#[test]
fn keep_one_element_lists() {
let expr: Expr = "ogc_fid IN ('1')".parse().unwrap();
assert_eq!(expr.to_text().unwrap(), "ogc_fid IN ('1')");
}
#[test]
fn canonical_ops_match_the_schema() {
let schema: Value =
serde_json::from_str(include_str!("cql2.json")).expect("schema is valid JSON");
let mut from_schema = HashSet::new();
collect_operator_enums(&schema, &mut from_schema);
assert!(
!from_schema.is_empty(),
"found no operator enums in the schema"
);
let known: HashSet<String> = canonical_ops().map(str::to_string).collect();
let missing: Vec<&String> = from_schema.difference(&known).collect();
assert!(
missing.is_empty(),
"these schema operators are absent from the operator constants: {missing:?}"
);
}
fn collect_operator_enums(node: &Value, out: &mut HashSet<String>) {
match node {
Value::Object(fields) => {
if let Some(Value::Array(values)) = fields.get("enum") {
let names: Vec<String> = values
.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect();
if names.iter().any(|n| n == "t_metBy" || n == "a_containedBy") {
out.extend(names);
}
}
for value in fields.values() {
collect_operator_enums(value, out);
}
}
Value::Array(items) => items
.iter()
.for_each(|item| collect_operator_enums(item, out)),
_ => {}
}
}
}