1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct BuiltinSpec {
14 pub name: &'static str,
15 pub arity: Arity,
16 pub enum_arg_slots: &'static [usize],
17 pub type_arg_slots: &'static [usize],
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct BuiltinAlias {
22 pub alias: &'static str,
23 pub canonical: &'static str,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct Arity {
28 pub min: usize,
29 pub max: Option<usize>,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum FunctionId {
34 Builtin(&'static BuiltinSpec),
35 Aggregate(AggregateFunction),
36}
37
38impl FunctionId {
39 #[must_use]
40 pub const fn name(self) -> &'static str {
41 match self {
42 FunctionId::Builtin(spec) => spec.name,
43 FunctionId::Aggregate(function) => function.name(),
44 }
45 }
46
47 #[must_use]
48 pub const fn arity(self) -> Arity {
49 match self {
50 FunctionId::Builtin(spec) => spec.arity,
51 FunctionId::Aggregate(function) => function.arity(),
52 }
53 }
54
55 #[must_use]
56 pub const fn is_aggregate(self) -> bool {
57 matches!(self, FunctionId::Aggregate(_))
58 }
59
60 #[must_use]
61 pub fn eq_ignore_ascii_case(self, other: &str) -> bool {
62 self.name().eq_ignore_ascii_case(other)
63 }
64
65 #[must_use]
66 pub fn to_ascii_lowercase(self) -> String {
67 self.name().to_ascii_lowercase()
68 }
69
70 #[must_use]
71 pub const fn as_aggregate(self) -> Option<AggregateFunction> {
72 match self {
73 FunctionId::Aggregate(function) => Some(function),
74 FunctionId::Builtin(_) => None,
75 }
76 }
77
78 #[must_use]
79 pub fn builtin(name: &str) -> Option<Self> {
80 builtin_spec(name).map(FunctionId::Builtin)
81 }
82}
83
84impl std::fmt::Display for FunctionId {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 f.write_str(self.name())
87 }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum AggregateFunction {
92 Count,
93 Sum,
94 Avg,
95 Min,
96 Max,
97 Collect,
98 Stdev,
99 Stdevp,
100 PercentileCont,
101 PercentileDisc,
102}
103
104impl AggregateFunction {
105 #[must_use]
106 pub const fn name(self) -> &'static str {
107 match self {
108 AggregateFunction::Count => "count",
109 AggregateFunction::Sum => "sum",
110 AggregateFunction::Avg => "avg",
111 AggregateFunction::Min => "min",
112 AggregateFunction::Max => "max",
113 AggregateFunction::Collect => "collect",
114 AggregateFunction::Stdev => "stdev",
115 AggregateFunction::Stdevp => "stdevp",
116 AggregateFunction::PercentileCont => "percentilecont",
117 AggregateFunction::PercentileDisc => "percentiledisc",
118 }
119 }
120
121 #[must_use]
122 pub const fn arity(self) -> Arity {
123 match self {
124 AggregateFunction::Count => Arity {
125 min: 0,
126 max: Some(1),
127 },
128 AggregateFunction::Sum
129 | AggregateFunction::Avg
130 | AggregateFunction::Min
131 | AggregateFunction::Max
132 | AggregateFunction::Collect
133 | AggregateFunction::Stdev
134 | AggregateFunction::Stdevp => Arity {
135 min: 1,
136 max: Some(1),
137 },
138 AggregateFunction::PercentileCont | AggregateFunction::PercentileDisc => Arity {
139 min: 2,
140 max: Some(2),
141 },
142 }
143 }
144
145 #[must_use]
146 pub fn parse(name: &str) -> Option<Self> {
147 Some(match name {
148 "count" => Self::Count,
149 "sum" => Self::Sum,
150 "avg" => Self::Avg,
151 "min" => Self::Min,
152 "max" => Self::Max,
153 "collect" => Self::Collect,
154 "stdev" => Self::Stdev,
155 "stdevp" => Self::Stdevp,
156 "percentilecont" => Self::PercentileCont,
157 "percentiledisc" => Self::PercentileDisc,
158 _ => return None,
159 })
160 }
161}
162
163const fn spec(name: &'static str, min: usize, max: Option<usize>) -> BuiltinSpec {
164 BuiltinSpec {
165 name,
166 arity: Arity { min, max },
167 enum_arg_slots: &[],
168 type_arg_slots: &[],
169 }
170}
171
172const fn spec_enum(
173 name: &'static str,
174 min: usize,
175 max: Option<usize>,
176 enum_arg_slots: &'static [usize],
177) -> BuiltinSpec {
178 BuiltinSpec {
179 name,
180 arity: Arity { min, max },
181 enum_arg_slots,
182 type_arg_slots: &[],
183 }
184}
185
186const fn spec_type(
187 name: &'static str,
188 min: usize,
189 max: Option<usize>,
190 type_arg_slots: &'static [usize],
191) -> BuiltinSpec {
192 BuiltinSpec {
193 name,
194 arity: Arity { min, max },
195 enum_arg_slots: &[],
196 type_arg_slots,
197 }
198}
199
200const fn alias(alias: &'static str, canonical: &'static str) -> BuiltinAlias {
201 BuiltinAlias { alias, canonical }
202}
203
204pub const BUILTIN_SPECS: &[BuiltinSpec] = &[
205 spec("list.sum", 1, Some(1)),
207 spec("list.avg", 1, Some(1)),
208 spec("list.min", 1, Some(1)),
209 spec("list.max", 1, Some(1)),
210 spec("list.product", 1, Some(1)),
211 spec("list.stdev", 1, Some(1)),
212 spec("list.median", 1, Some(1)),
213 spec("list.sort", 1, Some(2)),
214 spec("list.reverse", 1, Some(1)),
215 spec("list.unique", 1, Some(1)),
216 spec("list.first", 1, Some(1)),
217 spec("list.rest", 1, Some(1)),
218 spec("list.init", 1, Some(1)),
219 spec("list.last", 1, Some(1)),
220 spec("list.at", 2, Some(2)),
221 spec("list.slice", 2, Some(3)),
222 spec("list.size", 1, Some(1)),
223 spec("list.range", 2, Some(3)),
224 spec("list.contains", 2, Some(2)),
225 spec("list.contains_all", 2, Some(2)),
226 spec("list.has_duplicates", 1, Some(1)),
227 spec("list.all_distinct", 1, Some(1)),
228 spec("list.equal_unordered", 2, Some(2)),
229 spec("list.is_empty", 1, Some(1)),
230 spec("list.index_of", 2, Some(2)),
231 spec("list.indexes_of", 2, Some(2)),
232 spec("list.find_duplicates", 1, Some(1)),
233 spec("list.count_by", 1, Some(1)),
234 spec("list.union", 2, Some(2)),
235 spec("list.intersect", 2, Some(2)),
236 spec("list.diff", 2, Some(2)),
237 spec("list.symmetric_diff", 2, Some(2)),
238 spec("list.zip", 2, Some(2)),
239 spec("list.chunks", 2, Some(2)),
240 spec("list.split_by", 2, Some(2)),
241 spec("list.windows", 2, Some(3)),
242 spec("list.scan", 2, Some(2)),
243 spec("list.repeat", 2, Some(2)),
244 spec("list.flatten", 1, Some(2)),
245 spec("list.sample", 1, Some(2)),
246 spec("list.shuffle", 1, Some(1)),
247 spec("list.combinations", 2, Some(2)),
248 spec("list.concat", 2, None),
249 spec("list.append", 2, Some(2)),
250 spec("list.prepend", 2, Some(2)),
251 spec("list.take", 2, Some(2)),
252 spec("list.drop", 2, Some(2)),
253 spec("list.take_last", 2, Some(2)),
254 spec("list.drop_last", 2, Some(2)),
255 spec("list.insert", 3, Some(3)),
256 spec("list.remove", 2, Some(2)),
257 spec("list.compact", 1, Some(1)),
258 spec("string.upper", 1, Some(1)),
260 spec("string.lower", 1, Some(1)),
261 spec("string.capitalize", 1, Some(2)),
262 spec("string.case", 2, Some(2)),
263 spec("string.replace", 3, Some(4)),
264 spec("string.find", 2, Some(3)),
265 spec("string.count", 2, Some(2)),
266 spec("string.before", 2, Some(2)),
267 spec("string.after", 2, Some(2)),
268 spec("string.split", 2, Some(2)),
269 spec("string.join", 2, Some(2)),
270 spec("string.pad", 3, Some(4)),
271 spec("string.pad_left", 2, Some(3)),
272 spec("string.pad_right", 2, Some(3)),
273 spec("string.repeat", 2, Some(2)),
274 spec("string.slugify", 1, Some(1)),
275 spec("string.escape", 2, Some(2)),
276 spec("string.hex", 1, Some(1)),
277 spec("string.char_at", 2, Some(2)),
278 spec("string.code_at", 2, Some(2)),
279 spec("string.regex_groups", 2, Some(3)),
280 spec("string.matches", 2, Some(2)),
281 spec("string.starts_with", 2, Some(2)),
282 spec("string.ends_with", 2, Some(2)),
283 spec("string.contains", 2, Some(2)),
284 spec("string.words", 1, Some(1)),
285 spec("string.is_blank", 1, Some(1)),
286 spec("string.length", 1, Some(1)),
287 spec("string.url_encode", 1, Some(1)),
288 spec("string.url_decode", 1, Some(1)),
289 spec("string.swap_case", 1, Some(1)),
290 spec("string.trim", 1, Some(2)),
291 spec("string.trim_left", 1, Some(1)),
292 spec("string.trim_right", 1, Some(1)),
293 spec("string.slice", 2, Some(3)),
294 spec("string.prefix", 2, Some(2)),
295 spec("string.suffix", 2, Some(2)),
296 spec("string.reverse", 1, Some(1)),
297 spec("string.normalize", 1, Some(2)),
298 spec("text.distance", 3, Some(3)),
300 spec("text.similarity", 3, Some(3)),
301 spec("text.phonetic", 2, Some(2)),
302 spec("text.phonetic_match", 3, Some(3)),
303 spec("map.from", 1, Some(2)),
305 spec("map.set", 3, Some(3)),
306 spec("map.remove", 2, Some(2)),
307 spec("map.merge", 2, Some(3)),
308 spec("map.deep_merge", 2, Some(3)),
309 spec("map.compact", 1, Some(1)),
310 spec("map.group_by", 2, Some(2)),
311 spec("map.flatten", 1, Some(2)),
312 spec("map.unflatten", 1, Some(2)),
313 spec("map.get_path", 2, Some(3)),
314 spec("map.set_path", 3, Some(3)),
315 spec("map.remove_path", 2, Some(2)),
316 spec("map.entries", 1, Some(2)),
317 spec("map.values", 1, Some(2)),
318 spec("map.keys", 1, Some(1)),
319 spec("map.has_key", 2, Some(2)),
320 spec("map.pick", 2, Some(2)),
321 spec("map.rename", 3, Some(3)),
322 spec("map.invert", 1, Some(1)),
323 spec("map.get", 2, Some(3)),
324 spec("map.size", 1, Some(1)),
325 spec("map.index_by", 2, Some(2)),
326 spec("number.format", 1, Some(3)),
328 spec("number.to_base", 2, Some(2)),
329 spec("number.from_base", 2, Some(2)),
330 spec("number.to_roman", 1, Some(1)),
331 spec("number.from_roman", 1, Some(1)),
332 spec("bits.and", 2, Some(2)),
333 spec("bits.or", 2, Some(2)),
334 spec("bits.xor", 2, Some(2)),
335 spec("bits.shift_left", 2, Some(2)),
336 spec("bits.shift_right", 2, Some(2)),
337 spec("bits.not", 1, Some(1)),
338 spec("number.bitop", 3, Some(3)),
339 spec("number.is_integer", 1, Some(1)),
340 spec("number.is_even", 1, Some(1)),
341 spec("number.is_odd", 1, Some(1)),
342 spec("number.is_positive", 1, Some(1)),
343 spec("number.is_negative", 1, Some(1)),
344 spec("number.is_zero", 1, Some(1)),
345 spec("number.is_nan", 1, Some(1)),
346 spec("number.is_finite", 1, Some(1)),
347 spec("number.is_infinite", 1, Some(1)),
348 spec("math.min", 1, None),
350 spec("math.max", 1, None),
351 spec("math.round", 1, Some(3)),
352 spec("math.trunc", 1, Some(1)),
353 spec("math.sigmoid", 1, Some(1)),
354 spec("math.tanh", 1, Some(1)),
355 spec("math.cosh", 1, Some(1)),
356 spec("math.sinh", 1, Some(1)),
357 spec("math.cot", 1, Some(1)),
358 spec("math.coth", 1, Some(1)),
359 spec("math.atan2", 2, Some(2)),
360 spec("math.pow", 2, Some(2)),
361 spec("math.hypot", 2, Some(2)),
362 spec("math.log_base", 2, Some(2)),
363 spec("math.gcd", 2, Some(2)),
364 spec("math.lcm", 2, Some(2)),
365 spec("math.clamp", 3, Some(3)),
366 spec("math.lerp", 3, Some(3)),
367 spec("math.abs", 1, Some(1)),
368 spec("math.ceil", 1, Some(1)),
369 spec("math.floor", 1, Some(1)),
370 spec("math.sqrt", 1, Some(1)),
371 spec("math.sign", 1, Some(1)),
372 spec("math.log", 1, Some(1)),
373 spec("math.ln", 1, Some(1)),
374 spec("math.log10", 1, Some(1)),
375 spec("math.exp", 1, Some(1)),
376 spec("math.sin", 1, Some(1)),
377 spec("math.cos", 1, Some(1)),
378 spec("math.tan", 1, Some(1)),
379 spec("math.asin", 1, Some(1)),
380 spec("math.acos", 1, Some(1)),
381 spec("math.atan", 1, Some(1)),
382 spec("math.degrees", 1, Some(1)),
383 spec("math.radians", 1, Some(1)),
384 spec("math.pi", 0, Some(0)),
385 spec("math.e", 0, Some(0)),
386 spec("math.random", 0, Some(0)),
387 spec("temporal.now", 0, Some(1)),
389 spec("temporal.today", 0, Some(0)),
390 spec("temporal.timestamp", 0, Some(0)),
391 spec("temporal.timezone", 0, Some(0)),
392 spec("temporal.parse", 1, Some(3)),
393 spec("temporal.format", 1, Some(2)),
394 spec("temporal.reformat", 3, Some(3)),
395 spec("temporal.convert", 3, Some(3)),
396 spec("temporal.add", 2, Some(2)),
397 spec("temporal.get", 2, Some(2)),
398 spec("temporal.fields", 1, Some(1)),
399 spec("temporal.truncate", 2, Some(2)),
400 spec("temporal.between", 2, Some(2)),
401 spec("temporal.in_days", 2, Some(2)),
402 spec("bytes.size", 1, Some(1)),
404 spec("bytes.from_string", 1, Some(2)),
405 spec("bytes.to_string", 1, Some(2)),
406 spec("bytes.base64_encode", 1, Some(1)),
407 spec("bytes.base64_decode", 1, Some(1)),
408 spec("bytes.hex_encode", 1, Some(1)),
409 spec("bytes.hex_decode", 1, Some(1)),
410 spec("bytes.compress", 1, Some(2)),
411 spec("bytes.decompress", 1, Some(2)),
412 spec("crypto.blake3", 1, Some(1)),
414 spec("crypto.crc32", 1, Some(1)),
415 spec("uuid.new", 0, Some(0)),
417 spec("uuid.from_string", 1, Some(1)),
418 spec("uuid.is_valid", 1, Some(1)),
419 spec("json.encode", 1, Some(2)),
421 spec("json.decode", 1, Some(1)),
422 spec("json.path", 2, Some(2)),
423 spec("geo.distance", 2, Some(2)),
425 spec("geo.within_bbox", 3, Some(3)),
426 spec("vector.dimension", 1, Some(1)),
428 spec_enum("vector.distance", 3, Some(3), &[2]),
429 spec("vector.similarity", 2, Some(3)),
430 spec_enum("vector.norm", 2, Some(2), &[1]),
431 spec_enum("vector.coordinates", 2, Some(2), &[1]),
432 spec("node.id", 1, Some(1)),
434 spec("node.labels", 1, Some(1)),
435 spec("node.has_label", 2, Some(2)),
436 spec("node.keys", 1, Some(1)),
437 spec("node.properties", 1, Some(1)),
438 spec("edge.id", 1, Some(1)),
440 spec("edge.type", 1, Some(1)),
441 spec("edge.keys", 1, Some(1)),
442 spec("edge.properties", 1, Some(1)),
443 spec("edge.start", 1, Some(1)),
444 spec("edge.end", 1, Some(1)),
445 spec("path.nodes", 1, Some(1)),
447 spec("path.edges", 1, Some(1)),
448 spec("path.length", 1, Some(1)),
449 spec("path.first", 1, Some(1)),
450 spec("path.last", 1, Some(1)),
451 spec("value.size", 1, Some(1)),
453 spec("value.keys", 1, Some(1)),
454 spec("value.properties", 1, Some(1)),
455 spec("value.reverse", 1, Some(1)),
456 spec("value.coalesce", 1, None),
457 spec("value.is_null", 1, Some(1)),
458 spec("value.is_not_null", 1, Some(1)),
459 spec("value.id", 1, Some(1)),
460 spec("type.of", 1, Some(1)),
462 spec_type("type.is", 2, Some(2), &[1]),
463 spec_type("cast.to", 2, Some(2), &[1]),
465 spec_type("cast.try", 2, Some(2), &[1]),
466 spec_type("cast.can", 2, Some(2), &[1]),
467];
468
469pub const BUILTIN_ALIASES: &[BuiltinAlias] = &[
470 alias("list.find_index", "list.index_of"),
472 alias("list.find_indexes", "list.indexes_of"),
473 alias("vector.dim", "vector.dimension"),
474 alias("value.first_non_null", "value.coalesce"),
475 alias("type.cast", "cast.to"),
476 alias("type.try_cast", "cast.try"),
477 alias("type.can_cast", "cast.can"),
478 alias("now", "temporal.now"),
479 alias("datetime", "temporal.now"),
480 alias("timestamp", "temporal.timestamp"),
481 alias("timezone", "temporal.timezone"),
482 alias("new", "uuid.new"),
483 alias("random", "math.random"),
484 alias("rand", "math.random"),
485 alias("range", "list.range"),
486 alias("head", "list.first"),
488 alias("last", "list.last"),
489 alias("coalesce", "value.coalesce"),
490 alias("tolower", "string.lower"),
491 alias("toupper", "string.upper"),
492 alias("left", "string.prefix"),
493 alias("right", "string.suffix"),
494 alias("substring", "string.slice"),
495 alias("reverse", "value.reverse"),
496 alias("size", "value.size"),
497 alias("length", "path.length"),
498 alias("keys", "value.keys"),
499 alias("properties", "value.properties"),
500 alias("id", "value.id"),
501 alias("labels", "node.labels"),
502 alias("type", "edge.type"),
503 alias("randomuuid", "uuid.new"),
504 alias("tostring", "cast.to"),
505 alias("tointeger", "cast.to"),
506 alias("tofloat", "cast.to"),
507 alias("toboolean", "cast.to"),
508 alias("tointegerornull", "cast.try"),
509 alias("tofloatornull", "cast.try"),
510 alias("tobooleanornull", "cast.try"),
511 alias("tostringornull", "cast.try"),
512];
513
514pub fn builtin_spec(name: &str) -> Option<&'static BuiltinSpec> {
515 canonical_builtin_name(name)
516 .and_then(|canonical| BUILTIN_SPECS.iter().find(|spec| spec.name == canonical))
517}
518
519pub fn namespaced_arity(name: &str) -> Option<(usize, Option<usize>)> {
520 builtin_spec(name).map(|spec| (spec.arity.min, spec.arity.max))
521}
522
523pub fn accepts_enum_literal(name: &str, arg_idx: usize) -> bool {
524 builtin_spec(name).is_some_and(|spec| spec.enum_arg_slots.contains(&arg_idx))
525}
526
527pub fn accepts_type_literal(name: &str, arg_idx: usize) -> bool {
528 builtin_spec(name).is_some_and(|spec| spec.type_arg_slots.contains(&arg_idx))
529}
530
531pub fn resolve_function(name: &str) -> Option<FunctionId> {
532 let lower = name.to_ascii_lowercase();
533 builtin_spec(&lower)
534 .map(FunctionId::Builtin)
535 .or_else(|| AggregateFunction::parse(&lower).map(FunctionId::Aggregate))
536}
537
538pub fn canonical_builtin_name(name: &str) -> Option<&'static str> {
539 BUILTIN_SPECS
540 .iter()
541 .find(|spec| spec.name == name)
542 .map(|spec| spec.name)
543 .or_else(|| {
544 BUILTIN_ALIASES
545 .iter()
546 .find(|alias| alias.alias == name)
547 .map(|alias| alias.canonical)
548 })
549}