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