Skip to main content

linkrs_query/executor/expression/functions/
udf.rs

1//! Dynamic UDF support.
2//!
3//! The module provides the plugin interface (`plugin`), the error type
4//! (`error`), the ownership wrapper keeping the library alive (`dynamic`),
5//! and the file loading logic (`loader`).
6
7pub mod dynamic;
8pub mod error;
9pub mod loader;
10pub mod plugin;
11
12pub use dynamic::{execute_isolated, DynamicPlugin};
13pub use error::UdfError;
14pub use loader::{LoadedPlugin, UdfLoader};
15pub use plugin::{
16    check_arity, SharedPlugin, UdfAbiVersionFn, UdfCreateFn, UdfDestroyFn, UdfPlugin,
17    UDF_ABI_VERSION, UDF_ABI_VERSION_SYMBOL, UDF_CREATE_SYMBOL, UDF_DESTROY_SYMBOL,
18};
19
20#[cfg(test)]
21mod tests {
22    use super::*;
23    use crate::executor::expression::{ExpressionError, ExpressionErrorType};
24    use linkrs_core::Value;
25
26    struct Double;
27
28    impl UdfPlugin for Double {
29        fn name(&self) -> &str {
30            "test_double"
31        }
32
33        fn description(&self) -> &str {
34            "doubles an integer"
35        }
36
37        fn min_arity(&self) -> usize {
38            1
39        }
40
41        fn max_arity(&self) -> usize {
42            1
43        }
44
45        fn is_pure(&self) -> bool {
46            true
47        }
48
49        fn execute(&self, args: &[Value]) -> Result<Value, ExpressionError> {
50            match &args[0] {
51                Value::Int(v) => Ok(Value::Int(v * 2)),
52                other => Err(ExpressionError::new(
53                    ExpressionErrorType::TypeError,
54                    format!("expected int, got {other:?}"),
55                )),
56            }
57        }
58    }
59
60    struct Panic;
61
62    impl UdfPlugin for Panic {
63        fn name(&self) -> &str {
64            "test_panic"
65        }
66
67        fn description(&self) -> &str {
68            "always panics"
69        }
70
71        fn min_arity(&self) -> usize {
72            0
73        }
74
75        fn max_arity(&self) -> usize {
76            0
77        }
78
79        fn is_pure(&self) -> bool {
80            false
81        }
82
83        fn execute(&self, _args: &[Value]) -> Result<Value, ExpressionError> {
84            panic!("boom");
85        }
86    }
87
88    #[test]
89    fn test_isolated_execution_success() {
90        let plugin = Double;
91        let result = execute_isolated(&plugin, &[Value::Int(21)]).expect("execution failed");
92        assert_eq!(result, Value::Int(42));
93    }
94
95    #[test]
96    fn test_isolated_execution_arity_checked() {
97        let plugin = Double;
98        let err = execute_isolated(&plugin, &[]).unwrap_err();
99        assert_eq!(err.error_type, ExpressionErrorType::InvalidArgumentCount);
100    }
101
102    #[test]
103    fn test_isolated_execution_panic_contained() {
104        let plugin = Panic;
105        let err = execute_isolated(&plugin, &[]).unwrap_err();
106        assert_eq!(err.error_type, ExpressionErrorType::FunctionExecutionError);
107        assert!(err.message.contains("test_panic"));
108    }
109
110    #[test]
111    fn test_loader_rejects_missing_file() {
112        let err = UdfLoader::load(std::path::Path::new("/nonexistent/udf_plugin.so")).unwrap_err();
113        assert!(matches!(err, UdfError::InvalidPath(_, _)));
114    }
115
116    #[test]
117    fn test_loader_rejects_wrong_extension() {
118        let dir = std::env::temp_dir();
119        let path = dir.join("linkrs_udf_probe.txt");
120        std::fs::write(&path, b"not a library").expect("write probe file");
121        let err = UdfLoader::load(&path).unwrap_err();
122        std::fs::remove_file(&path).ok();
123        assert!(matches!(err, UdfError::InvalidPath(_, _)));
124    }
125
126    #[test]
127    fn test_install_from_source_rejects_remote() {
128        let err = UdfLoader::install_from_source("https://example.com/udf.so").unwrap_err();
129        assert!(matches!(err, UdfError::RepoDownloadUnsupported(_)));
130    }
131}