Skip to main content

nodedb_query/functions/
mod.rs

1//! Scalar function evaluation for SqlExpr.
2//!
3//! All functions return `Value::Null` on invalid/missing
4//! arguments (SQL NULL propagation semantics).
5
6mod array;
7mod conditional;
8mod datetime;
9mod id;
10mod json;
11mod math;
12pub(crate) mod shared;
13mod string;
14mod types;
15
16use nodedb_types::Value;
17
18/// Evaluate a scalar function call.
19pub fn eval_function(name: &str, args: &[Value]) -> Value {
20    if let Some(v) = string::try_eval(name, args) {
21        return v;
22    }
23    if let Some(v) = math::try_eval(name, args) {
24        return v;
25    }
26    if let Some(v) = conditional::try_eval(name, args) {
27        return v;
28    }
29    if let Some(v) = id::try_eval(name, args) {
30        return v;
31    }
32    if let Some(v) = datetime::try_eval(name, args) {
33        return v;
34    }
35    if let Some(v) = json::try_eval(name, args) {
36        return v;
37    }
38    if let Some(v) = types::try_eval(name, args) {
39        return v;
40    }
41    if let Some(v) = array::try_eval(name, args) {
42        return v;
43    }
44    // Geo / Spatial functions — delegated to geo_functions module.
45    crate::geo_functions::eval_geo_function(name, args).unwrap_or(Value::Null)
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use crate::expr::SqlExpr;
52
53    fn eval_fn(name: &str, args: Vec<Value>) -> Value {
54        eval_function(name, &args)
55    }
56
57    #[test]
58    fn upper() {
59        assert_eq!(
60            eval_fn("upper", vec![Value::String("hello".into())]),
61            Value::String("HELLO".into())
62        );
63    }
64
65    #[test]
66    fn upper_null_propagation() {
67        assert_eq!(eval_fn("upper", vec![Value::Null]), Value::Null);
68    }
69
70    #[test]
71    fn substring() {
72        assert_eq!(
73            eval_fn(
74                "substr",
75                vec![
76                    Value::String("hello".into()),
77                    Value::Integer(2),
78                    Value::Integer(3)
79                ]
80            ),
81            Value::String("ell".into())
82        );
83    }
84
85    #[test]
86    fn round_with_decimals() {
87        assert_eq!(
88            eval_fn("round", vec![Value::Float(3.15159), Value::Integer(2)]),
89            Value::Float(3.15)
90        );
91    }
92
93    #[test]
94    fn typeof_int() {
95        assert_eq!(
96            eval_fn("typeof", vec![Value::Integer(42)]),
97            Value::String("int".into())
98        );
99    }
100
101    #[test]
102    fn function_via_expr() {
103        let expr = SqlExpr::Function {
104            name: "upper".into(),
105            args: vec![SqlExpr::Column("name".into())],
106        };
107        let doc = Value::Object(
108            [("name".to_string(), Value::String("alice".into()))]
109                .into_iter()
110                .collect(),
111        );
112        assert_eq!(expr.eval(&doc), Value::String("ALICE".into()));
113    }
114
115    #[test]
116    fn geo_geohash_encode() {
117        let result = eval_fn(
118            "geo_geohash",
119            vec![
120                Value::Float(-73.9857),
121                Value::Float(40.758),
122                Value::Integer(6),
123            ],
124        );
125        let hash = result.as_str().unwrap();
126        assert_eq!(hash.len(), 6);
127        assert!(hash.starts_with("dr5ru"), "got {hash}");
128    }
129
130    #[test]
131    fn geo_geohash_decode() {
132        let hash = eval_fn(
133            "geo_geohash",
134            vec![Value::Float(0.0), Value::Float(0.0), Value::Integer(6)],
135        );
136        let result = eval_fn("geo_geohash_decode", vec![hash]);
137        assert!(!result.is_null());
138        assert!(result.get("min_lng").is_some());
139    }
140
141    #[test]
142    fn geo_geohash_neighbors_returns_8() {
143        let hash = eval_fn(
144            "geo_geohash",
145            vec![Value::Float(10.0), Value::Float(50.0), Value::Integer(6)],
146        );
147        let result = eval_fn("geo_geohash_neighbors", vec![hash]);
148        let arr = result.as_array().unwrap();
149        assert_eq!(arr.len(), 8);
150    }
151
152    fn point_value(lng: f64, lat: f64) -> Value {
153        let geom = nodedb_types::geometry::Geometry::point(lng, lat);
154        Value::Geometry(geom)
155    }
156
157    fn square_value() -> Value {
158        let geom = nodedb_types::geometry::Geometry::polygon(vec![vec![
159            [0.0, 0.0],
160            [10.0, 0.0],
161            [10.0, 10.0],
162            [0.0, 10.0],
163            [0.0, 0.0],
164        ]]);
165        Value::Geometry(geom)
166    }
167
168    #[test]
169    fn st_contains_sql() {
170        let result = eval_fn("st_contains", vec![square_value(), point_value(5.0, 5.0)]);
171        assert_eq!(result, Value::Bool(true));
172    }
173
174    #[test]
175    fn st_intersects_sql() {
176        let result = eval_fn("st_intersects", vec![square_value(), point_value(5.0, 0.0)]);
177        assert_eq!(result, Value::Bool(true));
178    }
179
180    #[test]
181    fn st_distance_sql() {
182        let result = eval_fn(
183            "st_distance",
184            vec![point_value(0.0, 0.0), point_value(0.0, 1.0)],
185        );
186        let d = result.as_f64().unwrap();
187        assert!((d - 111_195.0).abs() < 500.0, "got {d}");
188    }
189
190    #[test]
191    fn st_dwithin_sql() {
192        let result = eval_fn(
193            "st_dwithin",
194            vec![
195                point_value(0.0, 0.0),
196                point_value(0.001, 0.0),
197                Value::Float(200.0),
198            ],
199        );
200        assert_eq!(result, Value::Bool(true));
201    }
202
203    #[test]
204    fn st_buffer_sql() {
205        let result = eval_fn(
206            "st_buffer",
207            vec![
208                point_value(0.0, 0.0),
209                Value::Float(1000.0),
210                Value::Integer(8),
211            ],
212        );
213        // Result should be a Geometry (Polygon)
214        assert!(result.as_geometry().is_some());
215    }
216
217    #[test]
218    fn st_envelope_sql() {
219        let result = eval_fn("st_envelope", vec![square_value()]);
220        assert!(result.as_geometry().is_some());
221    }
222
223    #[test]
224    fn geo_length_sql() {
225        let line = Value::Geometry(nodedb_types::geometry::Geometry::line_string(vec![
226            [0.0, 0.0],
227            [0.0, 1.0],
228        ]));
229        let result = eval_fn("geo_length", vec![line]);
230        let d = result.as_f64().unwrap();
231        assert!((d - 111_195.0).abs() < 500.0, "got {d}");
232    }
233
234    #[test]
235    fn geo_x_y() {
236        assert_eq!(
237            eval_fn("geo_x", vec![point_value(5.0, 10.0)])
238                .as_f64()
239                .unwrap(),
240            5.0
241        );
242        assert_eq!(
243            eval_fn("geo_y", vec![point_value(5.0, 10.0)])
244                .as_f64()
245                .unwrap(),
246            10.0
247        );
248    }
249
250    #[test]
251    fn geo_type_sql() {
252        assert_eq!(
253            eval_fn("geo_type", vec![point_value(0.0, 0.0)]),
254            Value::String("Point".into())
255        );
256        assert_eq!(
257            eval_fn("geo_type", vec![square_value()]),
258            Value::String("Polygon".into())
259        );
260    }
261
262    #[test]
263    fn geo_num_points_sql() {
264        assert_eq!(
265            eval_fn("geo_num_points", vec![point_value(0.0, 0.0)]),
266            Value::Integer(1)
267        );
268        assert_eq!(
269            eval_fn("geo_num_points", vec![square_value()]),
270            Value::Integer(5)
271        );
272    }
273
274    #[test]
275    fn geo_is_valid_sql() {
276        assert_eq!(
277            eval_fn("geo_is_valid", vec![square_value()]),
278            Value::Bool(true)
279        );
280    }
281
282    #[test]
283    fn geo_as_wkt_sql() {
284        let result = eval_fn("geo_as_wkt", vec![point_value(5.0, 10.0)]);
285        assert_eq!(result, Value::String("POINT(5 10)".into()));
286    }
287
288    #[test]
289    fn geo_from_wkt_sql() {
290        let result = eval_fn("geo_from_wkt", vec![Value::String("POINT(5 10)".into())]);
291        assert!(result.as_geometry().is_some());
292    }
293
294    #[test]
295    fn geo_circle_sql() {
296        let result = eval_fn(
297            "geo_circle",
298            vec![
299                Value::Float(0.0),
300                Value::Float(0.0),
301                Value::Float(1000.0),
302                Value::Integer(16),
303            ],
304        );
305        assert!(result.as_geometry().is_some());
306    }
307
308    #[test]
309    fn geo_bbox_sql() {
310        let result = eval_fn(
311            "geo_bbox",
312            vec![
313                Value::Float(0.0),
314                Value::Float(0.0),
315                Value::Float(10.0),
316                Value::Float(10.0),
317            ],
318        );
319        assert!(result.as_geometry().is_some());
320    }
321}