Skip to main content

nodedb_query/functions/
mod.rs

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