1use nodedb_types::Value;
6
7use crate::expr::EvalError;
8
9use super::{
10 array, conditional, datetime, fts, id, json, math, string, system, text_chunk, types, vector,
11};
12
13pub fn eval_function(name: &str, args: &[Value]) -> Result<Value, EvalError> {
29 if let Some(v) = string::try_eval(name, args) {
30 return Ok(v);
31 }
32 if let Some(r) = math::try_eval(name, args) {
33 return r;
34 }
35 if let Some(v) = conditional::try_eval(name, args) {
36 return Ok(v);
37 }
38 if let Some(v) = id::try_eval(name, args) {
39 return Ok(v);
40 }
41 if let Some(v) = datetime::try_eval(name, args) {
42 return Ok(v);
43 }
44 if let Some(r) = json::try_eval(name, args) {
45 return r;
46 }
47 if let Some(v) = types::try_eval(name, args) {
48 return Ok(v);
49 }
50 if let Some(v) = array::try_eval(name, args) {
51 return Ok(v);
52 }
53 if let Some(v) = fts::try_eval_fts(name, args) {
54 return Ok(v);
55 }
56 if let Some(v) = system::try_eval(name, args) {
57 return Ok(v);
58 }
59 if let Some(r) = vector::try_eval(name, args) {
60 return r;
61 }
62 if let Some(v) = text_chunk::try_eval(name, args) {
63 return Ok(v);
64 }
65 crate::geo_functions::eval_geo_function(name, args).ok_or_else(|| EvalError::UnknownFunction {
67 name: name.to_owned(),
68 })
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use crate::expr::SqlExpr;
75
76 fn eval_fn(name: &str, args: Vec<Value>) -> Value {
77 eval_function(name, &args).unwrap()
78 }
79
80 #[test]
81 fn mod_by_zero_errors() {
82 let err = eval_function("mod", &[Value::Integer(5), Value::Integer(0)]).unwrap_err();
83 assert_eq!(err, EvalError::DivisionByZero);
84 }
85
86 #[test]
87 fn an_unknown_function_is_an_error_not_null() {
88 let err = eval_function("no_such_function", &[Value::Integer(1)]).unwrap_err();
89 assert_eq!(
90 err,
91 EvalError::UnknownFunction {
92 name: "no_such_function".into()
93 }
94 );
95 }
96
97 #[test]
100 fn registered_row_scalars_have_evaluators() {
101 let doc = Value::Object(
102 [(
103 "tags".to_string(),
104 Value::Array(vec![Value::String("a".into())]),
105 )]
106 .into_iter()
107 .collect(),
108 );
109 let path = Value::String("$.tags[0]".into());
110 let vector = Value::Array(vec![Value::Float(1.0), Value::Float(0.0)]);
111 let calls: [(&str, Vec<Value>); 8] = [
112 ("doc_get", vec![doc.clone(), path.clone()]),
113 ("doc_exists", vec![doc.clone(), path.clone()]),
114 (
115 "doc_array_contains",
116 vec![
117 doc.clone(),
118 Value::String("$.tags".into()),
119 Value::String("a".into()),
120 ],
121 ),
122 ("nav", vec![doc, path]),
123 ("vector_distance", vec![vector.clone(), vector.clone()]),
124 (
125 "vector_cosine_distance",
126 vec![vector.clone(), vector.clone()],
127 ),
128 ("vector_neg_inner_product", vec![vector.clone(), vector]),
129 (
130 "ndb_chunk_text",
131 vec![Value::String("abc".into()), Value::Integer(2)],
132 ),
133 ];
134 for (name, args) in calls {
135 let value = eval_function(name, &args)
136 .unwrap_or_else(|e| panic!("{name}() must evaluate, got {e}"));
137 assert_ne!(value, Value::Null, "{name}() gave NULL");
138 }
139 }
140
141 fn text(s: &str) -> Value {
142 Value::String(s.into())
143 }
144
145 #[test]
146 fn like_matches_percent_and_underscore() {
147 assert_eq!(
148 eval_fn("like", vec![text("alice"), text("a%")]),
149 Value::Bool(true)
150 );
151 assert_eq!(
152 eval_fn("like", vec![text("alice"), text("a_ice")]),
153 Value::Bool(true)
154 );
155 assert_eq!(
156 eval_fn("like", vec![text("alice"), text("b%")]),
157 Value::Bool(false)
158 );
159 assert_eq!(
160 eval_fn("like", vec![text("Alice"), text("a%")]),
161 Value::Bool(false)
162 );
163 }
164
165 #[test]
166 fn like_honours_the_escape_character() {
167 assert_eq!(
168 eval_fn("like", vec![text("50%"), text("50\\%")]),
169 Value::Bool(true)
170 );
171 assert_eq!(
172 eval_fn("like", vec![text("500"), text("50\\%")]),
173 Value::Bool(false)
174 );
175 assert_eq!(
176 eval_fn("like", vec![text("50%"), text("50!%"), text("!")]),
177 Value::Bool(true)
178 );
179 assert_eq!(
180 eval_fn("like", vec![text("a\\b"), text("a\\b"), text("")]),
181 Value::Bool(true)
182 );
183 }
184
185 #[test]
186 fn like_with_a_null_operand_is_null() {
187 assert_eq!(eval_fn("like", vec![Value::Null, text("a%")]), Value::Null);
188 assert_eq!(eval_fn("ilike", vec![text("a"), Value::Null]), Value::Null);
189 }
190
191 #[test]
192 fn ilike_folds_unicode_case() {
193 assert_eq!(
194 eval_fn("ilike", vec![text("ÉCOLE"), text("éc%")]),
195 Value::Bool(true)
196 );
197 assert_eq!(
198 eval_fn("ilike", vec![text("Straße"), text("STRA%")]),
199 Value::Bool(true)
200 );
201 assert_eq!(
202 eval_fn("like", vec![text("ÉCOLE"), text("éc%")]),
203 Value::Bool(false)
204 );
205 }
206
207 #[test]
208 fn not_like_negates_the_call() {
209 let expr = SqlExpr::Negate(Box::new(SqlExpr::Function {
210 name: "like".into(),
211 args: vec![SqlExpr::Literal(text("bob")), SqlExpr::Literal(text("a%"))],
212 }));
213 assert_eq!(expr.eval(&Value::Null).unwrap(), Value::Bool(true));
214 }
215
216 #[test]
217 fn make_array_keeps_every_evaluated_element() {
218 assert_eq!(
219 eval_fn(
220 "make_array",
221 vec![Value::Integer(5), Value::Null, text("x")]
222 ),
223 Value::Array(vec![Value::Integer(5), Value::Null, text("x")])
224 );
225 }
226
227 #[test]
228 fn upper() {
229 assert_eq!(
230 eval_fn("upper", vec![Value::String("hello".into())]),
231 Value::String("HELLO".into())
232 );
233 }
234
235 #[test]
236 fn upper_null_propagation() {
237 assert_eq!(eval_fn("upper", vec![Value::Null]), Value::Null);
238 }
239
240 #[test]
241 fn substring() {
242 assert_eq!(
243 eval_fn(
244 "substr",
245 vec![
246 Value::String("hello".into()),
247 Value::Integer(2),
248 Value::Integer(3)
249 ]
250 ),
251 Value::String("ell".into())
252 );
253 }
254
255 #[test]
256 fn round_with_decimals() {
257 assert_eq!(
258 eval_fn("round", vec![Value::Float(3.15159), Value::Integer(2)]),
259 Value::Float(3.15)
260 );
261 }
262
263 #[test]
264 fn typeof_int() {
265 assert_eq!(
266 eval_fn("typeof", vec![Value::Integer(42)]),
267 Value::String("int".into())
268 );
269 }
270
271 #[test]
272 fn function_via_expr() {
273 let expr = SqlExpr::Function {
274 name: "upper".into(),
275 args: vec![SqlExpr::Column("name".into())],
276 };
277 let doc = Value::Object(
278 [("name".to_string(), Value::String("alice".into()))]
279 .into_iter()
280 .collect(),
281 );
282 assert_eq!(expr.eval(&doc).unwrap(), Value::String("ALICE".into()));
283 }
284
285 #[test]
286 fn geo_geohash_encode() {
287 let result = eval_fn(
288 "geo_geohash",
289 vec![
290 Value::Float(-73.9857),
291 Value::Float(40.758),
292 Value::Integer(6),
293 ],
294 );
295 let hash = result.as_str().unwrap();
296 assert_eq!(hash.len(), 6);
297 assert!(hash.starts_with("dr5ru"), "got {hash}");
298 }
299
300 #[test]
301 fn geo_geohash_decode() {
302 let hash = eval_fn(
303 "geo_geohash",
304 vec![Value::Float(0.0), Value::Float(0.0), Value::Integer(6)],
305 );
306 let result = eval_fn("geo_geohash_decode", vec![hash]);
307 assert!(!result.is_null());
308 assert!(result.get("min_lng").is_some());
309 }
310
311 #[test]
312 fn geo_geohash_neighbors_returns_8() {
313 let hash = eval_fn(
314 "geo_geohash",
315 vec![Value::Float(10.0), Value::Float(50.0), Value::Integer(6)],
316 );
317 let result = eval_fn("geo_geohash_neighbors", vec![hash]);
318 let arr = result.as_array().unwrap();
319 assert_eq!(arr.len(), 8);
320 }
321
322 fn point_value(lng: f64, lat: f64) -> Value {
323 let geom = nodedb_types::geometry::Geometry::point(lng, lat);
324 Value::Geometry(geom)
325 }
326
327 fn square_value() -> Value {
328 let geom = nodedb_types::geometry::Geometry::polygon(vec![vec![
329 [0.0, 0.0],
330 [10.0, 0.0],
331 [10.0, 10.0],
332 [0.0, 10.0],
333 [0.0, 0.0],
334 ]]);
335 Value::Geometry(geom)
336 }
337
338 #[test]
339 fn st_contains_sql() {
340 let result = eval_fn("st_contains", vec![square_value(), point_value(5.0, 5.0)]);
341 assert_eq!(result, Value::Bool(true));
342 }
343
344 #[test]
345 fn st_intersects_sql() {
346 let result = eval_fn("st_intersects", vec![square_value(), point_value(5.0, 0.0)]);
347 assert_eq!(result, Value::Bool(true));
348 }
349
350 #[test]
351 fn st_distance_sql() {
352 let result = eval_fn(
353 "st_distance",
354 vec![point_value(0.0, 0.0), point_value(0.0, 1.0)],
355 );
356 let d = result.as_f64().unwrap();
357 assert!((d - 111_195.0).abs() < 500.0, "got {d}");
358 }
359
360 #[test]
361 fn st_dwithin_sql() {
362 let result = eval_fn(
363 "st_dwithin",
364 vec![
365 point_value(0.0, 0.0),
366 point_value(0.001, 0.0),
367 Value::Float(200.0),
368 ],
369 );
370 assert_eq!(result, Value::Bool(true));
371 }
372
373 #[test]
374 fn st_buffer_sql() {
375 let result = eval_fn(
376 "st_buffer",
377 vec![
378 point_value(0.0, 0.0),
379 Value::Float(1000.0),
380 Value::Integer(8),
381 ],
382 );
383 assert!(result.as_geometry().is_some());
385 }
386
387 #[test]
388 fn st_envelope_sql() {
389 let result = eval_fn("st_envelope", vec![square_value()]);
390 assert!(result.as_geometry().is_some());
391 }
392
393 #[test]
394 fn geo_length_sql() {
395 let line = Value::Geometry(nodedb_types::geometry::Geometry::line_string(vec![
396 [0.0, 0.0],
397 [0.0, 1.0],
398 ]));
399 let result = eval_fn("geo_length", vec![line]);
400 let d = result.as_f64().unwrap();
401 assert!((d - 111_195.0).abs() < 500.0, "got {d}");
402 }
403
404 #[test]
405 fn geo_x_y() {
406 assert_eq!(
407 eval_fn("geo_x", vec![point_value(5.0, 10.0)])
408 .as_f64()
409 .unwrap(),
410 5.0
411 );
412 assert_eq!(
413 eval_fn("geo_y", vec![point_value(5.0, 10.0)])
414 .as_f64()
415 .unwrap(),
416 10.0
417 );
418 }
419
420 #[test]
421 fn geo_type_sql() {
422 assert_eq!(
423 eval_fn("geo_type", vec![point_value(0.0, 0.0)]),
424 Value::String("Point".into())
425 );
426 assert_eq!(
427 eval_fn("geo_type", vec![square_value()]),
428 Value::String("Polygon".into())
429 );
430 }
431
432 #[test]
433 fn geo_num_points_sql() {
434 assert_eq!(
435 eval_fn("geo_num_points", vec![point_value(0.0, 0.0)]),
436 Value::Integer(1)
437 );
438 assert_eq!(
439 eval_fn("geo_num_points", vec![square_value()]),
440 Value::Integer(5)
441 );
442 }
443
444 #[test]
445 fn geo_is_valid_sql() {
446 assert_eq!(
447 eval_fn("geo_is_valid", vec![square_value()]),
448 Value::Bool(true)
449 );
450 }
451
452 #[test]
453 fn geo_as_wkt_sql() {
454 let result = eval_fn("geo_as_wkt", vec![point_value(5.0, 10.0)]);
455 assert_eq!(result, Value::String("POINT(5 10)".into()));
456 }
457
458 #[test]
459 fn geo_from_wkt_sql() {
460 let result = eval_fn("geo_from_wkt", vec![Value::String("POINT(5 10)".into())]);
461 assert!(result.as_geometry().is_some());
462 }
463
464 #[test]
465 fn geo_circle_sql() {
466 let result = eval_fn(
467 "geo_circle",
468 vec![
469 Value::Float(0.0),
470 Value::Float(0.0),
471 Value::Float(1000.0),
472 Value::Integer(16),
473 ],
474 );
475 assert!(result.as_geometry().is_some());
476 }
477
478 #[test]
479 fn geo_bbox_sql() {
480 let result = eval_fn(
481 "geo_bbox",
482 vec![
483 Value::Float(0.0),
484 Value::Float(0.0),
485 Value::Float(10.0),
486 Value::Float(10.0),
487 ],
488 );
489 assert!(result.as_geometry().is_some());
490 }
491
492 #[test]
493 fn version_returns_postgres_compatible_string() {
494 match eval_fn("version", vec![]) {
495 Value::String(s) => assert!(s.contains("PostgreSQL")),
496 other => panic!("expected Value::String, got {other:?}"),
497 }
498 }
499}