Skip to main content

uni_cypher/
plugin_aggregates.rs

1// Rust guideline compliant
2//! Cross-crate registry of plugin-registered aggregate function names.
3//!
4//! The Cypher AST's [`crate::ast::Expr::is_aggregate`] uses a hardcoded
5//! list of built-in aggregate names (`count`, `sum`, `min`, …) to
6//! decide whether a `FunctionCall` routes through the planner's
7//! aggregate translation. Plugin-registered aggregates (M9
8//! `uni.plugin.declareAggregate` and any other
9//! `uni_plugin::traits::aggregate::AggregatePluginFn` source) are
10//! not in that list, so a Cypher query like `RETURN myAgg(n.value)`
11//! would otherwise fall through to scalar UDF resolution and fail.
12//!
13//! Rather than thread a `PluginRegistry` reference through every AST
14//! query, plugin registrars publish each aggregate's lowercased qname
15//! into this process-wide set at registration time. The AST consults
16//! it inside `is_aggregate`; the planner's own copy of the hardcoded
17//! list (in `uni-query/src/query/planner.rs::is_aggregate_function_name`)
18//! does the same.
19//!
20//! # Lifecycle
21//!
22//! Entries are added but never removed today (M9 declared aggregates
23//! cannot be dropped while in-flight queries reference them). When
24//! `dropDeclared` infrastructure matures past M11, a counterpart
25//! `unregister_plugin_aggregate` will follow.
26
27use std::collections::HashSet;
28use std::sync::{OnceLock, RwLock};
29
30fn names() -> &'static RwLock<HashSet<String>> {
31    static SET: OnceLock<RwLock<HashSet<String>>> = OnceLock::new();
32    SET.get_or_init(|| RwLock::new(HashSet::new()))
33}
34
35/// Register a fully-qualified aggregate name (`"namespace.local"`)
36/// so the Cypher planner routes calls to it through the aggregate
37/// translation path instead of scalar UDF resolution.
38///
39/// The name is stored lowercase. Calls are idempotent.
40pub fn register_plugin_aggregate(qname: impl Into<String>) {
41    let lc = qname.into().to_ascii_lowercase();
42    if let Ok(mut set) = names().write() {
43        set.insert(lc);
44    }
45}
46
47/// Return `true` if `name` (case-insensitive) was previously registered
48/// via [`register_plugin_aggregate`].
49#[must_use]
50pub fn is_known_plugin_aggregate(name: &str) -> bool {
51    names()
52        .read()
53        .map(|set| set.contains(&name.to_ascii_lowercase()))
54        .unwrap_or(false)
55}