1use std::collections::BTreeMap;
24use std::sync::{LazyLock, OnceLock};
25
26use smol_str::SmolStr;
27
28use crate::{
29 DynamicReturnFn, EmptySchema, EndpointDecl, FnCategories, FunctionSignature, ParamDecl,
30 ProcedureSignature, PropertyDecl, PropertyType, ReturnTy, SchemaProvider,
31};
32
33struct BuiltIn {
42 name: &'static str,
43 params: Vec<(&'static str, PropertyType)>,
44 variadic: Option<PropertyType>,
45 return_ty: BuiltInReturn,
46 categories: FnCategories,
47}
48
49enum BuiltInReturn {
50 Constant(PropertyType),
51 Dynamic(fn() -> DynamicReturnFn),
53}
54
55impl BuiltIn {
56 fn to_signature(&self) -> FunctionSignature {
57 let params: Vec<ParamDecl> = self
58 .params
59 .iter()
60 .map(|(n, t)| ParamDecl {
61 name: SmolStr::new(n),
62 ty: t.clone(),
63 default: None,
64 })
65 .collect();
66 let variadic = self.variadic.as_ref().map(|t| ParamDecl {
67 name: SmolStr::new("args"),
68 ty: t.clone(),
69 default: None,
70 });
71 let return_ty = match &self.return_ty {
72 BuiltInReturn::Constant(t) => ReturnTy::Constant(t.clone()),
73 BuiltInReturn::Dynamic(f) => ReturnTy::Dynamic(f()),
74 };
75 FunctionSignature {
76 name: SmolStr::new(self.name),
77 params,
78 variadic,
79 return_ty,
80 categories: self.categories,
81 }
82 }
83}
84
85const fn pure() -> FnCategories {
90 FnCategories {
91 pure: true,
92 aggregate: false,
93 deterministic: true,
94 }
95}
96
97const fn agg() -> FnCategories {
98 FnCategories {
99 pure: true,
100 aggregate: true,
101 deterministic: true,
102 }
103}
104
105const fn nondet() -> FnCategories {
106 FnCategories {
107 pure: true,
108 aggregate: false,
109 deterministic: false,
110 }
111}
112
113static CATALOG: LazyLock<Vec<BuiltIn>> = LazyLock::new(|| {
119 vec![
120 BuiltIn {
122 name: "id",
123 params: vec![("x", PropertyType::Any)],
124 variadic: None,
125 return_ty: BuiltInReturn::Constant(PropertyType::Int),
126 categories: pure(),
127 },
128 BuiltIn {
129 name: "type",
130 params: vec![("r", PropertyType::Any)],
131 variadic: None,
132 return_ty: BuiltInReturn::Constant(PropertyType::String),
133 categories: pure(),
134 },
135 BuiltIn {
136 name: "labels",
137 params: vec![("n", PropertyType::Any)],
138 variadic: None,
139 return_ty: BuiltInReturn::Constant(PropertyType::List(Box::new(PropertyType::String))),
140 categories: pure(),
141 },
142 BuiltIn {
143 name: "keys",
150 params: vec![("x", PropertyType::Any)],
151 variadic: None,
152 return_ty: BuiltInReturn::Constant(PropertyType::List(Box::new(PropertyType::String))),
153 categories: pure(),
154 },
155 BuiltIn {
156 name: "values",
160 params: vec![("map", PropertyType::Any)],
161 variadic: None,
162 return_ty: BuiltInReturn::Constant(PropertyType::List(Box::new(PropertyType::Any))),
163 categories: pure(),
164 },
165 BuiltIn {
166 name: "properties",
167 params: vec![("x", PropertyType::Any)],
168 variadic: None,
169 return_ty: BuiltInReturn::Constant(PropertyType::Any),
170 categories: pure(),
171 },
172 BuiltIn {
174 name: "length",
175 params: vec![("x", PropertyType::Any)],
176 variadic: None,
177 return_ty: BuiltInReturn::Constant(PropertyType::Int),
178 categories: pure(),
179 },
180 BuiltIn {
181 name: "size",
182 params: vec![("x", PropertyType::Any)],
183 variadic: None,
184 return_ty: BuiltInReturn::Constant(PropertyType::Int),
185 categories: pure(),
186 },
187 BuiltIn {
189 name: "coalesce",
190 params: vec![],
191 variadic: Some(PropertyType::Any),
192 return_ty: BuiltInReturn::Dynamic(|| {
193 Box::new(|tys: &[PropertyType]| {
198 tys.iter()
199 .find(|t| !matches!(t, PropertyType::Any))
200 .cloned()
201 .unwrap_or(PropertyType::Any)
202 })
203 }),
204 categories: pure(),
205 },
206 BuiltIn {
208 name: "toupper",
209 params: vec![("s", PropertyType::String)],
210 variadic: None,
211 return_ty: BuiltInReturn::Constant(PropertyType::String),
212 categories: pure(),
213 },
214 BuiltIn {
215 name: "tolower",
216 params: vec![("s", PropertyType::String)],
217 variadic: None,
218 return_ty: BuiltInReturn::Constant(PropertyType::String),
219 categories: pure(),
220 },
221 BuiltIn {
222 name: "trim",
223 params: vec![("s", PropertyType::String)],
224 variadic: None,
225 return_ty: BuiltInReturn::Constant(PropertyType::String),
226 categories: pure(),
227 },
228 BuiltIn {
229 name: "ltrim",
230 params: vec![("s", PropertyType::String)],
231 variadic: None,
232 return_ty: BuiltInReturn::Constant(PropertyType::String),
233 categories: pure(),
234 },
235 BuiltIn {
236 name: "rtrim",
237 params: vec![("s", PropertyType::String)],
238 variadic: None,
239 return_ty: BuiltInReturn::Constant(PropertyType::String),
240 categories: pure(),
241 },
242 BuiltIn {
243 name: "reverse",
244 params: vec![("x", PropertyType::Any)],
245 variadic: None,
246 return_ty: BuiltInReturn::Dynamic(|| {
247 Box::new(|tys: &[PropertyType]| match tys.first() {
250 Some(t @ (PropertyType::String | PropertyType::List(_))) => t.clone(),
251 _ => PropertyType::Any,
252 })
253 }),
254 categories: pure(),
255 },
256 BuiltIn {
257 name: "substring",
258 params: vec![
259 ("original", PropertyType::String),
260 ("start", PropertyType::Int),
261 ],
262 variadic: Some(PropertyType::Int),
263 return_ty: BuiltInReturn::Constant(PropertyType::String),
264 categories: pure(),
265 },
266 BuiltIn {
267 name: "replace",
268 params: vec![
269 ("original", PropertyType::String),
270 ("search", PropertyType::String),
271 ("replace", PropertyType::String),
272 ],
273 variadic: None,
274 return_ty: BuiltInReturn::Constant(PropertyType::String),
275 categories: pure(),
276 },
277 BuiltIn {
278 name: "split",
279 params: vec![
280 ("original", PropertyType::String),
281 ("delim", PropertyType::String),
282 ],
283 variadic: None,
284 return_ty: BuiltInReturn::Constant(PropertyType::List(Box::new(PropertyType::String))),
285 categories: pure(),
286 },
287 BuiltIn {
288 name: "left",
289 params: vec![
290 ("original", PropertyType::String),
291 ("length", PropertyType::Int),
292 ],
293 variadic: None,
294 return_ty: BuiltInReturn::Constant(PropertyType::String),
295 categories: pure(),
296 },
297 BuiltIn {
298 name: "right",
299 params: vec![
300 ("original", PropertyType::String),
301 ("length", PropertyType::Int),
302 ],
303 variadic: None,
304 return_ty: BuiltInReturn::Constant(PropertyType::String),
305 categories: pure(),
306 },
307 BuiltIn {
308 name: "tostring",
309 params: vec![("x", PropertyType::Any)],
310 variadic: None,
311 return_ty: BuiltInReturn::Constant(PropertyType::String),
312 categories: pure(),
313 },
314 BuiltIn {
315 name: "tointeger",
316 params: vec![("x", PropertyType::Any)],
317 variadic: None,
318 return_ty: BuiltInReturn::Constant(PropertyType::Int),
319 categories: pure(),
320 },
321 BuiltIn {
322 name: "tofloat",
323 params: vec![("x", PropertyType::Any)],
324 variadic: None,
325 return_ty: BuiltInReturn::Constant(PropertyType::Float),
326 categories: pure(),
327 },
328 BuiltIn {
329 name: "toboolean",
330 params: vec![("x", PropertyType::Any)],
331 variadic: None,
332 return_ty: BuiltInReturn::Constant(PropertyType::Bool),
333 categories: pure(),
334 },
335 BuiltIn {
337 name: "head",
338 params: vec![("list", PropertyType::List(Box::new(PropertyType::Any)))],
339 variadic: None,
340 return_ty: BuiltInReturn::Dynamic(|| {
341 Box::new(|tys: &[PropertyType]| match tys.first() {
342 Some(PropertyType::List(inner)) => (**inner).clone(),
343 _ => PropertyType::Any,
344 })
345 }),
346 categories: pure(),
347 },
348 BuiltIn {
349 name: "last",
350 params: vec![("list", PropertyType::List(Box::new(PropertyType::Any)))],
351 variadic: None,
352 return_ty: BuiltInReturn::Dynamic(|| {
353 Box::new(|tys: &[PropertyType]| match tys.first() {
354 Some(PropertyType::List(inner)) => (**inner).clone(),
355 _ => PropertyType::Any,
356 })
357 }),
358 categories: pure(),
359 },
360 BuiltIn {
361 name: "tail",
362 params: vec![("list", PropertyType::List(Box::new(PropertyType::Any)))],
363 variadic: None,
364 return_ty: BuiltInReturn::Dynamic(|| {
365 Box::new(|tys: &[PropertyType]| match tys.first() {
366 Some(t @ PropertyType::List(_)) => t.clone(),
367 _ => PropertyType::Any,
368 })
369 }),
370 categories: pure(),
371 },
372 BuiltIn {
373 name: "range",
374 params: vec![("start", PropertyType::Int), ("end", PropertyType::Int)],
375 variadic: Some(PropertyType::Int),
376 return_ty: BuiltInReturn::Constant(PropertyType::List(Box::new(PropertyType::Int))),
377 categories: pure(),
378 },
379 BuiltIn {
380 name: "nodes",
381 params: vec![("path", PropertyType::Any)],
382 variadic: None,
383 return_ty: BuiltInReturn::Constant(PropertyType::List(Box::new(PropertyType::Any))),
384 categories: pure(),
385 },
386 BuiltIn {
387 name: "relationships",
388 params: vec![("path", PropertyType::Any)],
389 variadic: None,
390 return_ty: BuiltInReturn::Constant(PropertyType::List(Box::new(PropertyType::Any))),
391 categories: pure(),
392 },
393 BuiltIn {
395 name: "abs",
396 params: vec![("x", PropertyType::Any)],
397 variadic: None,
398 return_ty: BuiltInReturn::Dynamic(|| {
399 Box::new(|tys: &[PropertyType]| match tys.first() {
400 Some(PropertyType::Int) => PropertyType::Int,
401 Some(PropertyType::Float) => PropertyType::Float,
402 _ => PropertyType::Any,
403 })
404 }),
405 categories: pure(),
406 },
407 BuiltIn {
408 name: "sign",
409 params: vec![("x", PropertyType::Any)],
410 variadic: None,
411 return_ty: BuiltInReturn::Constant(PropertyType::Int),
412 categories: pure(),
413 },
414 BuiltIn {
415 name: "ceil",
416 params: vec![("x", PropertyType::Float)],
417 variadic: None,
418 return_ty: BuiltInReturn::Constant(PropertyType::Float),
419 categories: pure(),
420 },
421 BuiltIn {
422 name: "floor",
423 params: vec![("x", PropertyType::Float)],
424 variadic: None,
425 return_ty: BuiltInReturn::Constant(PropertyType::Float),
426 categories: pure(),
427 },
428 BuiltIn {
429 name: "round",
430 params: vec![("x", PropertyType::Float)],
431 variadic: None,
432 return_ty: BuiltInReturn::Constant(PropertyType::Float),
433 categories: pure(),
434 },
435 BuiltIn {
436 name: "sqrt",
437 params: vec![("x", PropertyType::Float)],
438 variadic: None,
439 return_ty: BuiltInReturn::Constant(PropertyType::Float),
440 categories: pure(),
441 },
442 BuiltIn {
443 name: "exp",
444 params: vec![("x", PropertyType::Float)],
445 variadic: None,
446 return_ty: BuiltInReturn::Constant(PropertyType::Float),
447 categories: pure(),
448 },
449 BuiltIn {
450 name: "log",
451 params: vec![("x", PropertyType::Float)],
452 variadic: None,
453 return_ty: BuiltInReturn::Constant(PropertyType::Float),
454 categories: pure(),
455 },
456 BuiltIn {
457 name: "log10",
458 params: vec![("x", PropertyType::Float)],
459 variadic: None,
460 return_ty: BuiltInReturn::Constant(PropertyType::Float),
461 categories: pure(),
462 },
463 BuiltIn {
464 name: "sin",
465 params: vec![("x", PropertyType::Float)],
466 variadic: None,
467 return_ty: BuiltInReturn::Constant(PropertyType::Float),
468 categories: pure(),
469 },
470 BuiltIn {
471 name: "cos",
472 params: vec![("x", PropertyType::Float)],
473 variadic: None,
474 return_ty: BuiltInReturn::Constant(PropertyType::Float),
475 categories: pure(),
476 },
477 BuiltIn {
478 name: "tan",
479 params: vec![("x", PropertyType::Float)],
480 variadic: None,
481 return_ty: BuiltInReturn::Constant(PropertyType::Float),
482 categories: pure(),
483 },
484 BuiltIn {
485 name: "pi",
486 params: vec![],
487 variadic: None,
488 return_ty: BuiltInReturn::Constant(PropertyType::Float),
489 categories: pure(),
490 },
491 BuiltIn {
492 name: "e",
493 params: vec![],
494 variadic: None,
495 return_ty: BuiltInReturn::Constant(PropertyType::Float),
496 categories: pure(),
497 },
498 BuiltIn {
499 name: "rand",
500 params: vec![],
501 variadic: None,
502 return_ty: BuiltInReturn::Constant(PropertyType::Float),
503 categories: nondet(),
504 },
505 BuiltIn {
507 name: "count",
508 params: vec![("x", PropertyType::Any)],
509 variadic: None,
510 return_ty: BuiltInReturn::Constant(PropertyType::Int),
511 categories: agg(),
512 },
513 BuiltIn {
514 name: "sum",
515 params: vec![("x", PropertyType::Any)],
516 variadic: None,
517 return_ty: BuiltInReturn::Dynamic(|| {
518 Box::new(|tys: &[PropertyType]| match tys.first() {
519 Some(PropertyType::Int) => PropertyType::Int,
520 Some(PropertyType::Float) => PropertyType::Float,
521 _ => PropertyType::Any,
522 })
523 }),
524 categories: agg(),
525 },
526 BuiltIn {
527 name: "avg",
528 params: vec![("x", PropertyType::Any)],
529 variadic: None,
530 return_ty: BuiltInReturn::Constant(PropertyType::Float),
531 categories: agg(),
532 },
533 BuiltIn {
534 name: "min",
535 params: vec![("x", PropertyType::Any)],
536 variadic: None,
537 return_ty: BuiltInReturn::Dynamic(|| {
538 Box::new(|tys: &[PropertyType]| tys.first().cloned().unwrap_or(PropertyType::Any))
539 }),
540 categories: agg(),
541 },
542 BuiltIn {
543 name: "max",
544 params: vec![("x", PropertyType::Any)],
545 variadic: None,
546 return_ty: BuiltInReturn::Dynamic(|| {
547 Box::new(|tys: &[PropertyType]| tys.first().cloned().unwrap_or(PropertyType::Any))
548 }),
549 categories: agg(),
550 },
551 BuiltIn {
552 name: "collect",
553 params: vec![("x", PropertyType::Any)],
554 variadic: None,
555 return_ty: BuiltInReturn::Dynamic(|| {
556 Box::new(|tys: &[PropertyType]| match tys.first() {
557 Some(t) => PropertyType::List(Box::new(t.clone())),
558 None => PropertyType::List(Box::new(PropertyType::Any)),
559 })
560 }),
561 categories: agg(),
562 },
563 BuiltIn {
564 name: "stdev",
565 params: vec![("x", PropertyType::Any)],
566 variadic: None,
567 return_ty: BuiltInReturn::Constant(PropertyType::Float),
568 categories: agg(),
569 },
570 BuiltIn {
571 name: "stdevp",
572 params: vec![("x", PropertyType::Any)],
573 variadic: None,
574 return_ty: BuiltInReturn::Constant(PropertyType::Float),
575 categories: agg(),
576 },
577 BuiltIn {
578 name: "percentilecont",
579 params: vec![
580 ("x", PropertyType::Any),
581 ("percentile", PropertyType::Float),
582 ],
583 variadic: None,
584 return_ty: BuiltInReturn::Constant(PropertyType::Float),
585 categories: agg(),
586 },
587 BuiltIn {
588 name: "percentiledisc",
589 params: vec![
590 ("x", PropertyType::Any),
591 ("percentile", PropertyType::Float),
592 ],
593 variadic: None,
594 return_ty: BuiltInReturn::Dynamic(|| {
595 Box::new(|tys: &[PropertyType]| tys.first().cloned().unwrap_or(PropertyType::Any))
596 }),
597 categories: agg(),
598 },
599 ]
600});
601
602fn find_builtin(name: &str) -> Option<&'static BuiltIn> {
606 static INDEX: OnceLock<BTreeMap<&'static str, usize>> = OnceLock::new();
607 let index = INDEX.get_or_init(|| {
608 CATALOG
609 .iter()
610 .enumerate()
611 .map(|(i, b)| (b.name, i))
612 .collect()
613 });
614 if name.bytes().all(|b| !b.is_ascii_uppercase()) {
616 return index.get(name).map(|&i| &CATALOG[i]);
617 }
618 let lower = name.to_ascii_lowercase();
619 index.get(lower.as_str()).map(|&i| &CATALOG[i])
620}
621
622pub struct StandardLibrary<S: SchemaProvider = EmptySchema> {
642 inner: S,
643}
644
645impl<S: SchemaProvider + core::fmt::Debug> core::fmt::Debug for StandardLibrary<S> {
646 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
647 f.debug_struct("StandardLibrary")
648 .field("builtin_count", &CATALOG.len())
649 .field("inner", &self.inner)
650 .finish()
651 }
652}
653
654impl StandardLibrary<EmptySchema> {
655 pub fn new() -> Self {
657 Self { inner: EmptySchema }
658 }
659}
660
661impl Default for StandardLibrary<EmptySchema> {
662 fn default() -> Self {
663 Self::new()
664 }
665}
666
667impl<S: SchemaProvider> StandardLibrary<S> {
668 pub fn wrap(inner: S) -> Self {
673 Self { inner }
674 }
675
676 pub fn inner(&self) -> &S {
679 &self.inner
680 }
681
682 pub fn builtin_names() -> Vec<&'static str> {
685 let mut v: Vec<&'static str> = CATALOG.iter().map(|b| b.name).collect();
686 v.sort_unstable();
687 v
688 }
689
690 pub fn builtin_count() -> usize {
692 CATALOG.len()
693 }
694}
695
696impl<S: SchemaProvider> SchemaProvider for StandardLibrary<S> {
697 fn labels(&self) -> Vec<SmolStr> {
698 self.inner.labels()
699 }
700
701 fn relationship_types(&self) -> Vec<SmolStr> {
702 self.inner.relationship_types()
703 }
704
705 fn has_label(&self, name: &str) -> bool {
706 self.inner.has_label(name)
707 }
708
709 fn has_relationship_type(&self, name: &str) -> bool {
710 self.inner.has_relationship_type(name)
711 }
712
713 fn node_properties(&self, label: &str) -> Option<Vec<PropertyDecl>> {
714 self.inner.node_properties(label)
715 }
716
717 fn relationship_properties(&self, rel_type: &str) -> Option<Vec<PropertyDecl>> {
718 self.inner.relationship_properties(rel_type)
719 }
720
721 fn relationship_endpoints(&self, rel_type: &str) -> Vec<EndpointDecl> {
722 self.inner.relationship_endpoints(rel_type)
723 }
724
725 fn inverse_of(&self, rel_type: &str) -> Option<SmolStr> {
726 self.inner.inverse_of(rel_type)
727 }
728
729 fn function(&self, name: &str) -> Option<FunctionSignature> {
730 if let Some(b) = find_builtin(name) {
731 return Some(b.to_signature());
732 }
733 self.inner.function(name)
734 }
735
736 fn procedure(&self, name: &str) -> Option<ProcedureSignature> {
737 self.inner.procedure(name)
740 }
741
742 fn schema_digest(&self) -> [u8; 32] {
743 self.inner.schema_digest()
747 }
748}
749
750#[cfg(test)]
755mod tests {
756 use super::*;
757
758 #[test]
759 fn catalog_has_no_duplicate_names() {
760 use std::collections::HashSet;
761 let mut seen = HashSet::new();
762 for b in CATALOG.iter() {
763 assert!(seen.insert(b.name), "duplicate built-in: {}", b.name);
764 }
765 }
766
767 #[test]
768 fn catalog_names_are_lowercase_ascii() {
769 for b in CATALOG.iter() {
770 assert!(
771 b.name
772 .bytes()
773 .all(|c: u8| c.is_ascii() && !c.is_ascii_uppercase()),
774 "catalog name not lowercase-ascii: {}",
775 b.name
776 );
777 }
778 }
779
780 #[test]
781 fn new_resolves_core_functions() {
782 let s = StandardLibrary::new();
783 for name in [
784 "id",
785 "type",
786 "labels",
787 "keys",
788 "values",
789 "properties",
790 "length",
791 "size",
792 "coalesce",
793 ] {
794 assert!(s.function(name).is_some(), "missing: {name}");
795 }
796 }
797
798 #[test]
801 fn values_returns_list_of_any() {
802 let s = StandardLibrary::new();
803 let sig = s.function("values").expect("values is registered");
804 assert_eq!(sig.name, SmolStr::new("values"));
805 match sig.return_ty {
806 ReturnTy::Constant(PropertyType::List(inner)) => {
807 assert_eq!(*inner, PropertyType::Any);
808 }
809 other => panic!("expected Constant(List(Any)), got {other:?}"),
810 }
811 }
812
813 #[test]
814 fn new_resolves_function_families() {
815 let s = StandardLibrary::new();
816 for n in ["toUpper", "toLower", "substring", "replace", "split"] {
818 assert!(s.function(n).is_some(), "string: {n}");
819 }
820 for n in ["head", "last", "tail", "range", "nodes", "relationships"] {
822 assert!(s.function(n).is_some(), "collection: {n}");
823 }
824 for n in ["abs", "ceil", "floor", "sqrt", "sin", "pi", "rand"] {
826 assert!(s.function(n).is_some(), "math: {n}");
827 }
828 for n in ["count", "sum", "avg", "min", "max", "collect"] {
830 assert!(s.function(n).is_some(), "agg: {n}");
831 }
832 }
833
834 #[test]
835 fn lookup_is_case_insensitive() {
836 let s = StandardLibrary::new();
837 assert!(s.function("COUNT").is_some());
838 assert!(s.function("Count").is_some());
839 assert!(s.function("count").is_some());
840 assert!(s.function("cOuNt").is_some());
841 }
842
843 #[test]
844 fn unknown_function_returns_none() {
845 let s = StandardLibrary::new();
846 assert!(s.function("fribble").is_none());
847 }
848
849 #[test]
850 fn aggregation_flag_set_on_aggs() {
851 let s = StandardLibrary::new();
852 let c = s.function("count").unwrap();
853 assert!(c.categories.aggregate);
854 let a = s.function("abs").unwrap();
855 assert!(!a.categories.aggregate);
856 }
857
858 #[test]
859 fn rand_is_non_deterministic() {
860 let s = StandardLibrary::new();
861 let r = s.function("rand").unwrap();
862 assert!(!r.categories.deterministic);
863 }
864
865 #[test]
866 fn dynamic_return_closure_is_fresh_per_lookup() {
867 let s = StandardLibrary::new();
870 let s1 = s.function("coalesce").unwrap();
871 let s2 = s.function("coalesce").unwrap();
872 let probe = |sig: FunctionSignature| match sig.return_ty {
873 ReturnTy::Dynamic(f) => f(&[PropertyType::String]),
874 ReturnTy::Constant(_) => panic!("expected Dynamic"),
875 };
876 assert_eq!(probe(s1), PropertyType::String);
877 assert_eq!(probe(s2), PropertyType::String);
878 }
879
880 #[test]
881 fn coalesce_dynamic_picks_first_non_any() {
882 let s = StandardLibrary::new();
883 let sig = s.function("coalesce").unwrap();
884 match sig.return_ty {
885 ReturnTy::Dynamic(f) => {
886 assert_eq!(
887 f(&[PropertyType::Any, PropertyType::Int, PropertyType::String]),
888 PropertyType::Int
889 );
890 assert_eq!(f(&[]), PropertyType::Any);
891 assert_eq!(f(&[PropertyType::Any]), PropertyType::Any);
892 }
893 ReturnTy::Constant(_) => panic!("coalesce should be Dynamic"),
894 }
895 }
896
897 #[test]
898 fn abs_dynamic_preserves_int_or_float() {
899 let s = StandardLibrary::new();
900 let sig = s.function("abs").unwrap();
901 match sig.return_ty {
902 ReturnTy::Dynamic(f) => {
903 assert_eq!(f(&[PropertyType::Int]), PropertyType::Int);
904 assert_eq!(f(&[PropertyType::Float]), PropertyType::Float);
905 assert_eq!(f(&[PropertyType::String]), PropertyType::Any);
906 }
907 ReturnTy::Constant(_) => panic!("abs should be Dynamic"),
908 }
909 }
910
911 #[test]
912 fn head_dynamic_unwraps_list() {
913 let s = StandardLibrary::new();
914 let sig = s.function("head").unwrap();
915 match sig.return_ty {
916 ReturnTy::Dynamic(f) => {
917 assert_eq!(
918 f(&[PropertyType::List(Box::new(PropertyType::Int))]),
919 PropertyType::Int
920 );
921 assert_eq!(f(&[PropertyType::Int]), PropertyType::Any);
922 }
923 ReturnTy::Constant(_) => panic!("head should be Dynamic"),
924 }
925 }
926
927 #[test]
928 fn collect_dynamic_wraps_in_list() {
929 let s = StandardLibrary::new();
930 let sig = s.function("collect").unwrap();
931 match sig.return_ty {
932 ReturnTy::Dynamic(f) => {
933 assert_eq!(
934 f(&[PropertyType::Int]),
935 PropertyType::List(Box::new(PropertyType::Int))
936 );
937 }
938 ReturnTy::Constant(_) => panic!("collect should be Dynamic"),
939 }
940 }
941
942 #[test]
943 fn builtin_names_is_sorted_and_complete() {
944 let names = StandardLibrary::<EmptySchema>::builtin_names();
945 assert_eq!(names.len(), StandardLibrary::<EmptySchema>::builtin_count());
946 assert_eq!(names.len(), CATALOG.len());
947 let mut sorted = names.clone();
948 sorted.sort_unstable();
949 assert_eq!(names, sorted);
950 }
951
952 struct FakeSchema;
955 impl SchemaProvider for FakeSchema {
956 fn labels(&self) -> Vec<SmolStr> {
957 vec![SmolStr::new("Person")]
958 }
959 fn relationship_types(&self) -> Vec<SmolStr> {
960 vec![SmolStr::new("KNOWS")]
961 }
962 fn node_properties(&self, label: &str) -> Option<Vec<PropertyDecl>> {
963 if label == "Person" {
964 Some(vec![PropertyDecl {
965 name: SmolStr::new("name"),
966 ty: PropertyType::String,
967 required: true,
968 }])
969 } else {
970 None
971 }
972 }
973 fn relationship_properties(&self, _: &str) -> Option<Vec<PropertyDecl>> {
974 None
975 }
976 fn relationship_endpoints(&self, rel: &str) -> Vec<EndpointDecl> {
977 if rel == "KNOWS" {
978 vec![EndpointDecl {
979 from: SmolStr::new("Person"),
980 to: SmolStr::new("Person"),
981 cardinality: crate::Cardinality::ManyToMany,
982 }]
983 } else {
984 Vec::new()
985 }
986 }
987 fn inverse_of(&self, _: &str) -> Option<SmolStr> {
988 None
989 }
990 fn function(&self, name: &str) -> Option<FunctionSignature> {
991 if name == "custom_fn" {
992 Some(FunctionSignature {
993 name: SmolStr::new("custom_fn"),
994 params: vec![],
995 variadic: None,
996 return_ty: ReturnTy::Constant(PropertyType::Bool),
997 categories: pure(),
998 })
999 } else if name == "count" {
1000 Some(FunctionSignature {
1002 name: SmolStr::new("count"),
1003 params: vec![],
1004 variadic: None,
1005 return_ty: ReturnTy::Constant(PropertyType::Bool),
1006 categories: pure(),
1007 })
1008 } else {
1009 None
1010 }
1011 }
1012 fn procedure(&self, _: &str) -> Option<ProcedureSignature> {
1013 None
1014 }
1015 fn schema_digest(&self) -> [u8; 32] {
1016 [1u8; 32]
1017 }
1018 }
1019
1020 #[test]
1021 fn wrap_delegates_non_function_surface() {
1022 let s = StandardLibrary::wrap(FakeSchema);
1023 assert_eq!(s.labels(), vec![SmolStr::new("Person")]);
1024 assert!(s.has_label("Person"));
1025 assert_eq!(s.relationship_types(), vec![SmolStr::new("KNOWS")]);
1026 assert!(s.has_relationship_type("KNOWS"));
1027 assert_eq!(
1028 s.node_properties("Person").unwrap()[0].name,
1029 SmolStr::new("name")
1030 );
1031 assert!(s.relationship_properties("KNOWS").is_none());
1032 assert_eq!(s.relationship_endpoints("KNOWS").len(), 1);
1033 assert_eq!(s.schema_digest(), [1u8; 32]);
1034 }
1035
1036 #[test]
1037 fn wrap_stdlib_shadows_inner_function() {
1038 let s = StandardLibrary::wrap(FakeSchema);
1039 let sig = s.function("count").expect("stdlib has count");
1041 match sig.return_ty {
1042 ReturnTy::Constant(PropertyType::Int) => {}
1043 other => panic!("expected stdlib count -> Int, got {other:?}"),
1044 }
1045 assert!(sig.categories.aggregate);
1046 }
1047
1048 #[test]
1049 fn wrap_falls_through_to_inner_for_unknown_stdlib_fn() {
1050 let s = StandardLibrary::wrap(FakeSchema);
1051 let sig = s.function("custom_fn").expect("inner fn");
1052 assert_eq!(sig.name, SmolStr::new("custom_fn"));
1053 match sig.return_ty {
1054 ReturnTy::Constant(PropertyType::Bool) => {}
1055 _ => panic!("expected inner Bool"),
1056 }
1057 }
1058
1059 #[test]
1060 fn wrap_returns_none_for_truly_unknown() {
1061 let s = StandardLibrary::wrap(FakeSchema);
1062 assert!(s.function("not_a_real_function").is_none());
1063 }
1064
1065 #[test]
1066 fn standard_library_is_object_safe() {
1067 fn accepts(_: &dyn SchemaProvider) {}
1069 let s = StandardLibrary::new();
1070 accepts(&s);
1071 let w = StandardLibrary::wrap(FakeSchema);
1072 accepts(&w);
1073 }
1074}