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