Skip to main content

cyrs_schema/
standard_library.rs

1//! Built-in openCypher function catalog (spec §8.3).
2//!
3//! [`StandardLibrary`] supplies the openCypher-standardised function set
4//! (`id`, `type`, `labels`, `keys`, `properties`, the `length`/`size`
5//! family, `coalesce`, string/collection/math/aggregation functions) and
6//! is independent of any consumer schema. Consumers wrap their own
7//! [`SchemaProvider`] with [`StandardLibrary::wrap`] to get the union.
8//!
9//! # Composition
10//!
11//! - `StandardLibrary::new()` — stdlib only; useful for schema-free mode
12//!   (spec §8.4 — stdlib is still consulted when no schema is supplied).
13//! - `StandardLibrary::wrap(inner)` — stdlib ∪ `inner`. Function lookup
14//!   tries stdlib first, then falls back to `inner`; every other method
15//!   delegates to `inner`. This ordering means stdlib names shadow
16//!   consumer-declared overrides by design (spec §8.3 names "the
17//!   openCypher-standardized function set"; consumers MUST NOT redefine
18//!   them).
19//! - All other [`SchemaProvider`] surface (labels, procedures, endpoints,
20//!   properties, digest) comes from the wrapped inner schema; stdlib is
21//!   function-only.
22
23use 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
33// ============================================================
34// Catalog entry
35// ============================================================
36
37/// A compact description of a built-in function. Converted to a full
38/// [`FunctionSignature`] on demand by [`StandardLibrary::function`]. The
39/// catalog is held in a [`LazyLock`] (rather than a `static`) because
40/// `PropertyType::List` requires a `Box::new` which is not `const`.
41struct 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    /// Produces a fresh closure per lookup (closures are not `Clone`).
52    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
85// ============================================================
86// Catalog (openCypher-standard functions)
87// ============================================================
88
89const 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
113/// The catalog. Names are stored lower-case; openCypher function names
114/// are case-insensitive per the TCK. Lookup via [`find_builtin`]
115/// normalises the query name. Built once on first access.
116///
117/// Grouped by the five families spec §8.3 names explicitly.
118static CATALOG: LazyLock<Vec<BuiltIn>> = LazyLock::new(|| {
119    vec![
120        // ---- element accessors ------------------------------------------
121        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            // openCypher: `keys(x)` — accepts a `NODE`, `RELATIONSHIP`,
144            // or `MAP`, returning the property-name / map-key list as
145            // `LIST<STRING>`. The parameter slot is typed `Any` here
146            // because the schema layer has no `Map` variant; the
147            // Node / Relationship / Map kind check happens in
148            // `cyrs-sema` via `ArgShape::GraphEntityOrMap`.
149            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            // openCypher: `values(map)` — returns the map's values as
157            // `LIST<ANY>`. The kind check (argument must be a `MAP`)
158            // lives in `cyrs-sema` via `ArgShape::Map`.
159            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        // ---- length / size family ---------------------------------------
173        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        // ---- coalesce (variadic; narrowest-supertype — see §21.2) -------
188        BuiltIn {
189            name: "coalesce",
190            params: vec![],
191            variadic: Some(PropertyType::Any),
192            return_ty: BuiltInReturn::Dynamic(|| {
193                // First non-`Any` argument type wins; `Any` otherwise. The
194                // full "narrowest supertype" rule lives in cyrs-sema's
195                // unification engine; this is a pragmatic approximation for
196                // the schema layer.
197                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        // ---- string functions -------------------------------------------
207        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                // `reverse` works on both strings and lists. Return the
248                // argument type when it is a string or list; `Any` otherwise.
249                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        // ---- collection functions ---------------------------------------
336        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        // ---- math functions ---------------------------------------------
394        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        // ---- aggregation functions --------------------------------------
506        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
602/// Case-insensitive lookup. openCypher function names are case-
603/// insensitive; we store the catalog lower-case and normalise once at
604/// lookup time using ASCII case folding (all built-in names are ASCII).
605fn 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    // Fast path: already lower-case ASCII.
615    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
622// ============================================================
623// StandardLibrary
624// ============================================================
625
626/// The openCypher built-in function catalog as a [`SchemaProvider`].
627///
628/// Two constructors:
629///
630/// - [`StandardLibrary::new`] — stdlib only; labels, endpoints, etc.
631///   delegate to an [`EmptySchema`]. Suitable for schema-free mode
632///   (spec §8.4).
633/// - [`StandardLibrary::wrap`] — stdlib ∪ consumer schema. Function
634///   lookup consults the stdlib first; all other surface delegates to
635///   the wrapped schema.
636///
637/// The type is generic over the inner provider so the composition is
638/// zero-cost. A `Box<dyn SchemaProvider>` also satisfies
639/// `SchemaProvider` (object-safe per spec §8.1), which makes the
640/// dynamic form ergonomic.
641pub 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    /// Construct a stdlib-only provider (no labels, no procedures).
656    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    /// Wrap a consumer schema so function lookups see stdlib ∪ inner.
669    ///
670    /// Stdlib names shadow consumer declarations for the same name.
671    /// Consumers MUST NOT redefine openCypher built-ins (spec §8.3).
672    pub fn wrap(inner: S) -> Self {
673        Self { inner }
674    }
675
676    /// Borrow the wrapped inner schema. Useful for callers that need
677    /// raw access bypassing the stdlib overlay.
678    pub fn inner(&self) -> &S {
679        &self.inner
680    }
681
682    /// All built-in function names, sorted. Stable across releases;
683    /// exposed for completion and documentation consumers.
684    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    /// Number of built-ins in the catalog. Test-facing shape check.
691    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        // StandardLibrary is function-only; procedures come from the
738        // wrapped schema (spec §8.3 names only functions).
739        self.inner.procedure(name)
740    }
741
742    fn schema_digest(&self) -> [u8; 32] {
743        // Stdlib contents are fixed at compile time, so the digest is
744        // the wrapped schema's digest unchanged. If stdlib ever becomes
745        // configurable, mix a stdlib version tag in here.
746        self.inner.schema_digest()
747    }
748}
749
750// ============================================================
751// Tests
752// ============================================================
753
754#[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    /// cy-afo: `values(map)` returns `LIST<ANY>` per openCypher (spec §8.3).
799    /// The kind check (argument must be a MAP) lives in cyrs-sema.
800    #[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        // String family.
817        for n in ["toUpper", "toLower", "substring", "replace", "split"] {
818            assert!(s.function(n).is_some(), "string: {n}");
819        }
820        // Collection family.
821        for n in ["head", "last", "tail", "range", "nodes", "relationships"] {
822            assert!(s.function(n).is_some(), "collection: {n}");
823        }
824        // Math family.
825        for n in ["abs", "ceil", "floor", "sqrt", "sin", "pi", "rand"] {
826            assert!(s.function(n).is_some(), "math: {n}");
827        }
828        // Aggregation family.
829        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        // Each lookup produces a fresh closure (closures are not
868        // Clone). Call it twice from two lookups.
869        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    // ---- wrap(inner) composition -------------------------------------
953
954    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                // Try to shadow a built-in; stdlib should win.
1001                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        // Inner returns Bool for `count`; stdlib wins with Int.
1040        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        // Force dyn coercion via a function that takes &dyn.
1068        fn accepts(_: &dyn SchemaProvider) {}
1069        let s = StandardLibrary::new();
1070        accepts(&s);
1071        let w = StandardLibrary::wrap(FakeSchema);
1072        accepts(&w);
1073    }
1074}