Skip to main content

linkrs_query/executor/expression/functions/registry/
udf.rs

1use super::super::udf::{LoadedPlugin, SharedPlugin, UdfError, UdfLoader};
2use super::super::CustomFunction;
3use super::FunctionRegistry;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::time::SystemTime;
7
8/// Metadata kept for each dynamically loaded UDF library.
9///
10/// The shared plugin handle keeps the underlying `Library` alive, so entries
11/// must be removed through `unload_dynamic_udf` (dropping the entry unloads
12/// the library).
13pub struct LoadedExtension {
14    /// Upper-cased function name.
15    pub name: String,
16    /// Canonical library path.
17    pub path: PathBuf,
18    /// Modification time observed at load, used by reload decisions.
19    pub loaded_mtime: Option<SystemTime>,
20    /// Shared plugin handle.
21    pub plugin: SharedPlugin,
22}
23
24impl std::fmt::Debug for LoadedExtension {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        f.debug_struct("LoadedExtension")
27            .field("name", &self.name)
28            .field("path", &self.path)
29            .finish()
30    }
31}
32/// Snapshot describing a loaded dynamic UDF for listing purposes.
33#[derive(Debug, Clone, PartialEq)]
34pub struct DynamicUdfInfo {
35    pub name: String,
36    pub path: String,
37    pub description: String,
38    pub min_arity: usize,
39    pub max_arity: usize,
40    pub is_pure: bool,
41}
42
43impl FunctionRegistry {
44    /// Modification time of a loaded dynamic UDF library, when recorded.
45    pub fn dynamic_udf_mtime(&self, name: &str) -> Option<SystemTime> {
46        self.dynamic_libraries
47            .get(&name.to_uppercase())
48            .and_then(|ext| ext.loaded_mtime)
49    }
50    /// Load a UDF dynamic library and register the exported function.
51    ///
52    /// Returns the registered (original-case) function name. The library
53    /// handle is retained until `unload_dynamic_udf` is called.
54    pub fn load_dynamic_udf(&mut self, path: &Path) -> Result<String, UdfError> {
55        let loaded = UdfLoader::load(path)?;
56        self.register_loaded_plugin(loaded)
57    }
58
59    /// Install a UDF from an `INSTALL EXTENSION ... FROM` source.
60    ///
61    /// Local file paths load directly; `http(s)://` sources return
62    /// `RepoDownloadUnsupported` until a repository module is added.
63    pub fn install_dynamic_udf(&mut self, source: &str) -> Result<String, UdfError> {
64        let loaded = UdfLoader::install_from_source(source)?;
65        self.register_loaded_plugin(loaded)
66    }
67    fn register_loaded_plugin(&mut self, loaded: LoadedPlugin) -> Result<String, UdfError> {
68        let name = loaded.plugin.name().to_string();
69        let upper = name.to_uppercase();
70        if self.builtin_functions.contains_key(&upper) {
71            return Err(UdfError::BuiltinConflict(name));
72        }
73        if let Some(existing) = self.dynamic_libraries.get(&upper) {
74            return Err(UdfError::AlreadyLoaded(
75                name,
76                existing.path.to_string_lossy().to_string(),
77            ));
78        }
79        if let Some(existing) = self.custom_functions.get(&upper) {
80            if !existing.is_dynamic() {
81                return Err(UdfError::AlreadyLoaded(
82                    name,
83                    "custom function registry".to_string(),
84                ));
85            }
86        }
87        let func = CustomFunction::new_dynamic(Arc::clone(&loaded.plugin));
88        self.register_custom_full(func);
89        self.dynamic_libraries.insert(
90            upper,
91            LoadedExtension {
92                name: name.clone(),
93                path: loaded.path,
94                loaded_mtime: loaded.loaded_mtime,
95                plugin: loaded.plugin,
96            },
97        );
98        Ok(name)
99    }
100    /// Register an in-process plugin without a dynamic library.
101    ///
102    /// Primarily intended for tests and embeddings that build the plugin
103    /// in the host binary; production libraries go through `load_dynamic_udf`.
104    pub fn register_dynamic_plugin(&mut self, plugin: SharedPlugin) -> Result<String, UdfError> {
105        let name = plugin.name().to_string();
106        if name.trim().is_empty() {
107            return Err(UdfError::InvalidName("<in-process>".to_string()));
108        }
109        let upper = name.to_uppercase();
110        if self.builtin_functions.contains_key(&upper) {
111            return Err(UdfError::BuiltinConflict(name));
112        }
113        if self.dynamic_libraries.contains_key(&upper) {
114            let existing = &self.dynamic_libraries[&upper];
115            return Err(UdfError::AlreadyLoaded(
116                name,
117                existing.path.to_string_lossy().to_string(),
118            ));
119        }
120        let func = CustomFunction::new_dynamic(Arc::clone(&plugin));
121        self.register_custom_full(func);
122        self.dynamic_libraries.insert(
123            upper,
124            LoadedExtension {
125                name: name.clone(),
126                path: PathBuf::from("<in-process>"),
127                loaded_mtime: None,
128                plugin,
129            },
130        );
131        Ok(name)
132    }
133    /// Unload a previously loaded dynamic UDF by function name.
134    pub fn unload_dynamic_udf(&mut self, name: &str) -> Result<(), UdfError> {
135        let upper = name.to_uppercase();
136        let extension = self
137            .dynamic_libraries
138            .remove(&upper)
139            .ok_or_else(|| UdfError::NotLoaded(name.to_string()))?;
140        let _ = extension;
141        // Only remove the registry entry if it still points at a dynamic
142        // function; a newer non-dynamic registration must be preserved.
143        let remove = self
144            .custom_functions
145            .get(&upper)
146            .map(|f| f.is_dynamic())
147            .unwrap_or(false);
148        if remove {
149            self.custom_functions.remove(&upper);
150        }
151        Ok(())
152    }
153    /// Reload a dynamic UDF from its original library path.
154    ///
155    /// Returns `true` when the library was reloaded, `false` when the file
156    /// is unchanged since the initial load and reloading was skipped.
157    /// In-process plugins (no library path) always report `false`.
158    pub fn reload_dynamic_udf(&mut self, name: &str) -> Result<bool, UdfError> {
159        let upper = name.to_uppercase();
160        let current_mtime = self
161            .dynamic_libraries
162            .get(&upper)
163            .ok_or_else(|| UdfError::NotLoaded(name.to_string()))?
164            .loaded_mtime;
165        let path = self.dynamic_libraries[&upper].path.clone();
166        if path.to_string_lossy() == "<in-process>" {
167            return Ok(false);
168        }
169        let current_file_mtime = std::fs::metadata(&path)
170            .ok()
171            .and_then(|m| m.modified().ok());
172        if current_mtime.is_some() && current_mtime == current_file_mtime {
173            return Ok(false);
174        }
175        // Load the replacement before dropping the old one so a failed
176        // reload keeps the previous version active.
177        let loaded = UdfLoader::load(&path)?;
178        let func = CustomFunction::new_dynamic(Arc::clone(&loaded.plugin));
179        let registered_name = loaded.plugin.name().to_string();
180        self.register_custom_full(func);
181        self.dynamic_libraries.insert(
182            upper,
183            LoadedExtension {
184                name: registered_name,
185                path: loaded.path,
186                loaded_mtime: loaded.loaded_mtime,
187                plugin: loaded.plugin,
188            },
189        );
190        Ok(true)
191    }
192    /// Check whether a function name resolves to a dynamic UDF.
193    pub fn is_dynamic(&self, name: &str) -> bool {
194        self.dynamic_libraries.contains_key(&name.to_uppercase())
195    }
196    /// List all loaded dynamic UDFs ordered by name.
197    pub fn list_dynamic_udfs(&self) -> Vec<DynamicUdfInfo> {
198        let mut infos: Vec<DynamicUdfInfo> = self
199            .dynamic_libraries
200            .values()
201            .map(|ext| DynamicUdfInfo {
202                name: ext.name.clone(),
203                path: ext.path.to_string_lossy().to_string(),
204                description: ext.plugin.description().to_string(),
205                min_arity: ext.plugin.min_arity(),
206                max_arity: ext.plugin.max_arity(),
207                is_pure: ext.plugin.is_pure(),
208            })
209            .collect();
210        infos.sort_by(|a, b| a.name.cmp(&b.name));
211        infos
212    }
213}