1use nodedb_types::columnar::ColumnType;
10
11use super::builtins::builtin_functions;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
15pub struct Version {
16 pub major: u8,
17 pub minor: u8,
18 pub patch: u8,
19}
20
21impl Version {
22 pub const fn new(major: u8, minor: u8, patch: u8) -> Self {
23 Self {
24 major,
25 minor,
26 patch,
27 }
28 }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct ArgTypeSpec {
34 pub name: &'static str,
36 pub accepted: &'static [ColumnType],
38 pub variadic: bool,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum FunctionCategory {
45 Scalar,
46 Aggregate,
47 Window,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum SearchTrigger {
53 None,
54 VectorSearch,
55 MultiVectorSearch,
56 SparseSearch,
60 TextSearch,
61 HybridSearch,
62 TextMatch,
63 SpatialDWithin,
64 SpatialContains,
65 SpatialIntersects,
66 SpatialWithin,
67 TimeBucket,
68 ArraySlice,
70 ArrayProject,
72 ArrayAgg,
75 ArrayElementwise,
78 ArrayFlush,
80 ArrayCompact,
82 GraphSearch,
87}
88
89#[derive(Debug, Clone)]
91pub struct FunctionMeta {
92 pub name: &'static str,
93 pub category: FunctionCategory,
94 pub min_args: usize,
95 pub max_args: usize,
96 pub search_trigger: SearchTrigger,
97 pub return_type: Option<ColumnType>,
100 pub arg_types: &'static [ArgTypeSpec],
102 pub since: Version,
104}
105
106pub struct FunctionRegistry {
108 functions: Vec<FunctionMeta>,
109}
110
111impl FunctionRegistry {
112 pub fn new() -> Self {
114 Self {
115 functions: builtin_functions(),
116 }
117 }
118
119 pub fn lookup(&self, name: &str) -> Option<&FunctionMeta> {
121 let lower = name.to_lowercase();
122 self.functions.iter().find(|f| f.name == lower)
123 }
124
125 pub fn search_trigger(&self, name: &str) -> SearchTrigger {
127 self.lookup(name)
128 .map(|f| f.search_trigger)
129 .unwrap_or(SearchTrigger::None)
130 }
131
132 pub fn is_aggregate(&self, name: &str) -> bool {
134 self.lookup(name)
135 .is_some_and(|f| f.category == FunctionCategory::Aggregate)
136 }
137
138 pub fn is_window(&self, name: &str) -> bool {
140 self.lookup(name)
141 .is_some_and(|f| f.category == FunctionCategory::Window)
142 }
143}
144
145impl Default for FunctionRegistry {
146 fn default() -> Self {
147 Self::new()
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn lookup_builtin() {
157 let reg = FunctionRegistry::new();
158 assert!(reg.is_aggregate("COUNT"));
159 assert!(reg.is_aggregate("sum"));
160 assert!(!reg.is_aggregate("vector_distance"));
161 assert!(reg.is_window("row_number"));
162 assert_eq!(
163 reg.search_trigger("vector_distance"),
164 SearchTrigger::VectorSearch
165 );
166 assert_eq!(
167 reg.search_trigger("st_dwithin"),
168 SearchTrigger::SpatialDWithin
169 );
170 assert_eq!(reg.search_trigger("time_bucket"), SearchTrigger::TimeBucket);
171 }
172
173 #[test]
174 fn chunk_text_renamed_to_ndb_chunk_text() {
175 let reg = FunctionRegistry::new();
176 assert!(
177 reg.lookup("ndb_chunk_text").is_some(),
178 "ndb_chunk_text must be registered"
179 );
180 assert!(
181 reg.lookup("chunk_text").is_none(),
182 "old name chunk_text must not exist"
183 );
184 }
185
186 #[test]
187 fn unimplemented_functions_removed() {
188 let reg = FunctionRegistry::new();
189 assert!(reg.lookup("currency").is_none());
190 assert!(reg.lookup("distribute").is_none());
191 assert!(reg.lookup("allocate").is_none());
192 assert!(reg.lookup("resolve_permission").is_none());
193 assert!(reg.lookup("convert_currency").is_none());
194 }
195
196 #[test]
197 fn all_builtins_have_since_set() {
198 let reg = FunctionRegistry::new();
199 let v0_1_0 = Version::new(0, 1, 0);
200 for f in ®.functions {
201 assert_eq!(
202 f.since, v0_1_0,
203 "function '{}' must have since = Version::new(0, 1, 0)",
204 f.name
205 );
206 }
207 }
208
209 #[test]
210 fn all_builtins_arg_counts_consistent() {
211 let reg = FunctionRegistry::new();
212 for f in ®.functions {
213 let n = f.arg_types.len();
214 let last_variadic = f.arg_types.last().is_some_and(|a| a.variadic);
215 if last_variadic {
216 assert!(
218 f.min_args <= n,
219 "function '{}': min_args ({}) > arg_types.len() ({}) with variadic last arg",
220 f.name,
221 f.min_args,
222 n
223 );
224 } else {
225 assert!(
227 f.min_args <= n,
228 "function '{}': min_args ({}) > arg_types.len() ({})",
229 f.name,
230 f.min_args,
231 n
232 );
233 assert!(
234 n <= f.max_args,
235 "function '{}': arg_types.len() ({}) > max_args ({})",
236 f.name,
237 n,
238 f.max_args
239 );
240 }
241 }
242 }
243
244 #[test]
245 fn return_type_spot_checks() {
246 let reg = FunctionRegistry::new();
247 assert_eq!(
248 reg.lookup("now").and_then(|f| f.return_type),
249 Some(ColumnType::Timestamptz),
250 "now() must return Timestamptz"
251 );
252 assert_eq!(
253 reg.lookup("count").and_then(|f| f.return_type),
254 Some(ColumnType::Int64),
255 "count must return Int64"
256 );
257 assert_eq!(
258 reg.lookup("doc_exists").and_then(|f| f.return_type),
259 Some(ColumnType::Bool),
260 "doc_exists must return Bool"
261 );
262 assert_eq!(
263 reg.lookup("st_contains").and_then(|f| f.return_type),
264 Some(ColumnType::Bool),
265 "st_contains must return Bool"
266 );
267 assert_eq!(
268 reg.lookup("pg_fts_match").and_then(|f| f.return_type),
269 Some(ColumnType::Bool),
270 "pg_fts_match must return Bool"
271 );
272 assert_eq!(
273 reg.lookup("lower").and_then(|f| f.return_type),
274 Some(ColumnType::String),
275 "lower must return String"
276 );
277 }
278}