1use crate::model::{Literal, NamedNode, Term};
7use crate::OxirsError;
9use chrono::{DateTime, Datelike, Timelike, Utc};
10use regex::Regex;
11use std::collections::HashMap;
12use std::sync::Arc;
13
14pub struct FunctionRegistry {
16 functions: HashMap<String, FunctionImpl>,
18 extensions: HashMap<String, Arc<dyn CustomFunction>>,
20}
21
22pub enum FunctionImpl {
24 Native(NativeFunction),
26 JavaScript(String),
28 Wasm(Vec<u8>),
30}
31
32pub type NativeFunction = Arc<dyn Fn(&[Term]) -> Result<Term, OxirsError> + Send + Sync>;
34
35pub trait CustomFunction: Send + Sync {
37 fn execute(&self, args: &[Term]) -> Result<Term, OxirsError>;
39
40 fn metadata(&self) -> FunctionMetadata;
42}
43
44#[derive(Debug, Clone)]
46pub struct FunctionMetadata {
47 pub name: String,
49 pub description: String,
51 pub min_args: usize,
53 pub max_args: Option<usize>,
55 pub arg_types: Vec<ArgumentType>,
57 pub return_type: ReturnType,
59}
60
61#[derive(Debug, Clone)]
63pub enum ArgumentType {
64 Any,
66 String,
68 Numeric,
70 Boolean,
72 DateTime,
74 IRI,
76 Datatype(String),
78}
79
80#[derive(Debug, Clone)]
82pub enum ReturnType {
83 SameAsInput,
85 Fixed(ArgumentType),
87 Dynamic,
89}
90
91impl Default for FunctionRegistry {
92 fn default() -> Self {
93 Self::new()
94 }
95}
96
97impl FunctionRegistry {
98 pub fn new() -> Self {
100 let mut registry = FunctionRegistry {
101 functions: HashMap::new(),
102 extensions: HashMap::new(),
103 };
104
105 registry.register_sparql_12_functions();
106 registry
107 }
108
109 fn register_sparql_12_functions(&mut self) {
111 self.register_native("CONCAT", Arc::new(fn_concat));
113 self.register_native("STRLEN", Arc::new(fn_strlen));
114 self.register_native("SUBSTR", Arc::new(fn_substr));
115 self.register_native("REPLACE", Arc::new(fn_replace));
116 self.register_native("REGEX", Arc::new(fn_regex));
117 self.register_native("STRAFTER", Arc::new(fn_strafter));
118 self.register_native("STRBEFORE", Arc::new(fn_strbefore));
119 self.register_native("STRSTARTS", Arc::new(fn_strstarts));
120 self.register_native("STRENDS", Arc::new(fn_strends));
121 self.register_native("CONTAINS", Arc::new(fn_contains));
122 self.register_native("ENCODE_FOR_URI", Arc::new(fn_encode_for_uri));
123
124 self.register_native("UCASE", Arc::new(fn_ucase));
126 self.register_native("LCASE", Arc::new(fn_lcase));
127
128 self.register_native("ABS", Arc::new(fn_abs));
130 self.register_native("CEIL", Arc::new(fn_ceil));
131 self.register_native("FLOOR", Arc::new(fn_floor));
132 self.register_native("ROUND", Arc::new(fn_round));
133 self.register_native("RAND", Arc::new(fn_rand));
134
135 self.register_native("SQRT", Arc::new(fn_sqrt));
137 self.register_native("SIN", Arc::new(fn_sin));
138 self.register_native("COS", Arc::new(fn_cos));
139 self.register_native("TAN", Arc::new(fn_tan));
140 self.register_native("ASIN", Arc::new(fn_asin));
141 self.register_native("ACOS", Arc::new(fn_acos));
142 self.register_native("ATAN", Arc::new(fn_atan));
143 self.register_native("ATAN2", Arc::new(fn_atan2));
144 self.register_native("EXP", Arc::new(fn_exp));
145 self.register_native("LOG", Arc::new(fn_log));
146 self.register_native("LOG10", Arc::new(fn_log10));
147 self.register_native("POW", Arc::new(fn_pow));
148
149 self.register_native("NOW", Arc::new(fn_now));
151 self.register_native("YEAR", Arc::new(fn_year));
152 self.register_native("MONTH", Arc::new(fn_month));
153 self.register_native("DAY", Arc::new(fn_day));
154 self.register_native("HOURS", Arc::new(fn_hours));
155 self.register_native("MINUTES", Arc::new(fn_minutes));
156 self.register_native("SECONDS", Arc::new(fn_seconds));
157 self.register_native("TIMEZONE", Arc::new(fn_timezone));
158 self.register_native("TZ", Arc::new(fn_tz));
159
160 self.register_native("SHA1", Arc::new(fn_sha1));
162 self.register_native("SHA256", Arc::new(fn_sha256));
163 self.register_native("SHA384", Arc::new(fn_sha384));
164 self.register_native("SHA512", Arc::new(fn_sha512));
165 self.register_native("MD5", Arc::new(fn_md5));
166
167 self.register_native("STR", Arc::new(fn_str));
169 self.register_native("LANG", Arc::new(fn_lang));
170 self.register_native("DATATYPE", Arc::new(fn_datatype));
171 self.register_native("IRI", Arc::new(fn_iri));
172 self.register_native("URI", Arc::new(fn_iri)); self.register_native("BNODE", Arc::new(fn_bnode));
174 self.register_native("STRDT", Arc::new(fn_strdt));
175 self.register_native("STRLANG", Arc::new(fn_strlang));
176 self.register_native("UUID", Arc::new(fn_uuid));
177 self.register_native("STRUUID", Arc::new(fn_struuid));
178
179 self.register_native("COUNT", Arc::new(fn_count));
181 self.register_native("SUM", Arc::new(fn_sum));
182 self.register_native("AVG", Arc::new(fn_avg));
183 self.register_native("MIN", Arc::new(fn_min));
184 self.register_native("MAX", Arc::new(fn_max));
185 self.register_native("GROUP_CONCAT", Arc::new(fn_group_concat));
186 self.register_native("SAMPLE", Arc::new(fn_sample));
187
188 self.register_native("NOT", Arc::new(fn_not));
190 self.register_native("EXISTS", Arc::new(fn_exists));
191 self.register_native("NOT_EXISTS", Arc::new(fn_not_exists));
192 self.register_native("BOUND", Arc::new(fn_bound));
193 self.register_native("COALESCE", Arc::new(fn_coalesce));
194 self.register_native("IF", Arc::new(fn_if));
195
196 self.register_native("IN", Arc::new(fn_in));
198 self.register_native("NOT_IN", Arc::new(fn_not_in));
199 }
200
201 fn register_native(&mut self, name: &str, func: NativeFunction) {
203 self.functions
204 .insert(name.to_string(), FunctionImpl::Native(func));
205 }
206
207 pub fn register_custom(&mut self, func: Arc<dyn CustomFunction>) {
209 let metadata = func.metadata();
210 self.extensions.insert(metadata.name.clone(), func);
211 }
212
213 pub fn execute(&self, name: &str, args: &[Term]) -> Result<Term, OxirsError> {
215 if let Some(func) = self.functions.get(name) {
217 match func {
218 FunctionImpl::Native(f) => f(args),
219 FunctionImpl::JavaScript(_) => Err(OxirsError::Query(
220 "JavaScript functions not yet implemented".to_string(),
221 )),
222 FunctionImpl::Wasm(_) => Err(OxirsError::Query(
223 "WASM functions not yet implemented".to_string(),
224 )),
225 }
226 }
227 else if let Some(func) = self.extensions.get(name) {
229 func.execute(args)
230 } else {
231 Err(OxirsError::Query(format!("Unknown function: {name}")))
232 }
233 }
234}
235
236fn fn_concat(args: &[Term]) -> Result<Term, OxirsError> {
239 let mut result = String::new();
240
241 for arg in args {
242 match arg {
243 Term::Literal(lit) => result.push_str(lit.value()),
244 Term::NamedNode(nn) => result.push_str(nn.as_str()),
245 _ => {
246 return Err(OxirsError::Query(
247 "CONCAT requires string arguments".to_string(),
248 ))
249 }
250 }
251 }
252
253 Ok(Term::Literal(Literal::new(&result)))
254}
255
256fn fn_strlen(args: &[Term]) -> Result<Term, OxirsError> {
257 if args.len() != 1 {
258 return Err(OxirsError::Query(
259 "STRLEN requires exactly 1 argument".to_string(),
260 ));
261 }
262
263 match &args[0] {
264 Term::Literal(lit) => {
265 let len = lit.value().chars().count() as i64;
266 Ok(Term::Literal(Literal::new_typed(
267 len.to_string(),
268 NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").unwrap(),
269 )))
270 }
271 _ => Err(OxirsError::Query(
272 "STRLEN requires string literal".to_string(),
273 )),
274 }
275}
276
277fn fn_substr(args: &[Term]) -> Result<Term, OxirsError> {
278 if args.len() < 2 || args.len() > 3 {
279 return Err(OxirsError::Query(
280 "SUBSTR requires 2 or 3 arguments".to_string(),
281 ));
282 }
283
284 match (&args[0], &args[1]) {
285 (Term::Literal(str_lit), Term::Literal(start_lit)) => {
286 let string = str_lit.value();
287 let start = start_lit
288 .value()
289 .parse::<usize>()
290 .map_err(|_| OxirsError::Query("Invalid start position".to_string()))?;
291
292 let result = if args.len() == 3 {
293 match &args[2] {
294 Term::Literal(len_lit) => {
295 let len = len_lit
296 .value()
297 .parse::<usize>()
298 .map_err(|_| OxirsError::Query("Invalid length".to_string()))?;
299 string.chars().skip(start - 1).take(len).collect::<String>()
300 }
301 _ => return Err(OxirsError::Query("Length must be numeric".to_string())),
302 }
303 } else {
304 string.chars().skip(start - 1).collect::<String>()
305 };
306
307 Ok(Term::Literal(Literal::new(&result)))
308 }
309 _ => Err(OxirsError::Query(
310 "SUBSTR requires string and numeric arguments".to_string(),
311 )),
312 }
313}
314
315fn fn_replace(args: &[Term]) -> Result<Term, OxirsError> {
316 if args.len() < 3 || args.len() > 4 {
317 return Err(OxirsError::Query(
318 "REPLACE requires 3 or 4 arguments".to_string(),
319 ));
320 }
321
322 match (&args[0], &args[1], &args[2]) {
323 (Term::Literal(text), Term::Literal(pattern), Term::Literal(replacement)) => {
324 let flags = if args.len() == 4 {
325 match &args[3] {
326 Term::Literal(f) => f.value(),
327 _ => return Err(OxirsError::Query("Flags must be string".to_string())),
328 }
329 } else {
330 ""
331 };
332
333 let regex_str = if flags.contains('i') {
335 format!("(?i){}", pattern.value())
336 } else {
337 pattern.value().to_string()
338 };
339
340 let regex = Regex::new(®ex_str)
341 .map_err(|e| OxirsError::Query(format!("Invalid regex: {e}")))?;
342
343 let result = regex.replace_all(text.value(), replacement.value());
344 Ok(Term::Literal(Literal::new(result.as_ref())))
345 }
346 _ => Err(OxirsError::Query(
347 "REPLACE requires string arguments".to_string(),
348 )),
349 }
350}
351
352fn fn_regex(args: &[Term]) -> Result<Term, OxirsError> {
353 if args.len() < 2 || args.len() > 3 {
354 return Err(OxirsError::Query(
355 "REGEX requires 2 or 3 arguments".to_string(),
356 ));
357 }
358
359 match (&args[0], &args[1]) {
360 (Term::Literal(text), Term::Literal(pattern)) => {
361 let flags = if args.len() == 3 {
362 match &args[2] {
363 Term::Literal(f) => f.value(),
364 _ => return Err(OxirsError::Query("Flags must be string".to_string())),
365 }
366 } else {
367 ""
368 };
369
370 let regex_str = if flags.contains('i') {
371 format!("(?i){}", pattern.value())
372 } else {
373 pattern.value().to_string()
374 };
375
376 let regex = Regex::new(®ex_str)
377 .map_err(|e| OxirsError::Query(format!("Invalid regex: {e}")))?;
378
379 let matches = regex.is_match(text.value());
380 Ok(Term::Literal(Literal::new_typed(
381 if matches { "true" } else { "false" },
382 NamedNode::new("http://www.w3.org/2001/XMLSchema#boolean").unwrap(),
383 )))
384 }
385 _ => Err(OxirsError::Query(
386 "REGEX requires string arguments".to_string(),
387 )),
388 }
389}
390
391fn fn_strafter(args: &[Term]) -> Result<Term, OxirsError> {
394 if args.len() != 2 {
395 return Err(OxirsError::Query(
396 "STRAFTER requires exactly 2 arguments".to_string(),
397 ));
398 }
399
400 match (&args[0], &args[1]) {
401 (Term::Literal(str_lit), Term::Literal(after_lit)) => {
402 let string = str_lit.value();
403 let after = after_lit.value();
404
405 if let Some(pos) = string.find(after) {
406 let result = &string[pos + after.len()..];
407 Ok(Term::Literal(Literal::new(result)))
408 } else {
409 Ok(Term::Literal(Literal::new("")))
410 }
411 }
412 _ => Err(OxirsError::Query(
413 "STRAFTER requires string arguments".to_string(),
414 )),
415 }
416}
417
418fn fn_strbefore(args: &[Term]) -> Result<Term, OxirsError> {
419 if args.len() != 2 {
420 return Err(OxirsError::Query(
421 "STRBEFORE requires exactly 2 arguments".to_string(),
422 ));
423 }
424
425 match (&args[0], &args[1]) {
426 (Term::Literal(str_lit), Term::Literal(before_lit)) => {
427 let string = str_lit.value();
428 let before = before_lit.value();
429
430 if let Some(pos) = string.find(before) {
431 let result = &string[..pos];
432 Ok(Term::Literal(Literal::new(result)))
433 } else {
434 Ok(Term::Literal(Literal::new("")))
435 }
436 }
437 _ => Err(OxirsError::Query(
438 "STRBEFORE requires string arguments".to_string(),
439 )),
440 }
441}
442
443fn fn_strstarts(args: &[Term]) -> Result<Term, OxirsError> {
444 if args.len() != 2 {
445 return Err(OxirsError::Query(
446 "STRSTARTS requires exactly 2 arguments".to_string(),
447 ));
448 }
449
450 match (&args[0], &args[1]) {
451 (Term::Literal(str_lit), Term::Literal(prefix_lit)) => {
452 let result = str_lit.value().starts_with(prefix_lit.value());
453 Ok(Term::Literal(Literal::new_typed(
454 if result { "true" } else { "false" },
455 NamedNode::new("http://www.w3.org/2001/XMLSchema#boolean").unwrap(),
456 )))
457 }
458 _ => Err(OxirsError::Query(
459 "STRSTARTS requires string arguments".to_string(),
460 )),
461 }
462}
463
464fn fn_strends(args: &[Term]) -> Result<Term, OxirsError> {
465 if args.len() != 2 {
466 return Err(OxirsError::Query(
467 "STRENDS requires exactly 2 arguments".to_string(),
468 ));
469 }
470
471 match (&args[0], &args[1]) {
472 (Term::Literal(str_lit), Term::Literal(suffix_lit)) => {
473 let result = str_lit.value().ends_with(suffix_lit.value());
474 Ok(Term::Literal(Literal::new_typed(
475 if result { "true" } else { "false" },
476 NamedNode::new("http://www.w3.org/2001/XMLSchema#boolean").unwrap(),
477 )))
478 }
479 _ => Err(OxirsError::Query(
480 "STRENDS requires string arguments".to_string(),
481 )),
482 }
483}
484
485fn fn_contains(args: &[Term]) -> Result<Term, OxirsError> {
486 if args.len() != 2 {
487 return Err(OxirsError::Query(
488 "CONTAINS requires exactly 2 arguments".to_string(),
489 ));
490 }
491
492 match (&args[0], &args[1]) {
493 (Term::Literal(str_lit), Term::Literal(substr_lit)) => {
494 let result = str_lit.value().contains(substr_lit.value());
495 Ok(Term::Literal(Literal::new_typed(
496 if result { "true" } else { "false" },
497 NamedNode::new("http://www.w3.org/2001/XMLSchema#boolean").unwrap(),
498 )))
499 }
500 _ => Err(OxirsError::Query(
501 "CONTAINS requires string arguments".to_string(),
502 )),
503 }
504}
505
506fn fn_encode_for_uri(args: &[Term]) -> Result<Term, OxirsError> {
507 if args.len() != 1 {
508 return Err(OxirsError::Query(
509 "ENCODE_FOR_URI requires exactly 1 argument".to_string(),
510 ));
511 }
512
513 match &args[0] {
514 Term::Literal(lit) => {
515 let encoded = urlencoding::encode(lit.value());
516 Ok(Term::Literal(Literal::new(encoded.as_ref())))
517 }
518 _ => Err(OxirsError::Query(
519 "ENCODE_FOR_URI requires string argument".to_string(),
520 )),
521 }
522}
523
524fn fn_ucase(args: &[Term]) -> Result<Term, OxirsError> {
527 if args.len() != 1 {
528 return Err(OxirsError::Query(
529 "UCASE requires exactly 1 argument".to_string(),
530 ));
531 }
532
533 match &args[0] {
534 Term::Literal(lit) => Ok(Term::Literal(Literal::new(lit.value().to_uppercase()))),
535 _ => Err(OxirsError::Query(
536 "UCASE requires string argument".to_string(),
537 )),
538 }
539}
540
541fn fn_lcase(args: &[Term]) -> Result<Term, OxirsError> {
542 if args.len() != 1 {
543 return Err(OxirsError::Query(
544 "LCASE requires exactly 1 argument".to_string(),
545 ));
546 }
547
548 match &args[0] {
549 Term::Literal(lit) => Ok(Term::Literal(Literal::new(lit.value().to_lowercase()))),
550 _ => Err(OxirsError::Query(
551 "LCASE requires string argument".to_string(),
552 )),
553 }
554}
555
556fn fn_abs(args: &[Term]) -> Result<Term, OxirsError> {
559 if args.len() != 1 {
560 return Err(OxirsError::Query(
561 "ABS requires exactly 1 argument".to_string(),
562 ));
563 }
564
565 match &args[0] {
566 Term::Literal(lit) => {
567 let value = lit
568 .value()
569 .parse::<f64>()
570 .map_err(|_| OxirsError::Query("ABS requires numeric argument".to_string()))?;
571 let result = value.abs();
572
573 let dt = lit.datatype();
575 if dt.as_str() == "http://www.w3.org/2001/XMLSchema#integer" {
576 Ok(Term::Literal(Literal::new_typed(
577 (result as i64).to_string(),
578 NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").unwrap(),
579 )))
580 } else {
581 Ok(Term::Literal(Literal::new_typed(
582 result.to_string(),
583 NamedNode::new("http://www.w3.org/2001/XMLSchema#double").unwrap(),
584 )))
585 }
586 }
587 _ => Err(OxirsError::Query(
588 "ABS requires numeric literal".to_string(),
589 )),
590 }
591}
592
593fn fn_ceil(args: &[Term]) -> Result<Term, OxirsError> {
594 if args.len() != 1 {
595 return Err(OxirsError::Query(
596 "CEIL requires exactly 1 argument".to_string(),
597 ));
598 }
599
600 match &args[0] {
601 Term::Literal(lit) => {
602 let value = lit
603 .value()
604 .parse::<f64>()
605 .map_err(|_| OxirsError::Query("CEIL requires numeric argument".to_string()))?;
606 let result = value.ceil() as i64;
607 Ok(Term::Literal(Literal::new_typed(
608 result.to_string(),
609 NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").unwrap(),
610 )))
611 }
612 _ => Err(OxirsError::Query(
613 "CEIL requires numeric literal".to_string(),
614 )),
615 }
616}
617
618fn fn_floor(args: &[Term]) -> Result<Term, OxirsError> {
619 if args.len() != 1 {
620 return Err(OxirsError::Query(
621 "FLOOR requires exactly 1 argument".to_string(),
622 ));
623 }
624
625 match &args[0] {
626 Term::Literal(lit) => {
627 let value = lit
628 .value()
629 .parse::<f64>()
630 .map_err(|_| OxirsError::Query("FLOOR requires numeric argument".to_string()))?;
631 let result = value.floor() as i64;
632 Ok(Term::Literal(Literal::new_typed(
633 result.to_string(),
634 NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").unwrap(),
635 )))
636 }
637 _ => Err(OxirsError::Query(
638 "FLOOR requires numeric literal".to_string(),
639 )),
640 }
641}
642
643fn fn_round(args: &[Term]) -> Result<Term, OxirsError> {
644 if args.len() != 1 {
645 return Err(OxirsError::Query(
646 "ROUND requires exactly 1 argument".to_string(),
647 ));
648 }
649
650 match &args[0] {
651 Term::Literal(lit) => {
652 let value = lit
653 .value()
654 .parse::<f64>()
655 .map_err(|_| OxirsError::Query("ROUND requires numeric argument".to_string()))?;
656 let result = value.round() as i64;
657 Ok(Term::Literal(Literal::new_typed(
658 result.to_string(),
659 NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").unwrap(),
660 )))
661 }
662 _ => Err(OxirsError::Query(
663 "ROUND requires numeric literal".to_string(),
664 )),
665 }
666}
667
668fn fn_rand(_args: &[Term]) -> Result<Term, OxirsError> {
669 use scirs2_core::random::{Random, Rng};
670 let mut random = Random::default();
671 let value: f64 = random.random();
672 Ok(Term::Literal(Literal::new_typed(
673 value.to_string(),
674 NamedNode::new("http://www.w3.org/2001/XMLSchema#double").unwrap(),
675 )))
676}
677
678fn fn_sqrt(args: &[Term]) -> Result<Term, OxirsError> {
681 if args.len() != 1 {
682 return Err(OxirsError::Query(
683 "SQRT requires exactly 1 argument".to_string(),
684 ));
685 }
686
687 match &args[0] {
688 Term::Literal(lit) => {
689 let value = lit
690 .value()
691 .parse::<f64>()
692 .map_err(|_| OxirsError::Query("SQRT requires numeric argument".to_string()))?;
693 if value < 0.0 {
694 return Err(OxirsError::Query("SQRT of negative number".to_string()));
695 }
696 let result = value.sqrt();
697 Ok(Term::Literal(Literal::new_typed(
698 result.to_string(),
699 NamedNode::new("http://www.w3.org/2001/XMLSchema#double").unwrap(),
700 )))
701 }
702 _ => Err(OxirsError::Query(
703 "SQRT requires numeric literal".to_string(),
704 )),
705 }
706}
707
708fn fn_sin(args: &[Term]) -> Result<Term, OxirsError> {
709 if args.len() != 1 {
710 return Err(OxirsError::Query(
711 "SIN requires exactly 1 argument".to_string(),
712 ));
713 }
714
715 match &args[0] {
716 Term::Literal(lit) => {
717 let value = lit
718 .value()
719 .parse::<f64>()
720 .map_err(|_| OxirsError::Query("SIN requires numeric argument".to_string()))?;
721 let result = value.sin();
722 Ok(Term::Literal(Literal::new_typed(
723 result.to_string(),
724 NamedNode::new("http://www.w3.org/2001/XMLSchema#double").unwrap(),
725 )))
726 }
727 _ => Err(OxirsError::Query(
728 "SIN requires numeric literal".to_string(),
729 )),
730 }
731}
732
733fn fn_cos(args: &[Term]) -> Result<Term, OxirsError> {
734 if args.len() != 1 {
735 return Err(OxirsError::Query(
736 "COS requires exactly 1 argument".to_string(),
737 ));
738 }
739
740 match &args[0] {
741 Term::Literal(lit) => {
742 let value = lit
743 .value()
744 .parse::<f64>()
745 .map_err(|_| OxirsError::Query("COS requires numeric argument".to_string()))?;
746 let result = value.cos();
747 Ok(Term::Literal(Literal::new_typed(
748 result.to_string(),
749 NamedNode::new("http://www.w3.org/2001/XMLSchema#double").unwrap(),
750 )))
751 }
752 _ => Err(OxirsError::Query(
753 "COS requires numeric literal".to_string(),
754 )),
755 }
756}
757
758fn fn_tan(args: &[Term]) -> Result<Term, OxirsError> {
759 if args.len() != 1 {
760 return Err(OxirsError::Query(
761 "TAN requires exactly 1 argument".to_string(),
762 ));
763 }
764
765 match &args[0] {
766 Term::Literal(lit) => {
767 let value = lit
768 .value()
769 .parse::<f64>()
770 .map_err(|_| OxirsError::Query("TAN requires numeric argument".to_string()))?;
771 let result = value.tan();
772 Ok(Term::Literal(Literal::new_typed(
773 result.to_string(),
774 NamedNode::new("http://www.w3.org/2001/XMLSchema#double").unwrap(),
775 )))
776 }
777 _ => Err(OxirsError::Query(
778 "TAN requires numeric literal".to_string(),
779 )),
780 }
781}
782
783fn fn_asin(args: &[Term]) -> Result<Term, OxirsError> {
784 if args.len() != 1 {
785 return Err(OxirsError::Query(
786 "ASIN requires exactly 1 argument".to_string(),
787 ));
788 }
789
790 match &args[0] {
791 Term::Literal(lit) => {
792 let value = lit
793 .value()
794 .parse::<f64>()
795 .map_err(|_| OxirsError::Query("ASIN requires numeric argument".to_string()))?;
796 if !(-1.0..=1.0).contains(&value) {
797 return Err(OxirsError::Query(
798 "ASIN argument must be between -1 and 1".to_string(),
799 ));
800 }
801 let result = value.asin();
802 Ok(Term::Literal(Literal::new_typed(
803 result.to_string(),
804 NamedNode::new("http://www.w3.org/2001/XMLSchema#double").unwrap(),
805 )))
806 }
807 _ => Err(OxirsError::Query(
808 "ASIN requires numeric literal".to_string(),
809 )),
810 }
811}
812
813fn fn_acos(args: &[Term]) -> Result<Term, OxirsError> {
814 if args.len() != 1 {
815 return Err(OxirsError::Query(
816 "ACOS requires exactly 1 argument".to_string(),
817 ));
818 }
819
820 match &args[0] {
821 Term::Literal(lit) => {
822 let value = lit
823 .value()
824 .parse::<f64>()
825 .map_err(|_| OxirsError::Query("ACOS requires numeric argument".to_string()))?;
826 if !(-1.0..=1.0).contains(&value) {
827 return Err(OxirsError::Query(
828 "ACOS argument must be between -1 and 1".to_string(),
829 ));
830 }
831 let result = value.acos();
832 Ok(Term::Literal(Literal::new_typed(
833 result.to_string(),
834 NamedNode::new("http://www.w3.org/2001/XMLSchema#double").unwrap(),
835 )))
836 }
837 _ => Err(OxirsError::Query(
838 "ACOS requires numeric literal".to_string(),
839 )),
840 }
841}
842
843fn fn_atan(args: &[Term]) -> Result<Term, OxirsError> {
844 if args.len() != 1 {
845 return Err(OxirsError::Query(
846 "ATAN requires exactly 1 argument".to_string(),
847 ));
848 }
849
850 match &args[0] {
851 Term::Literal(lit) => {
852 let value = lit
853 .value()
854 .parse::<f64>()
855 .map_err(|_| OxirsError::Query("ATAN requires numeric argument".to_string()))?;
856 let result = value.atan();
857 Ok(Term::Literal(Literal::new_typed(
858 result.to_string(),
859 NamedNode::new("http://www.w3.org/2001/XMLSchema#double").unwrap(),
860 )))
861 }
862 _ => Err(OxirsError::Query(
863 "ATAN requires numeric literal".to_string(),
864 )),
865 }
866}
867
868fn fn_atan2(args: &[Term]) -> Result<Term, OxirsError> {
869 if args.len() != 2 {
870 return Err(OxirsError::Query(
871 "ATAN2 requires exactly 2 arguments".to_string(),
872 ));
873 }
874
875 match (&args[0], &args[1]) {
876 (Term::Literal(y_lit), Term::Literal(x_lit)) => {
877 let y = y_lit
878 .value()
879 .parse::<f64>()
880 .map_err(|_| OxirsError::Query("ATAN2 requires numeric arguments".to_string()))?;
881 let x = x_lit
882 .value()
883 .parse::<f64>()
884 .map_err(|_| OxirsError::Query("ATAN2 requires numeric arguments".to_string()))?;
885 let result = y.atan2(x);
886 Ok(Term::Literal(Literal::new_typed(
887 result.to_string(),
888 NamedNode::new("http://www.w3.org/2001/XMLSchema#double").unwrap(),
889 )))
890 }
891 _ => Err(OxirsError::Query(
892 "ATAN2 requires numeric literals".to_string(),
893 )),
894 }
895}
896
897fn fn_exp(args: &[Term]) -> Result<Term, OxirsError> {
898 if args.len() != 1 {
899 return Err(OxirsError::Query(
900 "EXP requires exactly 1 argument".to_string(),
901 ));
902 }
903
904 match &args[0] {
905 Term::Literal(lit) => {
906 let value = lit
907 .value()
908 .parse::<f64>()
909 .map_err(|_| OxirsError::Query("EXP requires numeric argument".to_string()))?;
910 let result = value.exp();
911 Ok(Term::Literal(Literal::new_typed(
912 result.to_string(),
913 NamedNode::new("http://www.w3.org/2001/XMLSchema#double").unwrap(),
914 )))
915 }
916 _ => Err(OxirsError::Query(
917 "EXP requires numeric literal".to_string(),
918 )),
919 }
920}
921
922fn fn_log(args: &[Term]) -> Result<Term, OxirsError> {
923 if args.len() != 1 {
924 return Err(OxirsError::Query(
925 "LOG requires exactly 1 argument".to_string(),
926 ));
927 }
928
929 match &args[0] {
930 Term::Literal(lit) => {
931 let value = lit
932 .value()
933 .parse::<f64>()
934 .map_err(|_| OxirsError::Query("LOG requires numeric argument".to_string()))?;
935 if value <= 0.0 {
936 return Err(OxirsError::Query("LOG of non-positive number".to_string()));
937 }
938 let result = value.ln();
939 Ok(Term::Literal(Literal::new_typed(
940 result.to_string(),
941 NamedNode::new("http://www.w3.org/2001/XMLSchema#double").unwrap(),
942 )))
943 }
944 _ => Err(OxirsError::Query(
945 "LOG requires numeric literal".to_string(),
946 )),
947 }
948}
949
950fn fn_log10(args: &[Term]) -> Result<Term, OxirsError> {
951 if args.len() != 1 {
952 return Err(OxirsError::Query(
953 "LOG10 requires exactly 1 argument".to_string(),
954 ));
955 }
956
957 match &args[0] {
958 Term::Literal(lit) => {
959 let value = lit
960 .value()
961 .parse::<f64>()
962 .map_err(|_| OxirsError::Query("LOG10 requires numeric argument".to_string()))?;
963 if value <= 0.0 {
964 return Err(OxirsError::Query(
965 "LOG10 of non-positive number".to_string(),
966 ));
967 }
968 let result = value.log10();
969 Ok(Term::Literal(Literal::new_typed(
970 result.to_string(),
971 NamedNode::new("http://www.w3.org/2001/XMLSchema#double").unwrap(),
972 )))
973 }
974 _ => Err(OxirsError::Query(
975 "LOG10 requires numeric literal".to_string(),
976 )),
977 }
978}
979
980fn fn_pow(args: &[Term]) -> Result<Term, OxirsError> {
981 if args.len() != 2 {
982 return Err(OxirsError::Query(
983 "POW requires exactly 2 arguments".to_string(),
984 ));
985 }
986
987 match (&args[0], &args[1]) {
988 (Term::Literal(base_lit), Term::Literal(exp_lit)) => {
989 let base = base_lit
990 .value()
991 .parse::<f64>()
992 .map_err(|_| OxirsError::Query("POW requires numeric arguments".to_string()))?;
993 let exp = exp_lit
994 .value()
995 .parse::<f64>()
996 .map_err(|_| OxirsError::Query("POW requires numeric arguments".to_string()))?;
997 let result = base.powf(exp);
998 Ok(Term::Literal(Literal::new_typed(
999 result.to_string(),
1000 NamedNode::new("http://www.w3.org/2001/XMLSchema#double").unwrap(),
1001 )))
1002 }
1003 _ => Err(OxirsError::Query(
1004 "POW requires numeric literals".to_string(),
1005 )),
1006 }
1007}
1008
1009fn fn_now(_args: &[Term]) -> Result<Term, OxirsError> {
1012 let now = Utc::now();
1013 Ok(Term::Literal(Literal::new_typed(
1014 now.to_rfc3339(),
1015 NamedNode::new("http://www.w3.org/2001/XMLSchema#dateTime").unwrap(),
1016 )))
1017}
1018
1019fn fn_year(args: &[Term]) -> Result<Term, OxirsError> {
1020 if args.len() != 1 {
1021 return Err(OxirsError::Query(
1022 "YEAR requires exactly 1 argument".to_string(),
1023 ));
1024 }
1025
1026 match &args[0] {
1027 Term::Literal(lit) => {
1028 let dt = DateTime::parse_from_rfc3339(lit.value())
1029 .map_err(|_| OxirsError::Query("Invalid dateTime".to_string()))?;
1030 Ok(Term::Literal(Literal::new_typed(
1031 dt.year().to_string(),
1032 NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").unwrap(),
1033 )))
1034 }
1035 _ => Err(OxirsError::Query(
1036 "YEAR requires dateTime literal".to_string(),
1037 )),
1038 }
1039}
1040
1041fn fn_month(args: &[Term]) -> Result<Term, OxirsError> {
1042 if args.len() != 1 {
1043 return Err(OxirsError::Query(
1044 "MONTH requires exactly 1 argument".to_string(),
1045 ));
1046 }
1047
1048 match &args[0] {
1049 Term::Literal(lit) => {
1050 let dt = DateTime::parse_from_rfc3339(lit.value())
1051 .map_err(|_| OxirsError::Query("Invalid dateTime".to_string()))?;
1052 Ok(Term::Literal(Literal::new_typed(
1053 dt.month().to_string(),
1054 NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").unwrap(),
1055 )))
1056 }
1057 _ => Err(OxirsError::Query(
1058 "MONTH requires dateTime literal".to_string(),
1059 )),
1060 }
1061}
1062
1063fn fn_day(args: &[Term]) -> Result<Term, OxirsError> {
1064 if args.len() != 1 {
1065 return Err(OxirsError::Query(
1066 "DAY requires exactly 1 argument".to_string(),
1067 ));
1068 }
1069
1070 match &args[0] {
1071 Term::Literal(lit) => {
1072 let dt = DateTime::parse_from_rfc3339(lit.value())
1073 .map_err(|_| OxirsError::Query("Invalid dateTime".to_string()))?;
1074 Ok(Term::Literal(Literal::new_typed(
1075 dt.day().to_string(),
1076 NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").unwrap(),
1077 )))
1078 }
1079 _ => Err(OxirsError::Query(
1080 "DAY requires dateTime literal".to_string(),
1081 )),
1082 }
1083}
1084
1085fn fn_hours(args: &[Term]) -> Result<Term, OxirsError> {
1086 if args.len() != 1 {
1087 return Err(OxirsError::Query(
1088 "HOURS requires exactly 1 argument".to_string(),
1089 ));
1090 }
1091
1092 match &args[0] {
1093 Term::Literal(lit) => {
1094 let dt = DateTime::parse_from_rfc3339(lit.value())
1095 .map_err(|_| OxirsError::Query("Invalid dateTime".to_string()))?;
1096 Ok(Term::Literal(Literal::new_typed(
1097 dt.hour().to_string(),
1098 NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").unwrap(),
1099 )))
1100 }
1101 _ => Err(OxirsError::Query(
1102 "HOURS requires dateTime literal".to_string(),
1103 )),
1104 }
1105}
1106
1107fn fn_minutes(args: &[Term]) -> Result<Term, OxirsError> {
1108 if args.len() != 1 {
1109 return Err(OxirsError::Query(
1110 "MINUTES requires exactly 1 argument".to_string(),
1111 ));
1112 }
1113
1114 match &args[0] {
1115 Term::Literal(lit) => {
1116 let dt = DateTime::parse_from_rfc3339(lit.value())
1117 .map_err(|_| OxirsError::Query("Invalid dateTime".to_string()))?;
1118 Ok(Term::Literal(Literal::new_typed(
1119 dt.minute().to_string(),
1120 NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").unwrap(),
1121 )))
1122 }
1123 _ => Err(OxirsError::Query(
1124 "MINUTES requires dateTime literal".to_string(),
1125 )),
1126 }
1127}
1128
1129fn fn_seconds(args: &[Term]) -> Result<Term, OxirsError> {
1130 if args.len() != 1 {
1131 return Err(OxirsError::Query(
1132 "SECONDS requires exactly 1 argument".to_string(),
1133 ));
1134 }
1135
1136 match &args[0] {
1137 Term::Literal(lit) => {
1138 let dt = DateTime::parse_from_rfc3339(lit.value())
1139 .map_err(|_| OxirsError::Query("Invalid dateTime".to_string()))?;
1140 Ok(Term::Literal(Literal::new_typed(
1141 format!("{}.{:09}", dt.second(), dt.nanosecond()),
1142 NamedNode::new("http://www.w3.org/2001/XMLSchema#decimal").unwrap(),
1143 )))
1144 }
1145 _ => Err(OxirsError::Query(
1146 "SECONDS requires dateTime literal".to_string(),
1147 )),
1148 }
1149}
1150
1151fn fn_timezone(args: &[Term]) -> Result<Term, OxirsError> {
1152 if args.len() != 1 {
1153 return Err(OxirsError::Query(
1154 "TIMEZONE requires exactly 1 argument".to_string(),
1155 ));
1156 }
1157
1158 match &args[0] {
1159 Term::Literal(lit) => {
1160 let dt = DateTime::parse_from_rfc3339(lit.value())
1161 .map_err(|_| OxirsError::Query("Invalid dateTime".to_string()))?;
1162 let offset = dt.offset();
1163 let hours = offset.local_minus_utc() / 3600;
1164 let minutes = (offset.local_minus_utc() % 3600) / 60;
1165
1166 let duration = if hours == 0 && minutes == 0 {
1167 "PT0S".to_string()
1168 } else {
1169 format!("PT{}H{}M", hours.abs(), minutes.abs())
1170 };
1171
1172 Ok(Term::Literal(Literal::new_typed(
1173 &duration,
1174 NamedNode::new("http://www.w3.org/2001/XMLSchema#dayTimeDuration").unwrap(),
1175 )))
1176 }
1177 _ => Err(OxirsError::Query(
1178 "TIMEZONE requires dateTime literal".to_string(),
1179 )),
1180 }
1181}
1182
1183fn fn_tz(args: &[Term]) -> Result<Term, OxirsError> {
1184 if args.len() != 1 {
1185 return Err(OxirsError::Query(
1186 "TZ requires exactly 1 argument".to_string(),
1187 ));
1188 }
1189
1190 match &args[0] {
1191 Term::Literal(lit) => {
1192 let dt = DateTime::parse_from_rfc3339(lit.value())
1193 .map_err(|_| OxirsError::Query("Invalid dateTime".to_string()))?;
1194 let offset = dt.offset();
1195 let hours = offset.local_minus_utc() / 3600;
1196 let minutes = (offset.local_minus_utc() % 3600) / 60;
1197
1198 let tz_string = if hours == 0 && minutes == 0 {
1199 "Z".to_string()
1200 } else {
1201 format!("{:+03}:{:02}", hours, minutes.abs())
1202 };
1203
1204 Ok(Term::Literal(Literal::new(&tz_string)))
1205 }
1206 _ => Err(OxirsError::Query(
1207 "TZ requires dateTime literal".to_string(),
1208 )),
1209 }
1210}
1211
1212fn fn_sha1(args: &[Term]) -> Result<Term, OxirsError> {
1215 if args.len() != 1 {
1216 return Err(OxirsError::Query(
1217 "SHA1 requires exactly 1 argument".to_string(),
1218 ));
1219 }
1220
1221 match &args[0] {
1222 Term::Literal(lit) => {
1223 use sha1::{Digest, Sha1};
1224 let mut hasher = Sha1::new();
1225 hasher.update(lit.value().as_bytes());
1226 let result = hasher.finalize();
1227 let hex = format!("{result:x}");
1228 Ok(Term::Literal(Literal::new(&hex)))
1229 }
1230 _ => Err(OxirsError::Query(
1231 "SHA1 requires string literal".to_string(),
1232 )),
1233 }
1234}
1235
1236fn fn_sha256(args: &[Term]) -> Result<Term, OxirsError> {
1237 if args.len() != 1 {
1238 return Err(OxirsError::Query(
1239 "SHA256 requires exactly 1 argument".to_string(),
1240 ));
1241 }
1242
1243 match &args[0] {
1244 Term::Literal(lit) => {
1245 use sha2::{Digest, Sha256};
1246 let mut hasher = Sha256::new();
1247 hasher.update(lit.value().as_bytes());
1248 let result = hasher.finalize();
1249 let hex = format!("{result:x}");
1250 Ok(Term::Literal(Literal::new(&hex)))
1251 }
1252 _ => Err(OxirsError::Query(
1253 "SHA256 requires string literal".to_string(),
1254 )),
1255 }
1256}
1257
1258fn fn_sha384(args: &[Term]) -> Result<Term, OxirsError> {
1259 if args.len() != 1 {
1260 return Err(OxirsError::Query(
1261 "SHA384 requires exactly 1 argument".to_string(),
1262 ));
1263 }
1264
1265 match &args[0] {
1266 Term::Literal(lit) => {
1267 use sha2::{Digest, Sha384};
1268 let mut hasher = Sha384::new();
1269 hasher.update(lit.value().as_bytes());
1270 let result = hasher.finalize();
1271 let hex = format!("{result:x}");
1272 Ok(Term::Literal(Literal::new(&hex)))
1273 }
1274 _ => Err(OxirsError::Query(
1275 "SHA384 requires string literal".to_string(),
1276 )),
1277 }
1278}
1279
1280fn fn_sha512(args: &[Term]) -> Result<Term, OxirsError> {
1281 if args.len() != 1 {
1282 return Err(OxirsError::Query(
1283 "SHA512 requires exactly 1 argument".to_string(),
1284 ));
1285 }
1286
1287 match &args[0] {
1288 Term::Literal(lit) => {
1289 use sha2::{Digest, Sha512};
1290 let mut hasher = Sha512::new();
1291 hasher.update(lit.value().as_bytes());
1292 let result = hasher.finalize();
1293 let hex = format!("{result:x}");
1294 Ok(Term::Literal(Literal::new(&hex)))
1295 }
1296 _ => Err(OxirsError::Query(
1297 "SHA512 requires string literal".to_string(),
1298 )),
1299 }
1300}
1301
1302fn fn_md5(args: &[Term]) -> Result<Term, OxirsError> {
1303 if args.len() != 1 {
1304 return Err(OxirsError::Query(
1305 "MD5 requires exactly 1 argument".to_string(),
1306 ));
1307 }
1308
1309 match &args[0] {
1310 Term::Literal(lit) => {
1311 let mut hasher = md5::Context::new();
1312 hasher.consume(lit.value().as_bytes());
1313 let result = hasher.finalize();
1314 let hex = format!("{result:x}");
1315 Ok(Term::Literal(Literal::new(&hex)))
1316 }
1317 _ => Err(OxirsError::Query("MD5 requires string literal".to_string())),
1318 }
1319}
1320
1321fn fn_str(args: &[Term]) -> Result<Term, OxirsError> {
1324 if args.len() != 1 {
1325 return Err(OxirsError::Query(
1326 "STR requires exactly 1 argument".to_string(),
1327 ));
1328 }
1329
1330 match &args[0] {
1331 Term::Literal(lit) => Ok(Term::Literal(Literal::new(lit.value()))),
1332 Term::NamedNode(nn) => Ok(Term::Literal(Literal::new(nn.as_str()))),
1333 _ => Err(OxirsError::Query("STR requires literal or IRI".to_string())),
1334 }
1335}
1336
1337fn fn_lang(args: &[Term]) -> Result<Term, OxirsError> {
1338 if args.len() != 1 {
1339 return Err(OxirsError::Query(
1340 "LANG requires exactly 1 argument".to_string(),
1341 ));
1342 }
1343
1344 match &args[0] {
1345 Term::Literal(lit) => {
1346 let lang = lit.language().unwrap_or("");
1347 Ok(Term::Literal(Literal::new(lang)))
1348 }
1349 _ => Err(OxirsError::Query("LANG requires literal".to_string())),
1350 }
1351}
1352
1353fn fn_datatype(args: &[Term]) -> Result<Term, OxirsError> {
1354 if args.len() != 1 {
1355 return Err(OxirsError::Query(
1356 "DATATYPE requires exactly 1 argument".to_string(),
1357 ));
1358 }
1359
1360 match &args[0] {
1361 Term::Literal(lit) => {
1362 let dt = lit.datatype();
1363 Ok(Term::NamedNode(NamedNode::new(dt.as_str()).unwrap()))
1364 }
1365 _ => Err(OxirsError::Query("DATATYPE requires literal".to_string())),
1366 }
1367}
1368
1369fn fn_iri(args: &[Term]) -> Result<Term, OxirsError> {
1370 if args.len() != 1 {
1371 return Err(OxirsError::Query(
1372 "IRI requires exactly 1 argument".to_string(),
1373 ));
1374 }
1375
1376 match &args[0] {
1377 Term::Literal(lit) => {
1378 let iri = NamedNode::new(lit.value())?;
1379 Ok(Term::NamedNode(iri))
1380 }
1381 Term::NamedNode(nn) => Ok(Term::NamedNode(nn.clone())),
1382 _ => Err(OxirsError::Query(
1383 "IRI requires string literal or IRI".to_string(),
1384 )),
1385 }
1386}
1387
1388fn fn_bnode(args: &[Term]) -> Result<Term, OxirsError> {
1389 use crate::model::BlankNode;
1390
1391 if args.is_empty() {
1392 Ok(Term::BlankNode(BlankNode::new_unique()))
1393 } else if args.len() == 1 {
1394 match &args[0] {
1395 Term::Literal(lit) => {
1396 let bnode = BlankNode::new(lit.value())?;
1397 Ok(Term::BlankNode(bnode))
1398 }
1399 _ => Err(OxirsError::Query(
1400 "BNODE requires string literal or no arguments".to_string(),
1401 )),
1402 }
1403 } else {
1404 Err(OxirsError::Query(
1405 "BNODE requires 0 or 1 arguments".to_string(),
1406 ))
1407 }
1408}
1409
1410fn fn_strdt(args: &[Term]) -> Result<Term, OxirsError> {
1411 if args.len() != 2 {
1412 return Err(OxirsError::Query(
1413 "STRDT requires exactly 2 arguments".to_string(),
1414 ));
1415 }
1416
1417 match (&args[0], &args[1]) {
1418 (Term::Literal(value_lit), Term::NamedNode(datatype)) => Ok(Term::Literal(
1419 Literal::new_typed(value_lit.value(), datatype.clone()),
1420 )),
1421 _ => Err(OxirsError::Query(
1422 "STRDT requires string literal and IRI".to_string(),
1423 )),
1424 }
1425}
1426
1427fn fn_strlang(args: &[Term]) -> Result<Term, OxirsError> {
1428 if args.len() != 2 {
1429 return Err(OxirsError::Query(
1430 "STRLANG requires exactly 2 arguments".to_string(),
1431 ));
1432 }
1433
1434 match (&args[0], &args[1]) {
1435 (Term::Literal(value_lit), Term::Literal(lang_lit)) => Ok(Term::Literal(
1436 Literal::new_lang(value_lit.value(), lang_lit.value())?,
1437 )),
1438 _ => Err(OxirsError::Query(
1439 "STRLANG requires two string literals".to_string(),
1440 )),
1441 }
1442}
1443
1444fn fn_uuid(_args: &[Term]) -> Result<Term, OxirsError> {
1445 use uuid::Uuid;
1446 let uuid = Uuid::new_v4();
1447 let iri = NamedNode::new(format!("urn:uuid:{uuid}"))?;
1448 Ok(Term::NamedNode(iri))
1449}
1450
1451fn fn_struuid(_args: &[Term]) -> Result<Term, OxirsError> {
1452 use uuid::Uuid;
1453 let uuid = Uuid::new_v4();
1454 Ok(Term::Literal(Literal::new(uuid.to_string())))
1455}
1456
1457fn fn_count(_args: &[Term]) -> Result<Term, OxirsError> {
1460 Err(OxirsError::Query(
1462 "COUNT is an aggregate function".to_string(),
1463 ))
1464}
1465
1466fn fn_sum(_args: &[Term]) -> Result<Term, OxirsError> {
1467 Err(OxirsError::Query(
1468 "SUM is an aggregate function".to_string(),
1469 ))
1470}
1471
1472fn fn_avg(_args: &[Term]) -> Result<Term, OxirsError> {
1473 Err(OxirsError::Query(
1474 "AVG is an aggregate function".to_string(),
1475 ))
1476}
1477
1478fn fn_min(_args: &[Term]) -> Result<Term, OxirsError> {
1479 Err(OxirsError::Query(
1480 "MIN is an aggregate function".to_string(),
1481 ))
1482}
1483
1484fn fn_max(_args: &[Term]) -> Result<Term, OxirsError> {
1485 Err(OxirsError::Query(
1486 "MAX is an aggregate function".to_string(),
1487 ))
1488}
1489
1490fn fn_group_concat(_args: &[Term]) -> Result<Term, OxirsError> {
1491 Err(OxirsError::Query(
1492 "GROUP_CONCAT is an aggregate function".to_string(),
1493 ))
1494}
1495
1496fn fn_sample(_args: &[Term]) -> Result<Term, OxirsError> {
1497 Err(OxirsError::Query(
1498 "SAMPLE is an aggregate function".to_string(),
1499 ))
1500}
1501
1502fn fn_not(args: &[Term]) -> Result<Term, OxirsError> {
1505 if args.len() != 1 {
1506 return Err(OxirsError::Query(
1507 "NOT requires exactly 1 argument".to_string(),
1508 ));
1509 }
1510
1511 match &args[0] {
1512 Term::Literal(lit) => {
1513 let value = lit.value() == "true";
1514 Ok(Term::Literal(Literal::new_typed(
1515 if !value { "true" } else { "false" },
1516 NamedNode::new("http://www.w3.org/2001/XMLSchema#boolean").unwrap(),
1517 )))
1518 }
1519 _ => Err(OxirsError::Query(
1520 "NOT requires boolean literal".to_string(),
1521 )),
1522 }
1523}
1524
1525fn fn_exists(_args: &[Term]) -> Result<Term, OxirsError> {
1526 Err(OxirsError::Query(
1528 "EXISTS requires graph pattern context".to_string(),
1529 ))
1530}
1531
1532fn fn_not_exists(_args: &[Term]) -> Result<Term, OxirsError> {
1533 Err(OxirsError::Query(
1535 "NOT EXISTS requires graph pattern context".to_string(),
1536 ))
1537}
1538
1539fn fn_bound(args: &[Term]) -> Result<Term, OxirsError> {
1540 if args.len() != 1 {
1541 return Err(OxirsError::Query(
1542 "BOUND requires exactly 1 argument".to_string(),
1543 ));
1544 }
1545
1546 let is_bound = !matches!(&args[0], Term::Variable(_));
1547 Ok(Term::Literal(Literal::new_typed(
1548 if is_bound { "true" } else { "false" },
1549 NamedNode::new("http://www.w3.org/2001/XMLSchema#boolean").unwrap(),
1550 )))
1551}
1552
1553fn fn_coalesce(args: &[Term]) -> Result<Term, OxirsError> {
1554 for arg in args {
1555 if !matches!(arg, Term::Variable(_)) {
1556 return Ok(arg.clone());
1557 }
1558 }
1559 Err(OxirsError::Query(
1560 "COALESCE: all arguments are unbound".to_string(),
1561 ))
1562}
1563
1564fn fn_if(args: &[Term]) -> Result<Term, OxirsError> {
1565 if args.len() != 3 {
1566 return Err(OxirsError::Query(
1567 "IF requires exactly 3 arguments".to_string(),
1568 ));
1569 }
1570
1571 match &args[0] {
1572 Term::Literal(condition) => {
1573 let is_true = condition.value() == "true";
1574 Ok(if is_true {
1575 args[1].clone()
1576 } else {
1577 args[2].clone()
1578 })
1579 }
1580 _ => Err(OxirsError::Query(
1581 "IF condition must be boolean".to_string(),
1582 )),
1583 }
1584}
1585
1586fn fn_in(args: &[Term]) -> Result<Term, OxirsError> {
1589 if args.len() < 2 {
1590 return Err(OxirsError::Query(
1591 "IN requires at least 2 arguments".to_string(),
1592 ));
1593 }
1594
1595 let value = &args[0];
1596 for item in &args[1..] {
1597 if value == item {
1598 return Ok(Term::Literal(Literal::new_typed(
1599 "true",
1600 NamedNode::new("http://www.w3.org/2001/XMLSchema#boolean").unwrap(),
1601 )));
1602 }
1603 }
1604
1605 Ok(Term::Literal(Literal::new_typed(
1606 "false",
1607 NamedNode::new("http://www.w3.org/2001/XMLSchema#boolean").unwrap(),
1608 )))
1609}
1610
1611fn fn_not_in(args: &[Term]) -> Result<Term, OxirsError> {
1612 match fn_in(args)? {
1613 Term::Literal(lit) => {
1614 let value = lit.value() == "true";
1615 Ok(Term::Literal(Literal::new_typed(
1616 if !value { "true" } else { "false" },
1617 NamedNode::new("http://www.w3.org/2001/XMLSchema#boolean").unwrap(),
1618 )))
1619 }
1620 _ => unreachable!(),
1621 }
1622}
1623
1624#[cfg(test)]
1625mod tests {
1626 use super::*;
1627
1628 #[test]
1629 fn test_string_functions() {
1630 let registry = FunctionRegistry::new();
1631
1632 let args = vec![
1634 Term::Literal(Literal::new("Hello")),
1635 Term::Literal(Literal::new(" ")),
1636 Term::Literal(Literal::new("World")),
1637 ];
1638 let result = registry.execute("CONCAT", &args).unwrap();
1639 match result {
1640 Term::Literal(lit) => assert_eq!(lit.value(), "Hello World"),
1641 _ => panic!("Expected literal"),
1642 }
1643
1644 let args = vec![Term::Literal(Literal::new("Hello"))];
1646 let result = registry.execute("STRLEN", &args).unwrap();
1647 match result {
1648 Term::Literal(lit) => assert_eq!(lit.value(), "5"),
1649 _ => panic!("Expected literal"),
1650 }
1651
1652 let args = vec![Term::Literal(Literal::new("hello"))];
1654 let result = registry.execute("UCASE", &args).unwrap();
1655 match result {
1656 Term::Literal(lit) => assert_eq!(lit.value(), "HELLO"),
1657 _ => panic!("Expected literal"),
1658 }
1659 }
1660
1661 #[test]
1662 fn test_numeric_functions() {
1663 let registry = FunctionRegistry::new();
1664
1665 let args = vec![Term::Literal(Literal::new_typed(
1667 "-42",
1668 NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").unwrap(),
1669 ))];
1670 let result = registry.execute("ABS", &args).unwrap();
1671 match result {
1672 Term::Literal(lit) => assert_eq!(lit.value(), "42"),
1673 _ => panic!("Expected literal"),
1674 }
1675
1676 let args = vec![Term::Literal(Literal::new_typed(
1678 "9",
1679 NamedNode::new("http://www.w3.org/2001/XMLSchema#double").unwrap(),
1680 ))];
1681 let result = registry.execute("SQRT", &args).unwrap();
1682 match result {
1683 Term::Literal(lit) => {
1684 let value: f64 = lit.value().parse().unwrap();
1685 assert!((value - 3.0).abs() < 0.0001);
1686 }
1687 _ => panic!("Expected literal"),
1688 }
1689 }
1690
1691 #[test]
1692 fn test_hash_functions() {
1693 let registry = FunctionRegistry::new();
1694
1695 let args = vec![Term::Literal(Literal::new("test"))];
1697 let result = registry.execute("SHA256", &args).unwrap();
1698 match result {
1699 Term::Literal(lit) => {
1700 assert_eq!(
1701 lit.value(),
1702 "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
1703 );
1704 }
1705 _ => panic!("Expected literal"),
1706 }
1707 }
1708}