Skip to main content

nodedb_query/geo_functions/
dispatch.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Entry point for geospatial function evaluation.
4
5use nodedb_types::Value;
6
7use super::catalog;
8use super::{accessors, constructors, indexing, measures, predicates};
9
10/// Evaluate a geospatial function.
11///
12/// `name` may be any spelling the catalog accepts — the standard `ST_*` name
13/// or an internal `geo_*` one — and is resolved to a single canonical entry
14/// before dispatch, so every spelling of a capability behaves identically.
15///
16/// Returns `None` only when `name` is not a geospatial function at all, so the
17/// caller can fall through to its own function table.
18pub fn eval_geo_function(name: &str, args: &[Value]) -> Option<Value> {
19    let spec = catalog::lookup(name)?;
20    eval_canonical(spec.canonical, args)
21}
22
23/// Route a canonical name to its family evaluator.
24///
25/// A `None` here means the catalog declares a function the evaluators do not
26/// implement. That drift is caught by `every_catalog_entry_is_evaluable`
27/// below rather than being allowed to surface as a silent NULL at runtime.
28fn eval_canonical(canonical: &str, args: &[Value]) -> Option<Value> {
29    predicates::eval(canonical, args)
30        .or_else(|| measures::eval(canonical, args))
31        .or_else(|| accessors::eval(canonical, args))
32        .or_else(|| constructors::eval(canonical, args))
33        .or_else(|| indexing::eval(canonical, args))
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39    use nodedb_types::geometry::Geometry;
40
41    /// Arguments that satisfy any catalog shape: enough geometry-shaped and
42    /// number-shaped values that no evaluator indexes past the end.
43    fn probe_args() -> Vec<Value> {
44        vec![
45            Value::Geometry(Geometry::point(1.0, 2.0)),
46            Value::Geometry(Geometry::point(3.0, 4.0)),
47            Value::Float(5.0),
48            Value::Float(6.0),
49        ]
50    }
51
52    /// The catalog drives the SQL function registry, so a name it declares is
53    /// accepted at plan time. If no evaluator implements that name, the query
54    /// plans and then evaluates to NULL for every row — the exact silent
55    /// failure this catalog exists to prevent. Every entry must dispatch.
56    #[test]
57    fn every_catalog_entry_is_evaluable() {
58        for spec in catalog::GEO_FUNCTIONS {
59            assert!(
60                eval_canonical(spec.canonical, &probe_args()).is_some(),
61                "catalog declares '{}' but no evaluator handles it",
62                spec.canonical
63            );
64        }
65    }
66
67    /// Every accepted spelling must reach the same evaluator as its canonical
68    /// name — the property that makes `ST_X` and `geo_x` interchangeable.
69    #[test]
70    fn every_alias_evaluates_identically_to_its_canonical_name() {
71        for spec in catalog::GEO_FUNCTIONS {
72            let canonical = eval_geo_function(spec.canonical, &probe_args());
73            for alias in spec.aliases {
74                assert_eq!(
75                    eval_geo_function(alias, &probe_args()),
76                    canonical,
77                    "'{alias}' must behave exactly like '{}'",
78                    spec.canonical
79                );
80            }
81        }
82    }
83
84    /// Names are matched case-insensitively, as SQL identifiers are.
85    #[test]
86    fn dispatch_is_case_insensitive() {
87        let args = probe_args();
88        assert_eq!(
89            eval_geo_function("ST_AsText", &args),
90            eval_geo_function("st_astext", &args)
91        );
92    }
93
94    #[test]
95    fn non_geo_function_falls_through() {
96        assert_eq!(eval_geo_function("lower", &probe_args()), None);
97    }
98
99    /// The reported bug in one line: the standard accessor spelling must read
100    /// a geometry that the internal spelling already read.
101    #[test]
102    fn standard_accessor_names_read_stored_geometry() {
103        use crate::value_ops::to_value_number;
104        let point = vec![Value::Geometry(Geometry::point(1.0, 2.0))];
105        assert_eq!(
106            eval_geo_function("st_x", &point),
107            Some(to_value_number(1.0))
108        );
109        assert_eq!(
110            eval_geo_function("st_y", &point),
111            Some(to_value_number(2.0))
112        );
113        assert!(matches!(
114            eval_geo_function("st_astext", &point),
115            Some(Value::String(_))
116        ));
117    }
118}