use std::collections::BTreeMap;
use std::sync::{LazyLock, OnceLock};
use smol_str::SmolStr;
use crate::{
DynamicReturnFn, EmptySchema, EndpointDecl, FnCategories, FunctionSignature, ParamDecl,
ProcedureSignature, PropertyDecl, PropertyType, ReturnTy, SchemaProvider,
};
struct BuiltIn {
name: &'static str,
params: Vec<(&'static str, PropertyType)>,
variadic: Option<PropertyType>,
return_ty: BuiltInReturn,
categories: FnCategories,
}
enum BuiltInReturn {
Constant(PropertyType),
Dynamic(fn() -> DynamicReturnFn),
}
impl BuiltIn {
fn to_signature(&self) -> FunctionSignature {
let params: Vec<ParamDecl> = self
.params
.iter()
.map(|(n, t)| ParamDecl {
name: SmolStr::new(n),
ty: t.clone(),
default: None,
})
.collect();
let variadic = self.variadic.as_ref().map(|t| ParamDecl {
name: SmolStr::new("args"),
ty: t.clone(),
default: None,
});
let return_ty = match &self.return_ty {
BuiltInReturn::Constant(t) => ReturnTy::Constant(t.clone()),
BuiltInReturn::Dynamic(f) => ReturnTy::Dynamic(f()),
};
FunctionSignature {
name: SmolStr::new(self.name),
params,
variadic,
return_ty,
categories: self.categories,
}
}
}
const fn pure() -> FnCategories {
FnCategories {
pure: true,
aggregate: false,
deterministic: true,
}
}
const fn agg() -> FnCategories {
FnCategories {
pure: true,
aggregate: true,
deterministic: true,
}
}
const fn nondet() -> FnCategories {
FnCategories {
pure: true,
aggregate: false,
deterministic: false,
}
}
static CATALOG: LazyLock<Vec<BuiltIn>> = LazyLock::new(|| {
vec![
BuiltIn {
name: "id",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Int),
categories: pure(),
},
BuiltIn {
name: "type",
params: vec![("r", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::String),
categories: pure(),
},
BuiltIn {
name: "labels",
params: vec![("n", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::List(Box::new(PropertyType::String))),
categories: pure(),
},
BuiltIn {
name: "keys",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::List(Box::new(PropertyType::String))),
categories: pure(),
},
BuiltIn {
name: "values",
params: vec![("map", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::List(Box::new(PropertyType::Any))),
categories: pure(),
},
BuiltIn {
name: "properties",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Any),
categories: pure(),
},
BuiltIn {
name: "length",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Int),
categories: pure(),
},
BuiltIn {
name: "size",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Int),
categories: pure(),
},
BuiltIn {
name: "coalesce",
params: vec![],
variadic: Some(PropertyType::Any),
return_ty: BuiltInReturn::Dynamic(|| {
Box::new(|tys: &[PropertyType]| {
tys.iter()
.find(|t| !matches!(t, PropertyType::Any))
.cloned()
.unwrap_or(PropertyType::Any)
})
}),
categories: pure(),
},
BuiltIn {
name: "toupper",
params: vec![("s", PropertyType::String)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::String),
categories: pure(),
},
BuiltIn {
name: "tolower",
params: vec![("s", PropertyType::String)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::String),
categories: pure(),
},
BuiltIn {
name: "trim",
params: vec![("s", PropertyType::String)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::String),
categories: pure(),
},
BuiltIn {
name: "ltrim",
params: vec![("s", PropertyType::String)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::String),
categories: pure(),
},
BuiltIn {
name: "rtrim",
params: vec![("s", PropertyType::String)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::String),
categories: pure(),
},
BuiltIn {
name: "reverse",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Dynamic(|| {
Box::new(|tys: &[PropertyType]| match tys.first() {
Some(t @ (PropertyType::String | PropertyType::List(_))) => t.clone(),
_ => PropertyType::Any,
})
}),
categories: pure(),
},
BuiltIn {
name: "substring",
params: vec![
("original", PropertyType::String),
("start", PropertyType::Int),
],
variadic: Some(PropertyType::Int),
return_ty: BuiltInReturn::Constant(PropertyType::String),
categories: pure(),
},
BuiltIn {
name: "replace",
params: vec![
("original", PropertyType::String),
("search", PropertyType::String),
("replace", PropertyType::String),
],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::String),
categories: pure(),
},
BuiltIn {
name: "split",
params: vec![
("original", PropertyType::String),
("delim", PropertyType::String),
],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::List(Box::new(PropertyType::String))),
categories: pure(),
},
BuiltIn {
name: "left",
params: vec![
("original", PropertyType::String),
("length", PropertyType::Int),
],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::String),
categories: pure(),
},
BuiltIn {
name: "right",
params: vec![
("original", PropertyType::String),
("length", PropertyType::Int),
],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::String),
categories: pure(),
},
BuiltIn {
name: "tostring",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::String),
categories: pure(),
},
BuiltIn {
name: "tointeger",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Int),
categories: pure(),
},
BuiltIn {
name: "tofloat",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Float),
categories: pure(),
},
BuiltIn {
name: "toboolean",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Bool),
categories: pure(),
},
BuiltIn {
name: "head",
params: vec![("list", PropertyType::List(Box::new(PropertyType::Any)))],
variadic: None,
return_ty: BuiltInReturn::Dynamic(|| {
Box::new(|tys: &[PropertyType]| match tys.first() {
Some(PropertyType::List(inner)) => (**inner).clone(),
_ => PropertyType::Any,
})
}),
categories: pure(),
},
BuiltIn {
name: "last",
params: vec![("list", PropertyType::List(Box::new(PropertyType::Any)))],
variadic: None,
return_ty: BuiltInReturn::Dynamic(|| {
Box::new(|tys: &[PropertyType]| match tys.first() {
Some(PropertyType::List(inner)) => (**inner).clone(),
_ => PropertyType::Any,
})
}),
categories: pure(),
},
BuiltIn {
name: "tail",
params: vec![("list", PropertyType::List(Box::new(PropertyType::Any)))],
variadic: None,
return_ty: BuiltInReturn::Dynamic(|| {
Box::new(|tys: &[PropertyType]| match tys.first() {
Some(t @ PropertyType::List(_)) => t.clone(),
_ => PropertyType::Any,
})
}),
categories: pure(),
},
BuiltIn {
name: "range",
params: vec![("start", PropertyType::Int), ("end", PropertyType::Int)],
variadic: Some(PropertyType::Int),
return_ty: BuiltInReturn::Constant(PropertyType::List(Box::new(PropertyType::Int))),
categories: pure(),
},
BuiltIn {
name: "nodes",
params: vec![("path", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::List(Box::new(PropertyType::Any))),
categories: pure(),
},
BuiltIn {
name: "relationships",
params: vec![("path", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::List(Box::new(PropertyType::Any))),
categories: pure(),
},
BuiltIn {
name: "abs",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Dynamic(|| {
Box::new(|tys: &[PropertyType]| match tys.first() {
Some(PropertyType::Int) => PropertyType::Int,
Some(PropertyType::Float) => PropertyType::Float,
_ => PropertyType::Any,
})
}),
categories: pure(),
},
BuiltIn {
name: "sign",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Int),
categories: pure(),
},
BuiltIn {
name: "ceil",
params: vec![("x", PropertyType::Float)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Float),
categories: pure(),
},
BuiltIn {
name: "floor",
params: vec![("x", PropertyType::Float)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Float),
categories: pure(),
},
BuiltIn {
name: "round",
params: vec![("x", PropertyType::Float)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Float),
categories: pure(),
},
BuiltIn {
name: "sqrt",
params: vec![("x", PropertyType::Float)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Float),
categories: pure(),
},
BuiltIn {
name: "exp",
params: vec![("x", PropertyType::Float)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Float),
categories: pure(),
},
BuiltIn {
name: "log",
params: vec![("x", PropertyType::Float)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Float),
categories: pure(),
},
BuiltIn {
name: "log10",
params: vec![("x", PropertyType::Float)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Float),
categories: pure(),
},
BuiltIn {
name: "sin",
params: vec![("x", PropertyType::Float)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Float),
categories: pure(),
},
BuiltIn {
name: "cos",
params: vec![("x", PropertyType::Float)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Float),
categories: pure(),
},
BuiltIn {
name: "tan",
params: vec![("x", PropertyType::Float)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Float),
categories: pure(),
},
BuiltIn {
name: "pi",
params: vec![],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Float),
categories: pure(),
},
BuiltIn {
name: "e",
params: vec![],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Float),
categories: pure(),
},
BuiltIn {
name: "rand",
params: vec![],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Float),
categories: nondet(),
},
BuiltIn {
name: "count",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Int),
categories: agg(),
},
BuiltIn {
name: "sum",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Dynamic(|| {
Box::new(|tys: &[PropertyType]| match tys.first() {
Some(PropertyType::Int) => PropertyType::Int,
Some(PropertyType::Float) => PropertyType::Float,
_ => PropertyType::Any,
})
}),
categories: agg(),
},
BuiltIn {
name: "avg",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Float),
categories: agg(),
},
BuiltIn {
name: "min",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Dynamic(|| {
Box::new(|tys: &[PropertyType]| tys.first().cloned().unwrap_or(PropertyType::Any))
}),
categories: agg(),
},
BuiltIn {
name: "max",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Dynamic(|| {
Box::new(|tys: &[PropertyType]| tys.first().cloned().unwrap_or(PropertyType::Any))
}),
categories: agg(),
},
BuiltIn {
name: "collect",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Dynamic(|| {
Box::new(|tys: &[PropertyType]| match tys.first() {
Some(t) => PropertyType::List(Box::new(t.clone())),
None => PropertyType::List(Box::new(PropertyType::Any)),
})
}),
categories: agg(),
},
BuiltIn {
name: "stdev",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Float),
categories: agg(),
},
BuiltIn {
name: "stdevp",
params: vec![("x", PropertyType::Any)],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Float),
categories: agg(),
},
BuiltIn {
name: "percentilecont",
params: vec![
("x", PropertyType::Any),
("percentile", PropertyType::Float),
],
variadic: None,
return_ty: BuiltInReturn::Constant(PropertyType::Float),
categories: agg(),
},
BuiltIn {
name: "percentiledisc",
params: vec![
("x", PropertyType::Any),
("percentile", PropertyType::Float),
],
variadic: None,
return_ty: BuiltInReturn::Dynamic(|| {
Box::new(|tys: &[PropertyType]| tys.first().cloned().unwrap_or(PropertyType::Any))
}),
categories: agg(),
},
]
});
fn find_builtin(name: &str) -> Option<&'static BuiltIn> {
static INDEX: OnceLock<BTreeMap<&'static str, usize>> = OnceLock::new();
let index = INDEX.get_or_init(|| {
CATALOG
.iter()
.enumerate()
.map(|(i, b)| (b.name, i))
.collect()
});
if name.bytes().all(|b| !b.is_ascii_uppercase()) {
return index.get(name).map(|&i| &CATALOG[i]);
}
let lower = name.to_ascii_lowercase();
index.get(lower.as_str()).map(|&i| &CATALOG[i])
}
pub struct StandardLibrary<S: SchemaProvider = EmptySchema> {
inner: S,
}
impl<S: SchemaProvider + core::fmt::Debug> core::fmt::Debug for StandardLibrary<S> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("StandardLibrary")
.field("builtin_count", &CATALOG.len())
.field("inner", &self.inner)
.finish()
}
}
impl StandardLibrary<EmptySchema> {
pub fn new() -> Self {
Self { inner: EmptySchema }
}
}
impl Default for StandardLibrary<EmptySchema> {
fn default() -> Self {
Self::new()
}
}
impl<S: SchemaProvider> StandardLibrary<S> {
pub fn wrap(inner: S) -> Self {
Self { inner }
}
pub fn inner(&self) -> &S {
&self.inner
}
pub fn builtin_names() -> Vec<&'static str> {
let mut v: Vec<&'static str> = CATALOG.iter().map(|b| b.name).collect();
v.sort_unstable();
v
}
pub fn builtin_count() -> usize {
CATALOG.len()
}
}
impl<S: SchemaProvider> SchemaProvider for StandardLibrary<S> {
fn labels(&self) -> Vec<SmolStr> {
self.inner.labels()
}
fn relationship_types(&self) -> Vec<SmolStr> {
self.inner.relationship_types()
}
fn has_label(&self, name: &str) -> bool {
self.inner.has_label(name)
}
fn has_relationship_type(&self, name: &str) -> bool {
self.inner.has_relationship_type(name)
}
fn node_properties(&self, label: &str) -> Option<Vec<PropertyDecl>> {
self.inner.node_properties(label)
}
fn relationship_properties(&self, rel_type: &str) -> Option<Vec<PropertyDecl>> {
self.inner.relationship_properties(rel_type)
}
fn relationship_endpoints(&self, rel_type: &str) -> Vec<EndpointDecl> {
self.inner.relationship_endpoints(rel_type)
}
fn inverse_of(&self, rel_type: &str) -> Option<SmolStr> {
self.inner.inverse_of(rel_type)
}
fn function(&self, name: &str) -> Option<FunctionSignature> {
if let Some(b) = find_builtin(name) {
return Some(b.to_signature());
}
self.inner.function(name)
}
fn procedure(&self, name: &str) -> Option<ProcedureSignature> {
self.inner.procedure(name)
}
fn schema_digest(&self) -> [u8; 32] {
self.inner.schema_digest()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn catalog_has_no_duplicate_names() {
use std::collections::HashSet;
let mut seen = HashSet::new();
for b in CATALOG.iter() {
assert!(seen.insert(b.name), "duplicate built-in: {}", b.name);
}
}
#[test]
fn catalog_names_are_lowercase_ascii() {
for b in CATALOG.iter() {
assert!(
b.name
.bytes()
.all(|c: u8| c.is_ascii() && !c.is_ascii_uppercase()),
"catalog name not lowercase-ascii: {}",
b.name
);
}
}
#[test]
fn new_resolves_core_functions() {
let s = StandardLibrary::new();
for name in [
"id",
"type",
"labels",
"keys",
"values",
"properties",
"length",
"size",
"coalesce",
] {
assert!(s.function(name).is_some(), "missing: {name}");
}
}
#[test]
fn values_returns_list_of_any() {
let s = StandardLibrary::new();
let sig = s.function("values").expect("values is registered");
assert_eq!(sig.name, SmolStr::new("values"));
match sig.return_ty {
ReturnTy::Constant(PropertyType::List(inner)) => {
assert_eq!(*inner, PropertyType::Any);
}
other => panic!("expected Constant(List(Any)), got {other:?}"),
}
}
#[test]
fn new_resolves_function_families() {
let s = StandardLibrary::new();
for n in ["toUpper", "toLower", "substring", "replace", "split"] {
assert!(s.function(n).is_some(), "string: {n}");
}
for n in ["head", "last", "tail", "range", "nodes", "relationships"] {
assert!(s.function(n).is_some(), "collection: {n}");
}
for n in ["abs", "ceil", "floor", "sqrt", "sin", "pi", "rand"] {
assert!(s.function(n).is_some(), "math: {n}");
}
for n in ["count", "sum", "avg", "min", "max", "collect"] {
assert!(s.function(n).is_some(), "agg: {n}");
}
}
#[test]
fn lookup_is_case_insensitive() {
let s = StandardLibrary::new();
assert!(s.function("COUNT").is_some());
assert!(s.function("Count").is_some());
assert!(s.function("count").is_some());
assert!(s.function("cOuNt").is_some());
}
#[test]
fn unknown_function_returns_none() {
let s = StandardLibrary::new();
assert!(s.function("fribble").is_none());
}
#[test]
fn aggregation_flag_set_on_aggs() {
let s = StandardLibrary::new();
let c = s.function("count").unwrap();
assert!(c.categories.aggregate);
let a = s.function("abs").unwrap();
assert!(!a.categories.aggregate);
}
#[test]
fn rand_is_non_deterministic() {
let s = StandardLibrary::new();
let r = s.function("rand").unwrap();
assert!(!r.categories.deterministic);
}
#[test]
fn dynamic_return_closure_is_fresh_per_lookup() {
let s = StandardLibrary::new();
let s1 = s.function("coalesce").unwrap();
let s2 = s.function("coalesce").unwrap();
let probe = |sig: FunctionSignature| match sig.return_ty {
ReturnTy::Dynamic(f) => f(&[PropertyType::String]),
ReturnTy::Constant(_) => panic!("expected Dynamic"),
};
assert_eq!(probe(s1), PropertyType::String);
assert_eq!(probe(s2), PropertyType::String);
}
#[test]
fn coalesce_dynamic_picks_first_non_any() {
let s = StandardLibrary::new();
let sig = s.function("coalesce").unwrap();
match sig.return_ty {
ReturnTy::Dynamic(f) => {
assert_eq!(
f(&[PropertyType::Any, PropertyType::Int, PropertyType::String]),
PropertyType::Int
);
assert_eq!(f(&[]), PropertyType::Any);
assert_eq!(f(&[PropertyType::Any]), PropertyType::Any);
}
ReturnTy::Constant(_) => panic!("coalesce should be Dynamic"),
}
}
#[test]
fn abs_dynamic_preserves_int_or_float() {
let s = StandardLibrary::new();
let sig = s.function("abs").unwrap();
match sig.return_ty {
ReturnTy::Dynamic(f) => {
assert_eq!(f(&[PropertyType::Int]), PropertyType::Int);
assert_eq!(f(&[PropertyType::Float]), PropertyType::Float);
assert_eq!(f(&[PropertyType::String]), PropertyType::Any);
}
ReturnTy::Constant(_) => panic!("abs should be Dynamic"),
}
}
#[test]
fn head_dynamic_unwraps_list() {
let s = StandardLibrary::new();
let sig = s.function("head").unwrap();
match sig.return_ty {
ReturnTy::Dynamic(f) => {
assert_eq!(
f(&[PropertyType::List(Box::new(PropertyType::Int))]),
PropertyType::Int
);
assert_eq!(f(&[PropertyType::Int]), PropertyType::Any);
}
ReturnTy::Constant(_) => panic!("head should be Dynamic"),
}
}
#[test]
fn collect_dynamic_wraps_in_list() {
let s = StandardLibrary::new();
let sig = s.function("collect").unwrap();
match sig.return_ty {
ReturnTy::Dynamic(f) => {
assert_eq!(
f(&[PropertyType::Int]),
PropertyType::List(Box::new(PropertyType::Int))
);
}
ReturnTy::Constant(_) => panic!("collect should be Dynamic"),
}
}
#[test]
fn builtin_names_is_sorted_and_complete() {
let names = StandardLibrary::<EmptySchema>::builtin_names();
assert_eq!(names.len(), StandardLibrary::<EmptySchema>::builtin_count());
assert_eq!(names.len(), CATALOG.len());
let mut sorted = names.clone();
sorted.sort_unstable();
assert_eq!(names, sorted);
}
struct FakeSchema;
impl SchemaProvider for FakeSchema {
fn labels(&self) -> Vec<SmolStr> {
vec![SmolStr::new("Person")]
}
fn relationship_types(&self) -> Vec<SmolStr> {
vec![SmolStr::new("KNOWS")]
}
fn node_properties(&self, label: &str) -> Option<Vec<PropertyDecl>> {
if label == "Person" {
Some(vec![PropertyDecl {
name: SmolStr::new("name"),
ty: PropertyType::String,
required: true,
}])
} else {
None
}
}
fn relationship_properties(&self, _: &str) -> Option<Vec<PropertyDecl>> {
None
}
fn relationship_endpoints(&self, rel: &str) -> Vec<EndpointDecl> {
if rel == "KNOWS" {
vec![EndpointDecl {
from: SmolStr::new("Person"),
to: SmolStr::new("Person"),
cardinality: crate::Cardinality::ManyToMany,
}]
} else {
Vec::new()
}
}
fn inverse_of(&self, _: &str) -> Option<SmolStr> {
None
}
fn function(&self, name: &str) -> Option<FunctionSignature> {
if name == "custom_fn" {
Some(FunctionSignature {
name: SmolStr::new("custom_fn"),
params: vec![],
variadic: None,
return_ty: ReturnTy::Constant(PropertyType::Bool),
categories: pure(),
})
} else if name == "count" {
Some(FunctionSignature {
name: SmolStr::new("count"),
params: vec![],
variadic: None,
return_ty: ReturnTy::Constant(PropertyType::Bool),
categories: pure(),
})
} else {
None
}
}
fn procedure(&self, _: &str) -> Option<ProcedureSignature> {
None
}
fn schema_digest(&self) -> [u8; 32] {
[1u8; 32]
}
}
#[test]
fn wrap_delegates_non_function_surface() {
let s = StandardLibrary::wrap(FakeSchema);
assert_eq!(s.labels(), vec![SmolStr::new("Person")]);
assert!(s.has_label("Person"));
assert_eq!(s.relationship_types(), vec![SmolStr::new("KNOWS")]);
assert!(s.has_relationship_type("KNOWS"));
assert_eq!(
s.node_properties("Person").unwrap()[0].name,
SmolStr::new("name")
);
assert!(s.relationship_properties("KNOWS").is_none());
assert_eq!(s.relationship_endpoints("KNOWS").len(), 1);
assert_eq!(s.schema_digest(), [1u8; 32]);
}
#[test]
fn wrap_stdlib_shadows_inner_function() {
let s = StandardLibrary::wrap(FakeSchema);
let sig = s.function("count").expect("stdlib has count");
match sig.return_ty {
ReturnTy::Constant(PropertyType::Int) => {}
other => panic!("expected stdlib count -> Int, got {other:?}"),
}
assert!(sig.categories.aggregate);
}
#[test]
fn wrap_falls_through_to_inner_for_unknown_stdlib_fn() {
let s = StandardLibrary::wrap(FakeSchema);
let sig = s.function("custom_fn").expect("inner fn");
assert_eq!(sig.name, SmolStr::new("custom_fn"));
match sig.return_ty {
ReturnTy::Constant(PropertyType::Bool) => {}
_ => panic!("expected inner Bool"),
}
}
#[test]
fn wrap_returns_none_for_truly_unknown() {
let s = StandardLibrary::wrap(FakeSchema);
assert!(s.function("not_a_real_function").is_none());
}
#[test]
fn standard_library_is_object_safe() {
fn accepts(_: &dyn SchemaProvider) {}
let s = StandardLibrary::new();
accepts(&s);
let w = StandardLibrary::wrap(FakeSchema);
accepts(&w);
}
}