#![cfg(feature = "plugin")]
use cypherlite_core::error::CypherLiteError;
use cypherlite_core::plugin::{Plugin, ScalarFunction};
use cypherlite_core::types::PropertyValue;
use cypherlite_query::api::CypherLite;
use cypherlite_query::executor::Value;
use tempfile::tempdir;
struct DoubleFunction;
impl Plugin for DoubleFunction {
fn name(&self) -> &str {
"double"
}
fn version(&self) -> &str {
"1.0.0"
}
}
impl ScalarFunction for DoubleFunction {
fn call(&self, args: &[PropertyValue]) -> Result<PropertyValue, CypherLiteError> {
if args.len() != 1 {
return Err(CypherLiteError::PluginError(
"double() requires exactly one argument".to_string(),
));
}
match &args[0] {
PropertyValue::Int64(n) => Ok(PropertyValue::Int64(n * 2)),
_ => Err(CypherLiteError::PluginError(
"double() requires an integer argument".to_string(),
)),
}
}
}
struct MyUpperFunction;
impl Plugin for MyUpperFunction {
fn name(&self) -> &str {
"my_upper"
}
fn version(&self) -> &str {
"1.0.0"
}
}
impl ScalarFunction for MyUpperFunction {
fn call(&self, args: &[PropertyValue]) -> Result<PropertyValue, CypherLiteError> {
if args.len() != 1 {
return Err(CypherLiteError::PluginError(
"my_upper() requires exactly one argument".to_string(),
));
}
match &args[0] {
PropertyValue::String(s) => Ok(PropertyValue::String(s.to_uppercase())),
_ => Err(CypherLiteError::PluginError(
"my_upper() requires a string argument".to_string(),
)),
}
}
}
fn test_config(dir: &std::path::Path) -> cypherlite_core::DatabaseConfig {
cypherlite_core::DatabaseConfig {
path: dir.join("test.cyl"),
wal_sync_mode: cypherlite_core::SyncMode::Normal,
..Default::default()
}
}
#[test]
fn test_plugin_scalar_double() {
let dir = tempdir().expect("tempdir");
let mut db = CypherLite::open(test_config(dir.path())).expect("open");
db.register_scalar_function(Box::new(DoubleFunction))
.expect("register");
let result = db
.execute("UNWIND [21] AS x RETURN double(x) AS val")
.expect("execute");
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0].get("val"), Some(&Value::Int64(42)));
}
#[test]
fn test_plugin_scalar_my_upper() {
let dir = tempdir().expect("tempdir");
let mut db = CypherLite::open(test_config(dir.path())).expect("open");
db.register_scalar_function(Box::new(MyUpperFunction))
.expect("register");
db.execute("CREATE (n:Person {name: 'alice'})")
.expect("create");
let result = db
.execute("MATCH (n:Person) RETURN my_upper(n.name) AS upper_name")
.expect("execute");
assert_eq!(result.rows.len(), 1);
assert_eq!(
result.rows[0].get("upper_name"),
Some(&Value::String("ALICE".to_string()))
);
}
#[test]
fn test_builtin_functions_still_work_with_plugin() {
let dir = tempdir().expect("tempdir");
let mut db = CypherLite::open(test_config(dir.path())).expect("open");
db.execute("CREATE (n:Person {name: 'Alice'})-[:KNOWS]->(m:Person {name: 'Bob'})")
.expect("create");
let result = db
.execute("MATCH (n:Person) RETURN id(n) AS nid")
.expect("id query");
assert_eq!(result.rows.len(), 2);
let result = db
.execute("MATCH ()-[r:KNOWS]->() RETURN type(r) AS rel_type")
.expect("type query");
assert_eq!(result.rows.len(), 1);
assert_eq!(
result.rows[0].get("rel_type"),
Some(&Value::String("KNOWS".to_string()))
);
let result = db
.execute("MATCH (n:Person) RETURN labels(n) AS lbls")
.expect("labels query");
assert_eq!(result.rows.len(), 2);
}
#[test]
fn test_unregistered_function_error() {
let dir = tempdir().expect("tempdir");
let mut db = CypherLite::open(test_config(dir.path())).expect("open");
let result = db.execute("UNWIND [42] AS x RETURN nonexistent(x)");
assert!(result.is_err());
let err = result.expect_err("should fail");
let err_msg = format!("{}", err);
assert!(
err_msg.contains("unknown function") || err_msg.contains("not found"),
"expected 'unknown function' or 'not found' in error, got: {}",
err_msg
);
}
#[test]
fn test_list_scalar_functions() {
let dir = tempdir().expect("tempdir");
let mut db = CypherLite::open(test_config(dir.path())).expect("open");
db.register_scalar_function(Box::new(DoubleFunction))
.expect("register double");
db.register_scalar_function(Box::new(MyUpperFunction))
.expect("register my_upper");
let fns = db.list_scalar_functions();
assert_eq!(fns.len(), 2);
let names: Vec<&str> = fns.iter().map(|(n, _)| *n).collect();
assert!(names.contains(&"double"));
assert!(names.contains(&"my_upper"));
}
#[test]
fn test_duplicate_registration_error() {
let dir = tempdir().expect("tempdir");
let mut db = CypherLite::open(test_config(dir.path())).expect("open");
db.register_scalar_function(Box::new(DoubleFunction))
.expect("first register");
let result = db.register_scalar_function(Box::new(DoubleFunction));
assert!(result.is_err());
}